Jelajahi Sumber

Add all rubrics.

Lee Morgan 2 tahun lalu
induk
melakukan
fc8d5aec6a
20 mengubah file dengan 25599 tambahan dan 0 penghapusan
  1. 3 0
      routes.js
  2. 1325 0
      rubrics/mod01.html
  3. 1306 0
      rubrics/mod02.html
  4. 1327 0
      rubrics/mod03.html
  5. 1292 0
      rubrics/mod04.html
  6. 1304 0
      rubrics/mod05.html
  7. 1430 0
      rubrics/mod06.html
  8. 1262 0
      rubrics/mod08.html
  9. 1288 0
      rubrics/mod09.html
  10. 1286 0
      rubrics/mod10.html
  11. 1237 0
      rubrics/mod11.html
  12. 1362 0
      rubrics/mod12.html
  13. 1430 0
      rubrics/mod13.html
  14. 1406 0
      rubrics/mod14.html
  15. 1284 0
      rubrics/mod17.html
  16. 1588 0
      rubrics/mod18.html
  17. 1346 0
      rubrics/mod19.html
  18. 1367 0
      rubrics/mod20.html
  19. 1353 0
      rubrics/mod21.html
  20. 1403 0
      rubrics/mod22.html

+ 3 - 0
routes.js

@@ -74,4 +74,7 @@ module.exports = function(app){
     app.get("/tracker", (req, res)=>res.sendFile(`${views}/timeTracker/tracker.html`));
     app.post("/tracker", tracker.create);
     app.put("/tracker", tracker.update);
+
+    //2u RUBRICS
+    
 }

+ 1325 - 0
rubrics/mod01.html

@@ -0,0 +1,1325 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+    <title>Module 1 HTML & CSS Challenge Rubric</title>
+    <style>
+      *,
+      *::before,
+      *::after {
+        box-sizing: border-box;
+      }
+
+      html,
+      body {
+        margin: 0;
+        padding: 0;
+        line-height: 1.15;
+        font-size: 100%;
+        color: #fefefe;
+        transition: all 0.4s;
+      }
+
+      body {
+        background-color: #fbfcfd;
+        font-family: "Inter", sans-serif;
+        color: #4a4a4a;
+      }
+
+      #app.dark {
+        background-color: #121212;
+        color: #e2e2e2;
+      }
+
+      h1 {
+        font-size: 1.9rem;
+      }
+
+      h2 {
+        font-size: 1.6rem;
+      }
+
+      h3 {
+        font-size: 1.2rem;
+      }
+
+      h4 {
+        font-size: 0.9rem;
+      }
+
+      p,
+      legend {
+        font-size: 1.2rem;
+      }
+
+      label {
+        font-size: 0.9rem;
+        line-height: 2;
+      }
+
+      fieldset {
+        border: none;
+        padding: 0;
+        margin-top: 0.9rem;
+      }
+
+      input[type="radio"],
+      input[type="checkbox"] {
+        position: absolute !important;
+        height: 1px;
+        width: 1px;
+        overflow: hidden;
+        clip: rect(1px 1px 1px 1px);
+        clip: rect(1px, 1px, 1px, 1px);
+        white-space: nowrap;
+      }
+
+      .select-lg,
+      .select-lg option {
+        font-family: "Inter", sans-serif;
+        padding: 10px;
+      }
+
+      .narrow {
+        width: 800px;
+      }
+
+      #app {
+        padding: 3%;
+        transition: all 0.4s;
+      }
+
+      #app h1 {
+        font-size: 1.9rem;
+        font-weight: 700;
+        line-height: 1.3;
+      }
+
+      header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+      }
+
+      .switch-group {
+        display: flex;
+        align-items: center;
+        height: 2.25rem;
+        width: 4.5rem;
+      }
+
+      input[type="checkbox"] + .toggle {
+        height: 100%;
+        width: 100%;
+        background-color: #77a1db;
+        border-radius: 1.9rem;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        padding: 0.35rem;
+        cursor: pointer;
+        overflow: hidden;
+        background-position: 25% 125%;
+        transition: all 0.5s;
+      }
+
+      input[type="checkbox"]:focus + .toggle {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      input[type="checkbox"]:checked + .toggle {
+        background-color: rgb(85, 89, 204);
+        background-size: 1rem;
+        background-repeat: no-repeat;
+        background-position: 25%;
+        background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+        transition: all 0.5s;
+      }
+
+      input[type="checkbox"] + .toggle::after {
+        content: "";
+        display: block;
+        height: 1.7rem;
+        width: 1.7rem;
+        border-radius: 50%;
+        cursor: pointer;
+        border: 4px solid #e1be64;
+        background-color: #fddf74;
+        transform: translateX(0rem);
+        transition: all 0.5s;
+        background-position: center bottom;
+      }
+
+      input[type="checkbox"]:hover + .toggle::after {
+        transform: translateX(0rem) scale(1.1);
+      }
+
+      input[type="checkbox"]:checked + .toggle::after {
+        border: 4px solid #ffffff;
+        background-color: rgba(255, 255, 255, 0.4);
+        transform: translateX(2.1rem);
+        transition: all 0.5s;
+        background-position: center;
+        background-size: 60%;
+        background-repeat: no-repeat;
+        background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+      }
+
+      input[type="checkbox"]:checked:hover + .toggle::after {
+        transform: translateX(2.1rem) scale(1.075);
+      }
+
+      .card {
+        background-color: #fff;
+        border-radius: 6px;
+        border-width: 0;
+        box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+        font-size: 1.1rem;
+        height: 100%;
+        line-height: 1.8rem;
+        padding: 1.5rem 2rem;
+        width: 100%;
+        margin-bottom: 1.9rem;
+      }
+
+      .dark .card {
+        background-color: #333333;
+        box-shadow: none;
+      }
+
+      .card h1,
+      .card h2,
+      .card h3,
+      .card h4,
+      .card h5,
+      .card h6 {
+        color: #4a4a4a;
+      }
+
+      .dark .card h1,
+      .dark .card h2,
+      .dark .card h3,
+      .dark .card h4,
+      .dark .card h5,
+      .dark .card h6 {
+        color: #e2e2e2;
+      }
+
+      .card-header h2 {
+        display: inline-block;
+      }
+
+      .card .cat-score {
+        margin-left: 20px;
+        font-size: smaller;
+      }
+
+      .card h3 {
+        font-size: 1.2rem;
+        font-weight: 700;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card p,
+      .card label,
+      .card legend {
+        color: #7a869c;
+        border-width: 0;
+        display: block;
+        font-size: 1.1rem;
+        font-weight: 300;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card legend {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+      }
+
+      .card legend .score {
+        font-size: smaller;
+        margin-left: 20px;
+        font-weight: 100;
+        min-width: 5%;
+      }
+
+      .dark .card p,
+      .dark .card label,
+      .dark .card legend {
+        color: #e2e2e2;
+      }
+
+      .card label {
+        display: inline-block;
+      }
+
+      .card input[type="radio"] + label::before {
+        content: "";
+        height: 0.9rem;
+        width: 0.9rem;
+        margin-right: 0.7rem;
+        border-radius: 50%;
+        border: 1px solid rgb(74, 74, 74);
+        background-color: #ffffff;
+        display: inline-block;
+        cursor: pointer;
+        position: relative;
+        top: 0.09rem;
+      }
+
+      .dark .card input[type="radio"] + label::before {
+        border: 1px solid #e2e2e2;
+        background-color: inherit;
+      }
+
+      .card input[type="radio"]:checked + label::before {
+        background-color: rgb(47, 106, 189);
+        border-color: rgb(47, 106, 189);
+      }
+
+      .dark .card input[type="radio"]:checked + label::before {
+        background-color: #d9d9d9;
+        border-color: #d9d9d9;
+      }
+
+      .card input[type="radio"]:checked + label {
+        font-weight: bold;
+        color: #4a4a4a;
+      }
+
+      .dark input[type="radio"]:checked + label {
+        color: #e2e2e2;
+      }
+
+      .card input[type="radio"]:focus + label::before {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      .card legend {
+        margin-bottom: 0.4rem;
+      }
+
+      .totals {
+        font-size: 1.6rem;
+      }
+
+      .totals + h2 {
+        font-size: 1.9rem;
+        margin-top: 0;
+      }
+
+      .grade {
+        font-size: 1.9rem;
+      }
+
+      button {
+        background-color: #fff;
+        border: 1px solid #4a4a4a;
+        border-radius: 6px;
+        box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+        color: #4a4a4a;
+        cursor: pointer;
+        display: inline-block;
+        font-size: 1rem;
+        font-weight: 500;
+        letter-spacing: 0.04em;
+        line-height: 1.8em;
+        margin: 0.75rem 1.1rem 1.1rem 0;
+        outline: none;
+        padding: 0.5rem 1.4rem;
+        text-transform: uppercase;
+        transition-delay: 0s;
+        transition-duration: 0.25s;
+        transition-property: all;
+        transition-timing-function: ease-out;
+        width: fit-content;
+      }
+
+      .dark button {
+        background: #d9d9d9;
+        color: #121212;
+        box-shadow: none;
+      }
+
+      button:hover {
+        border-color: #dbdbdb;
+        color: #dbdbdb;
+      }
+
+      .dark button:hover {
+        color: #121212;
+      }
+
+      button.copy {
+        margin-left: 0.4rem;
+        font-size: .8rem;
+      }
+
+      button:focus {
+        border-color: rgb(56, 56, 56);
+        box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+        color: rgb(56, 56, 56);
+      }
+
+      div[role="alert"] {
+        color: #4a4a4a;
+        font-size: 1.2rem;
+        margin-bottom: 1.2rem;
+      }
+
+      .dark div[role="alert"] {
+        color: #e2e2e2;
+      }
+
+      .summary {
+        font-size: 1.2rem;
+        border: none;
+        font-family: monospace;
+        background-color: #d5d9de;
+        padding: 0.9rem;
+        border-radius: 6px;
+        line-height: 1.5;
+        width: 100%;
+      }
+
+      .summary h2,
+      .dark .summary h2 {
+        font-size: 1.2rem;
+        font-family: "Inter", sans-serif;
+        margin-top: 35px;
+        margin-bottom: 5px;
+      }
+
+      .summary h2:first-of-type,
+      .dark .summary h2:first-of-type {
+        margin-top: 15px;
+      }
+
+      .dark .summary {
+        background: #6b6a6a;
+        color: #e2e2e2;
+      }
+
+      .summary pre,
+      .dark .summary pre {
+        white-space: pre-wrap;
+        border-radius: 5px;
+        padding: 15px;
+        border: solid 2px rgba(0,0,0,.4);
+      }
+
+      .summary pre:empty {
+        display: none;
+      }
+
+      .summary pre:empty + button {
+        display: none;
+      }
+
+      .d-none {
+        display: none;
+      }
+
+      @media (max-width: 450px) {
+        .btn-container {
+          text-align: center;
+        }
+
+        button {
+          width: 100%;
+          margin: 0.5rem;
+        }
+
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.3rem);
+          background-size: 50%;
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+          background-size: 50%;
+        }
+      }
+
+      @media (max-width: 515px) {
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.7rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.7rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 460px) {
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.4rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.4rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 400px) {
+        #app h1 {
+          font-size: 1.6rem;
+        }
+
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.3rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 340px) {
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(0.9rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(0.9rem) scale(1.075);
+        }
+      }
+    </style>
+    <script>
+      $(function () {
+        let $select2 = $('.select2').select2({
+          containerCssClass: "wrap"
+        })
+
+        function triggerEvent(name, val) {
+          let selected = $("select[name='" + name + "']")
+          let change = new Event('change', { bubbles: true })
+          selected[0].dispatchEvent(change)
+        }
+
+        $('.select2').on('select2:select', function (e) {
+          triggerEvent(e.target.name, e.target.value)
+        });
+      })
+  </script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script>
+      (function(window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Module 1 HTML & CSS Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: "Deployment",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Student submitted deployed live application URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title:
+                    "Application loads at deployed URL (no 404, or Heroku/GH Pages error)",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title: "Student submitted application GitHub URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Missing GitHub submission URL",
+                  status: false
+                },
+                {
+                  title: "GitHub Repo contains application code",
+                  score: 8,
+                  type: "true false",
+                  message: "GitHub repo is empty",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Technical Acceptance Criteria",
+              subtitle:
+                "Keep your browser console open and watch out for errors from this point forward.",
+              score: 0,
+              questions: [
+                {
+                  title:
+                    "Application utilizes semantic HTML (articles, sections, headers, etc)",
+                  score: 5,
+                  type: "true false",
+                  message: "Application doesn't utilize semantic HTML",
+                  status: false
+                },
+                {
+                  title: "Application's HTML elements follow logical structure independent of styling and positioning",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application HTML elements do not follow logical structure independent of styling and positioning",
+                  status: false
+                },
+                {
+                  title: "Application's icon and image elements all have accessible `alt` attributes",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application's icon and image image elements do not have accessible `alt` attributes",
+                      score: 0
+                    },
+                    {
+                      message: "Very few of the application's icon and image elements have accessible `alt` attributes",
+                      score: 2
+                    },
+                    {
+                      message: "Most of the application's icon and image elements have accessible `alt` attributes",
+                      score: 4
+                    },
+                    {
+                      message: "Application's icon and image elements all have accessible `alt` attributes",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title:
+                    "Application's heading attributes appear in sequential order",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application's heading attributes do not appear in sequential order",
+                  status: false
+                },
+                {
+                  title:
+                    "Application's title element is concise and descriptive",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application's title element is not concise and descriptive",
+                  status: false
+                },
+                {
+                  title: "Application's links all function correctly",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application's links do not function correctly",
+                      score: 0
+                    },
+                    {
+                      message: "Very few of the application's links function correctly",
+                      score: 2
+                    },
+                    {
+                      message: "Most of the application's links function correctly",
+                      score: 4
+                    },
+                    {
+                      message: "Application's links all function correctly",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Application's CSS selectors and properties are consolidated and organized to follow semantic structure",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application's CSS selectors and properties are not consolidated and organized to follow semantic structure",
+                      score: 0
+                    },
+                    {
+                      message: "Application's CSS selectors and properties are either not consolidated or organized to follow semantic structure",
+                      score: 2
+                    },
+                    {
+                      message: "Application's CSS selectors and properties could be better consolidated or better organized to follow semantic structure",
+                      score: 4
+                    },
+                    {
+                      message: "Application's CSS selectors and properties are consolidated and organized to follow semantic structure",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Application's CSS file is properly commented",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application's CSS file contains no comments",
+                      score: 0
+                    },
+                    {
+                      message: "Application's CSS file contains very few comments",
+                      score: 2
+                    },
+                    {
+                      message: "Application's CSS file contains some comments, but should have more",
+                      score: 4
+                    },
+                    {
+                      message: "Application's CSS file is properly commented",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Repository Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Repository has a unique name",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't have a unique name",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for file structure and naming conventions",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository contains multiple descriptive commit messages",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false
+                },
+                {
+                  title: "Repository contains quality README with description, screenshot, link to deployed application",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                      score: 0
+                    },
+                    {
+                      message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                      score: 2
+                    },
+                    {
+                      message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                      score: 4
+                    },
+                    {
+                      message: "Repository contains quality README with description, screenshot, link to deployed application",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Application Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Application resembles (at least 90%) screenshots provided in Challenge instructions",
+                  score: 15,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application does not resemble screenshots provided with Challenge instructions",
+                      score: 0
+                    },
+                    {
+                      message: "Application could resemble screenshots provided with Challenge instructions much more closely",
+                      score: 5
+                    },
+                    {
+                      message: "Application could resemble screenshots provided with Challenge instructions a bit more closely",
+                      score: 10
+                    },
+                    {
+                      message: "Application resembles (at least 90%) screenshots provided in Challenge instructions",
+                      score: 15
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Academic Integrity",
+              subtitle:
+                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+              score: 0,
+              questions: [
+                {
+                  title: "Application contains a low number of commits",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application codebase appears advanced for the student, at this point in the course, or in general",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references a different author and/or repository",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references much older versions of npm packages",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                }
+              ]
+            }
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function() {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+          .forEach(category => {
+            category.score = 0;
+            category.questions.forEach(question => {
+              question.category = category.title;
+              question.status = false;
+              question.id = this.questions.length;
+              question.flagged = category.title === "Academic Integrity";
+              question.bonus = category.title === "Bonus";
+              if (question.type === "likert") {
+                question.message = question.options[0].message.split(" - ").pop();
+              };
+
+              let messageObj = {
+                message : question.message,
+                category : question.category
+              };
+
+              question.message && !question.bonus && this.messages.add(messageObj);
+              this.questions.push(question);
+              category.score += question.score;
+            });
+          });
+        };
+
+        Model.prototype.toggleDarkMode = function() {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function(radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+            
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message : question.message,
+                category : category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+          
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+           
+           question.message = newMessage;
+
+           let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+          
+          
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function() {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function() {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function() {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+          
+          this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .map(category => {
+            let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+            let summaryName = summary + "-summary";
+            return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+          
+          this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .map(category => {
+            let button = category.title.replace(/\s+/g, '-').toLowerCase();
+            let buttonName = "copyButton-" + button;
+            return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+          `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+          `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+        }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function({ title, subtitle, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function() {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .map(category => {
+            let score = category.score;
+            let title = category.title;
+            let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+            return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+            .filter(messageObj => messageObj.category === category.title)
+            .map(messageObj => `* ${messageObj.message}\n`)
+            .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+            })
+          .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function(title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+                isDarkMode ? "checked" : ""
+              }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function() {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function() {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .forEach(category => {
+            let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+            this[catClass + "-summary"].innerHTML = [...this.model.messages]
+            .filter(messageObj => messageObj.category === category.title)
+            .map(messageObj => `* ${messageObj.message}\n`)
+            .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function() {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+            
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+            
+            this.model.questions
+            .filter(question => question.category !== "Academic Integrity")
+            .forEach(question => {
+              const id = question.id;
+              const questionScore = document.querySelector("span[data-id='" + id + "']");
+              questionScore.textContent = 0;
+            });
+            
+            this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              const title = category.title
+              const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+              const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+              categoryScore.textContent = 0;
+              resultsCategoryScore.textContent = 0;
+            });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+              
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+    </script>
+  </body>
+  </html>
+  

+ 1306 - 0
rubrics/mod02.html

@@ -0,0 +1,1306 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+  <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+  <title>Module 2 Advanced CSS Challenge Rubric</title>
+  <style>
+    *,
+    *::before,
+    *::after {
+      box-sizing: border-box;
+    }
+
+    html,
+    body {
+      margin: 0;
+      padding: 0;
+      line-height: 1.15;
+      font-size: 100%;
+      color: #fefefe;
+      transition: all 0.4s;
+    }
+
+    body {
+      background-color: #fbfcfd;
+      font-family: "Inter", sans-serif;
+      color: #4a4a4a;
+    }
+
+    #app.dark {
+      background-color: #121212;
+      color: #e2e2e2;
+    }
+
+    h1 {
+      font-size: 1.9rem;
+    }
+
+    h2 {
+      font-size: 1.6rem;
+    }
+
+    h3 {
+      font-size: 1.2rem;
+    }
+
+    h4 {
+      font-size: 0.9rem;
+    }
+
+    p,
+    legend {
+      font-size: 1.2rem;
+    }
+
+    label {
+      font-size: 0.9rem;
+      line-height: 2;
+    }
+
+    fieldset {
+      border: none;
+      padding: 0;
+      margin-top: 0.9rem;
+    }
+
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute !important;
+      height: 1px;
+      width: 1px;
+      overflow: hidden;
+      clip: rect(1px 1px 1px 1px);
+      clip: rect(1px, 1px, 1px, 1px);
+      white-space: nowrap;
+    }
+
+    .select-lg,
+    .select-lg option {
+      font-family: "Inter", sans-serif;
+      padding: 10px;
+    }
+
+    .narrow {
+      width: 800px;
+    }
+
+    #app {
+      padding: 3%;
+      transition: all 0.4s;
+    }
+
+    #app h1 {
+      font-size: 1.9rem;
+      font-weight: 700;
+      line-height: 1.3;
+    }
+
+    header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .switch-group {
+      display: flex;
+      align-items: center;
+      height: 2.25rem;
+      width: 4.5rem;
+    }
+
+    input[type="checkbox"]+.toggle {
+      height: 100%;
+      width: 100%;
+      background-color: #77a1db;
+      border-radius: 1.9rem;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      padding: 0.35rem;
+      cursor: pointer;
+      overflow: hidden;
+      background-position: 25% 125%;
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]:focus+.toggle {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    input[type="checkbox"]:checked+.toggle {
+      background-color: rgb(85, 89, 204);
+      background-size: 1rem;
+      background-repeat: no-repeat;
+      background-position: 25%;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]+.toggle::after {
+      content: "";
+      display: block;
+      height: 1.7rem;
+      width: 1.7rem;
+      border-radius: 50%;
+      cursor: pointer;
+      border: 4px solid #e1be64;
+      background-color: #fddf74;
+      transform: translateX(0rem);
+      transition: all 0.5s;
+      background-position: center bottom;
+    }
+
+    input[type="checkbox"]:hover+.toggle::after {
+      transform: translateX(0rem) scale(1.1);
+    }
+
+    input[type="checkbox"]:checked+.toggle::after {
+      border: 4px solid #ffffff;
+      background-color: rgba(255, 255, 255, 0.4);
+      transform: translateX(2.1rem);
+      transition: all 0.5s;
+      background-position: center;
+      background-size: 60%;
+      background-repeat: no-repeat;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+    }
+
+    input[type="checkbox"]:checked:hover+.toggle::after {
+      transform: translateX(2.1rem) scale(1.075);
+    }
+
+    .card {
+      background-color: #fff;
+      border-radius: 6px;
+      border-width: 0;
+      box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+      font-size: 1.1rem;
+      height: 100%;
+      line-height: 1.8rem;
+      padding: 1.5rem 2rem;
+      width: 100%;
+      margin-bottom: 1.9rem;
+    }
+
+    .dark .card {
+      background-color: #333333;
+      box-shadow: none;
+    }
+
+    .card h1,
+    .card h2,
+    .card h3,
+    .card h4,
+    .card h5,
+    .card h6 {
+      color: #4a4a4a;
+    }
+
+    .dark .card h1,
+    .dark .card h2,
+    .dark .card h3,
+    .dark .card h4,
+    .dark .card h5,
+    .dark .card h6 {
+      color: #e2e2e2;
+    }
+
+    .card-header h2 {
+      display: inline-block;
+    }
+
+    .card .cat-score {
+      margin-left: 20px;
+      font-size: smaller;
+    }
+
+    .card h3 {
+      font-size: 1.2rem;
+      font-weight: 700;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card p,
+    .card label,
+    .card legend {
+      color: #7a869c;
+      border-width: 0;
+      display: block;
+      font-size: 1.1rem;
+      font-weight: 300;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card legend {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+
+    .card legend .score {
+      font-size: smaller;
+      margin-left: 20px;
+      font-weight: 100;
+      min-width: 5%;
+    }
+
+    .dark .card p,
+    .dark .card label,
+    .dark .card legend {
+      color: #e2e2e2;
+    }
+
+    .card label {
+      display: inline-block;
+    }
+
+    .card input[type="radio"]+label::before {
+      content: "";
+      height: 0.9rem;
+      width: 0.9rem;
+      margin-right: 0.7rem;
+      border-radius: 50%;
+      border: 1px solid rgb(74, 74, 74);
+      background-color: #ffffff;
+      display: inline-block;
+      cursor: pointer;
+      position: relative;
+      top: 0.09rem;
+    }
+
+    .dark .card input[type="radio"]+label::before {
+      border: 1px solid #e2e2e2;
+      background-color: inherit;
+    }
+
+    .card input[type="radio"]:checked+label::before {
+      background-color: rgb(47, 106, 189);
+      border-color: rgb(47, 106, 189);
+    }
+
+    .dark .card input[type="radio"]:checked+label::before {
+      background-color: #d9d9d9;
+      border-color: #d9d9d9;
+    }
+
+    .card input[type="radio"]:checked+label {
+      font-weight: bold;
+      color: #4a4a4a;
+    }
+
+    .dark input[type="radio"]:checked+label {
+      color: #e2e2e2;
+    }
+
+    .card input[type="radio"]:focus+label::before {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    .card legend {
+      margin-bottom: 0.4rem;
+    }
+
+    .totals {
+      font-size: 1.6rem;
+    }
+
+    .totals+h2 {
+      font-size: 1.9rem;
+      margin-top: 0;
+    }
+
+    .grade {
+      font-size: 1.9rem;
+    }
+
+    button {
+      background-color: #fff;
+      border: 1px solid #4a4a4a;
+      border-radius: 6px;
+      box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+      color: #4a4a4a;
+      cursor: pointer;
+      display: inline-block;
+      font-size: 1rem;
+      font-weight: 500;
+      letter-spacing: 0.04em;
+      line-height: 1.8em;
+      margin: 0.75rem 1.1rem 1.1rem 0;
+      outline: none;
+      padding: 0.5rem 1.4rem;
+      text-transform: uppercase;
+      transition-delay: 0s;
+      transition-duration: 0.25s;
+      transition-property: all;
+      transition-timing-function: ease-out;
+      width: fit-content;
+    }
+
+    .dark button {
+      background: #d9d9d9;
+      color: #121212;
+      box-shadow: none;
+    }
+
+    button:hover {
+      border-color: #dbdbdb;
+      color: #dbdbdb;
+    }
+
+    .dark button:hover {
+      color: #121212;
+    }
+
+    button.copy {
+      margin-left: 0.4rem;
+      font-size: .8rem;
+    }
+
+    button:focus {
+      border-color: rgb(56, 56, 56);
+      box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+      color: rgb(56, 56, 56);
+    }
+
+    div[role="alert"] {
+      color: #4a4a4a;
+      font-size: 1.2rem;
+      margin-bottom: 1.2rem;
+    }
+
+    .dark div[role="alert"] {
+      color: #e2e2e2;
+    }
+
+    .summary {
+      font-size: 1.2rem;
+      border: none;
+      font-family: monospace;
+      background-color: #d5d9de;
+      padding: 0.9rem;
+      border-radius: 6px;
+      line-height: 1.5;
+      width: 100%;
+    }
+
+    .summary h2,
+    .dark .summary h2 {
+      font-size: 1.2rem;
+      font-family: "Inter", sans-serif;
+      margin-top: 35px;
+      margin-bottom: 5px;
+    }
+
+    .summary h2:first-of-type,
+    .dark .summary h2:first-of-type {
+      margin-top: 15px;
+    }
+
+    .dark .summary {
+      background: #6b6a6a;
+      color: #e2e2e2;
+    }
+
+    .summary pre,
+    .dark .summary pre {
+      white-space: pre-wrap;
+      border-radius: 5px;
+      padding: 15px;
+      border: solid 2px rgba(0, 0, 0, .4);
+    }
+
+    .summary pre:empty {
+      display: none;
+    }
+
+    .summary pre:empty+button {
+      display: none;
+    }
+
+    .d-none {
+      display: none;
+    }
+
+    @media (max-width: 450px) {
+      .btn-container {
+        text-align: center;
+      }
+
+      button {
+        width: 100%;
+        margin: 0.5rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+        background-size: 50%;
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+        background-size: 50%;
+      }
+    }
+
+    @media (max-width: 515px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.7rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.7rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 460px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.4rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.4rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 400px) {
+      #app h1 {
+        font-size: 1.6rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 340px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(0.9rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(0.9rem) scale(1.075);
+      }
+    }
+  </style>
+  <script>
+    $(function () {
+      let $select2 = $('.select2').select2({
+        containerCssClass: "wrap"
+      })
+
+      function triggerEvent(name, val) {
+        let selected = $("select[name='" + name + "']")
+        let change = new Event('change', { bubbles: true })
+        selected[0].dispatchEvent(change)
+      }
+
+      $('.select2').on('select2:select', function (e) {
+        triggerEvent(e.target.name, e.target.value)
+      });
+    })
+  </script>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script>
+      (function (window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Module 2 Advanced CSS Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: "Deployment",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Student submitted deployed live application URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title:
+                    "Application loads at deployed URL (no 404, or Heroku/GH Pages error)",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title: "Student submitted application GitHub URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Missing GitHub submission URL",
+                  status: false
+                },
+                {
+                  title: "GitHub Repo contains application code",
+                  score: 8,
+                  type: "true false",
+                  message: "GitHub repo is empty",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Technical Acceptance Criteria",
+              subtitle:
+                "Keep your browser console open and watch out for errors from this point forward.",
+              score: 0,
+              questions: [
+                {
+                  title:
+                    "Application includes the student's name at the top of the portfolio",
+                  score: 5,
+                  type: "true false",
+                  message: "Application doesn't include the student's name at the top of the portfolio",
+                  status: false
+                },
+                {
+                  title:
+                    "Application includes a photo or avatar of the student",
+                  score: 5,
+                  type: "true false",
+                  message: "Application doesn't include a photo or avatar of the student",
+                  status: false
+                },
+                {
+                  title: "The application's navigation includes links to sections about them, their work, and how to contact them",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "The application's navigation doesn't include links to sections about them, their work, and how to contact them",
+                      score: 0
+                    },
+                    {
+                      message: "The application's navigation is missing links to 2 of the following sections: about, work, or contact",
+                      score: 1
+                    },
+                    {
+                      message: "The application's navigation is missing a link to 1 of the following sections: about, work, or contact",
+                      score: 3
+                    },
+                    {
+                      message: "The application's navigation includes links to sections about them, their work, and how to contact them",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "The application's navigation links to each corresponding section on the page",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "The application's navigation doesn't link to each corresponding section on the page",
+                      score: 0
+                    },
+                    {
+                      message: "The application's navigation links to 1 of the 3 sections on the page",
+                      score: 1
+                    },
+                    {
+                      message: "The application's navigation links to 2 of the 3 sections on the page",
+                      score: 3
+                    },
+                    {
+                      message: "The application's navigation links to each corresponding section on the page",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "The section about the student's work contains titled images of the student's applications (or placeholders)",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "The section about the student's work doesn't contain titled images of the student's applications (or placeholders)",
+                      score: 0
+                    },
+                    {
+                      message: "The section about the student's work contains titled images for some of the student's applications (or placeholders)",
+                      score: 3
+                    },
+                    {
+                      message: "The section about the student's work contains titled images for all of the student's applications (or placeholders)",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title:
+                    "In the application's section about their work, the first image is larger in size than the others",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "In the application's section about their work, the first image isn't larger in size than the others",
+                  status: false
+                },
+                {
+                  title:
+                    "Clicking on a featured application opens that deployed application (at least one application, in case they are using placeholders)",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Clicking on a featured application doesn't open that deployed application (at least one application, in case they are using placeholders)",
+                  status: false
+                },
+                {
+                  title:
+                    "The application adapts to the viewport with a responsive layout by resizing the page on various screens and devices",
+                  score: 5,
+                  type: "true false",
+                  message: "The application doesn't adapt to the viewport with a responsive layout by resizing the page on various screens and devices",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Repository Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Repository has a unique name",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't have a unique name",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for file structure and naming conventions",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository contains multiple descriptive commit messages",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false
+                },
+                {
+                  title: "Repository contains quality README with description, screenshot, link to deployed application",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                      score: 0
+                    },
+                    {
+                      message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                      score: 2
+                    },
+                    {
+                      message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                      score: 4
+                    },
+                    {
+                      message: "Repository contains quality README with description, screenshot, link to deployed application",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Application Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Application resembles the mock-up functionality provided in Challenge instructions",
+                  score: 15,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application does not resemble the mock-up functionality provided in Challenge instructions",
+                      score: 0
+                    },
+                    {
+                      message: "Application could resemble the mock-up functionality provided in Challenge instructions a lot more closely",
+                      score: 5
+                    },
+                    {
+                      message: "Application could resemble the mock-up functionality provided in Challenge instructions a bit more closely",
+                      score: 10
+                    },
+                    {
+                      message: "Application resembles the mock-up functionality provided in Challenge instructions",
+                      score: 15
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Academic Integrity",
+              subtitle:
+                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+              score: 0,
+              questions: [
+                {
+                  title: "Application contains a low number of commits",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application codebase appears advanced for the student, at this point in the course, or in general",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references a different author and/or repository",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references much older versions of npm packages",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                }
+              ]
+            }
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+            .forEach(category => {
+              category.score = 0;
+              category.questions.forEach(question => {
+                question.category = category.title;
+                question.status = false;
+                question.id = this.questions.length;
+                question.flagged = category.title === "Academic Integrity";
+                question.bonus = category.title === "Bonus";
+                if (question.type === "likert") {
+                  question.message = question.options[0].message.split(" - ").pop();
+                };
+
+                let messageObj = {
+                  message: question.message,
+                  category: question.category
+                };
+
+                question.message && !question.bonus && this.messages.add(messageObj);
+                this.questions.push(question);
+                category.score += question.score;
+              });
+            });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+            if (question.type === "true false") {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector("select[name='" + question.id + "']").value;
+              categoryTotal += parseInt(score);
+            }
+          })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + "-summary";
+              return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + button;
+              return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+              `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+              `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+            }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .map(category => {
+                let score = category.score;
+                let title = category.title;
+                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                  .filter(messageObj => messageObj.category === category.title)
+                  .map(messageObj => `* ${messageObj.message}\n`)
+                  .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+              })
+              .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+            isDarkMode ? "checked" : ""
+            }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                .filter(messageObj => messageObj.category === category.title)
+                .map(messageObj => `* ${messageObj.message}\n`)
+                .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+
+            this.model.questions
+              .filter(question => question.category !== "Academic Integrity")
+              .forEach(question => {
+                const id = question.id;
+                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .forEach(category => {
+                const title = category.title
+                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+  </script>
+</body>
+
+</html>

+ 1327 - 0
rubrics/mod03.html

@@ -0,0 +1,1327 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+  <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+  <title>Module 3 JavaScript Challenge Rubric</title>
+  <style>
+    *,
+    *::before,
+    *::after {
+      box-sizing: border-box;
+    }
+
+    html,
+    body {
+      margin: 0;
+      padding: 0;
+      line-height: 1.15;
+      font-size: 100%;
+      color: #fefefe;
+      transition: all 0.4s;
+    }
+
+    body {
+      background-color: #fbfcfd;
+      font-family: "Inter", sans-serif;
+      color: #4a4a4a;
+    }
+
+    #app.dark {
+      background-color: #121212;
+      color: #e2e2e2;
+    }
+
+    h1 {
+      font-size: 1.9rem;
+    }
+
+    h2 {
+      font-size: 1.6rem;
+    }
+
+    h3 {
+      font-size: 1.2rem;
+    }
+
+    h4 {
+      font-size: 0.9rem;
+    }
+
+    p,
+    legend {
+      font-size: 1.2rem;
+    }
+
+    label {
+      font-size: 0.9rem;
+      line-height: 2;
+    }
+
+    fieldset {
+      border: none;
+      padding: 0;
+      margin-top: 0.9rem;
+    }
+
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute !important;
+      height: 1px;
+      width: 1px;
+      overflow: hidden;
+      clip: rect(1px 1px 1px 1px);
+      clip: rect(1px, 1px, 1px, 1px);
+      white-space: nowrap;
+    }
+
+    .select-lg,
+    .select-lg option {
+      font-family: "Inter", sans-serif;
+      padding: 10px;
+    }
+
+    .narrow {
+      width: 800px;
+    }
+
+    #app {
+      padding: 3%;
+      transition: all 0.4s;
+    }
+
+    #app h1 {
+      font-size: 1.9rem;
+      font-weight: 700;
+      line-height: 1.3;
+    }
+
+    header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .switch-group {
+      display: flex;
+      align-items: center;
+      height: 2.25rem;
+      width: 4.5rem;
+    }
+
+    input[type="checkbox"]+.toggle {
+      height: 100%;
+      width: 100%;
+      background-color: #77a1db;
+      border-radius: 1.9rem;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      padding: 0.35rem;
+      cursor: pointer;
+      overflow: hidden;
+      background-position: 25% 125%;
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]:focus+.toggle {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    input[type="checkbox"]:checked+.toggle {
+      background-color: rgb(85, 89, 204);
+      background-size: 1rem;
+      background-repeat: no-repeat;
+      background-position: 25%;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]+.toggle::after {
+      content: "";
+      display: block;
+      height: 1.7rem;
+      width: 1.7rem;
+      border-radius: 50%;
+      cursor: pointer;
+      border: 4px solid #e1be64;
+      background-color: #fddf74;
+      transform: translateX(0rem);
+      transition: all 0.5s;
+      background-position: center bottom;
+    }
+
+    input[type="checkbox"]:hover+.toggle::after {
+      transform: translateX(0rem) scale(1.1);
+    }
+
+    input[type="checkbox"]:checked+.toggle::after {
+      border: 4px solid #ffffff;
+      background-color: rgba(255, 255, 255, 0.4);
+      transform: translateX(2.1rem);
+      transition: all 0.5s;
+      background-position: center;
+      background-size: 60%;
+      background-repeat: no-repeat;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+    }
+
+    input[type="checkbox"]:checked:hover+.toggle::after {
+      transform: translateX(2.1rem) scale(1.075);
+    }
+
+    .card {
+      background-color: #fff;
+      border-radius: 6px;
+      border-width: 0;
+      box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+      font-size: 1.1rem;
+      height: 100%;
+      line-height: 1.8rem;
+      padding: 1.5rem 2rem;
+      width: 100%;
+      margin-bottom: 1.9rem;
+    }
+
+    .dark .card {
+      background-color: #333333;
+      box-shadow: none;
+    }
+
+    .card h1,
+    .card h2,
+    .card h3,
+    .card h4,
+    .card h5,
+    .card h6 {
+      color: #4a4a4a;
+    }
+
+    .dark .card h1,
+    .dark .card h2,
+    .dark .card h3,
+    .dark .card h4,
+    .dark .card h5,
+    .dark .card h6 {
+      color: #e2e2e2;
+    }
+
+    .card-header h2 {
+      display: inline-block;
+    }
+
+    .card .cat-score {
+      margin-left: 20px;
+      font-size: smaller;
+    }
+
+    .card h3 {
+      font-size: 1.2rem;
+      font-weight: 700;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card p,
+    .card label,
+    .card legend {
+      color: #7a869c;
+      border-width: 0;
+      display: block;
+      font-size: 1.1rem;
+      font-weight: 300;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card legend {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+
+    .card legend .score {
+      font-size: smaller;
+      margin-left: 20px;
+      font-weight: 100;
+      min-width: 5%;
+    }
+
+    .dark .card p,
+    .dark .card label,
+    .dark .card legend {
+      color: #e2e2e2;
+    }
+
+    .card label {
+      display: inline-block;
+    }
+
+    .card input[type="radio"]+label::before {
+      content: "";
+      height: 0.9rem;
+      width: 0.9rem;
+      margin-right: 0.7rem;
+      border-radius: 50%;
+      border: 1px solid rgb(74, 74, 74);
+      background-color: #ffffff;
+      display: inline-block;
+      cursor: pointer;
+      position: relative;
+      top: 0.09rem;
+    }
+
+    .dark .card input[type="radio"]+label::before {
+      border: 1px solid #e2e2e2;
+      background-color: inherit;
+    }
+
+    .card input[type="radio"]:checked+label::before {
+      background-color: rgb(47, 106, 189);
+      border-color: rgb(47, 106, 189);
+    }
+
+    .dark .card input[type="radio"]:checked+label::before {
+      background-color: #d9d9d9;
+      border-color: #d9d9d9;
+    }
+
+    .card input[type="radio"]:checked+label {
+      font-weight: bold;
+      color: #4a4a4a;
+    }
+
+    .dark input[type="radio"]:checked+label {
+      color: #e2e2e2;
+    }
+
+    .card input[type="radio"]:focus+label::before {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    .card legend {
+      margin-bottom: 0.4rem;
+    }
+
+    .totals {
+      font-size: 1.6rem;
+    }
+
+    .totals+h2 {
+      font-size: 1.9rem;
+      margin-top: 0;
+    }
+
+    .grade {
+      font-size: 1.9rem;
+    }
+
+    button {
+      background-color: #fff;
+      border: 1px solid #4a4a4a;
+      border-radius: 6px;
+      box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+      color: #4a4a4a;
+      cursor: pointer;
+      display: inline-block;
+      font-size: 1rem;
+      font-weight: 500;
+      letter-spacing: 0.04em;
+      line-height: 1.8em;
+      margin: 0.75rem 1.1rem 1.1rem 0;
+      outline: none;
+      padding: 0.5rem 1.4rem;
+      text-transform: uppercase;
+      transition-delay: 0s;
+      transition-duration: 0.25s;
+      transition-property: all;
+      transition-timing-function: ease-out;
+      width: fit-content;
+    }
+
+    .dark button {
+      background: #d9d9d9;
+      color: #121212;
+      box-shadow: none;
+    }
+
+    button:hover {
+      border-color: #dbdbdb;
+      color: #dbdbdb;
+    }
+
+    .dark button:hover {
+      color: #121212;
+    }
+
+    button.copy {
+      margin-left: 0.4rem;
+      font-size: .8rem;
+    }
+
+    button:focus {
+      border-color: rgb(56, 56, 56);
+      box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+      color: rgb(56, 56, 56);
+    }
+
+    div[role="alert"] {
+      color: #4a4a4a;
+      font-size: 1.2rem;
+      margin-bottom: 1.2rem;
+    }
+
+    .dark div[role="alert"] {
+      color: #e2e2e2;
+    }
+
+    .summary {
+      font-size: 1.2rem;
+      border: none;
+      font-family: monospace;
+      background-color: #d5d9de;
+      padding: 0.9rem;
+      border-radius: 6px;
+      line-height: 1.5;
+      width: 100%;
+    }
+
+    .summary h2,
+    .dark .summary h2 {
+      font-size: 1.2rem;
+      font-family: "Inter", sans-serif;
+      margin-top: 35px;
+      margin-bottom: 5px;
+    }
+
+    .summary h2:first-of-type,
+    .dark .summary h2:first-of-type {
+      margin-top: 15px;
+    }
+
+    .dark .summary {
+      background: #6b6a6a;
+      color: #e2e2e2;
+    }
+
+    .summary pre,
+    .dark .summary pre {
+      white-space: pre-wrap;
+      border-radius: 5px;
+      padding: 15px;
+      border: solid 2px rgba(0, 0, 0, .4);
+    }
+
+    .summary pre:empty {
+      display: none;
+    }
+
+    .summary pre:empty+button {
+      display: none;
+    }
+
+    .d-none {
+      display: none;
+    }
+
+    @media (max-width: 450px) {
+      .btn-container {
+        text-align: center;
+      }
+
+      button {
+        width: 100%;
+        margin: 0.5rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+        background-size: 50%;
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+        background-size: 50%;
+      }
+    }
+
+    @media (max-width: 515px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.7rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.7rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 460px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.4rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.4rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 400px) {
+      #app h1 {
+        font-size: 1.6rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 340px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(0.9rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(0.9rem) scale(1.075);
+      }
+    }
+  </style>
+  <script>
+    $(function () {
+      let $select2 = $('.select2').select2({
+        containerCssClass: "wrap"
+      })
+
+      function triggerEvent(name, val) {
+        let selected = $("select[name='" + name + "']")
+        let change = new Event('change', { bubbles: true })
+        selected[0].dispatchEvent(change)
+      }
+
+      $('.select2').on('select2:select', function (e) {
+        triggerEvent(e.target.name, e.target.value)
+      });
+    })
+  </script>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script>
+      (function (window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Module 3 JavaScript Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: "Deployment",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Student submitted deployed live application URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title:
+                    "Application loads at deployed URL (no 404, or Heroku/GH Pages error)",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title: "Student submitted application GitHub URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Missing GitHub submission URL",
+                  status: false
+                },
+                {
+                  title: "GitHub Repo contains application code",
+                  score: 8,
+                  type: "true false",
+                  message: "GitHub repo is empty",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Technical Acceptance Criteria",
+              subtitle:
+                "Keep your browser console open and watch out for errors from this point forward.",
+              score: 0,
+              questions: [
+                {
+                  title: "Application prompts user for password length and if they would like to include lowercased, uppercased, numeric, and special characters",
+                  score: 8,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application doesn't prompt for any password options",
+                      score: 0
+                    },
+                    {
+                      message: "Application prompts user for 1 of the 5 password options: password length, lowercased characters, uppercased characters, numeric characters, and special characters",
+                      score: 2
+                    },
+                    {
+                      message: "Application prompts user for 2 of the 5 password options: password length, lowercased characters, uppercased characters, numeric characters, and special characters",
+                      score: 4
+                    },
+                    {
+                      message: "Application prompts user for 3 of the 5 password options: password length, lowercased characters, uppercased characters, numeric characters, and special characters",
+                      score: 6
+                    },
+                    {
+                      message: "Application prompts user for 4 of the 5 password options: password length, lowercased characters, uppercased characters, numeric characters, and special characters",
+                      score: 7
+                    },
+                    {
+                      message: "Application prompts user for password length and if they would like to include lowercased, uppercased, numeric, and special characters",
+                      score: 8
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title:
+                    "Application requires minimum password length of 8 characters",
+                  score: 8,
+                  type: "true false",
+                  message:
+                    "Application doesn't require minimum password length of 8 character",
+                  status: false
+                },
+                {
+                  title:
+                    "Application requires maximum password length of 128 characters",
+                  score: 8,
+                  type: "true false",
+                  message:
+                    "Application doesn't require maximum password length of 128 characters",
+                  status: false
+                },
+                {
+                  title: "Application generates passwords that consistently demonstrate a valid reflection of user input (but may not necessarily include all user selections for character type)",
+                  score: 8,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application doesn't generate a password at all",
+                      score: 0
+                    },
+                    {
+                      message: "Application generates passwords that only include character type(s) which user didn't select",
+                      score: 2
+                    },
+                    {
+                      message: "Application generates passwords that always include at least one selected character type, but also character type(s) not selected by user",
+                      score: 4
+                    },
+                    {
+                      message: "Application generates passwords that consistently demonstrate a valid reflection of user input (but may not necessarily include all user selections for character type)",
+                      score: 8
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Application generates valid password based on options outlined in all other criteria",
+                  score: 8,
+                  type: "true false",
+                  message:
+                    "Application doesn't generate valid password based on options outlined in all other criteria",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Repository Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Repository has a unique name",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't have a unique name",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for file structure and naming conventions",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository contains multiple descriptive commit messages",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false
+                },
+                {
+                  title: "Repository contains quality README with description, screenshot, link to deployed application",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                      score: 0
+                    },
+                    {
+                      message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                      score: 2
+                    },
+                    {
+                      message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                      score: 4
+                    },
+                    {
+                      message: "Repository contains quality README with description, screenshot, link to deployed application",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Application Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Application user experience is intuitive and easy to navigate",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application user experience is not intuitive and easy to navigate",
+                      score: 0
+                    },
+                    {
+                      message: "Application user experience could be much more intuitive and easy to navigate",
+                      score: 2
+                    },
+                    {
+                      message: "Application user experience could be a lot more intuitive and easy to navigate",
+                      score: 4
+                    },
+                    {
+                      message: "Application user experience is intuitive and easy to navigate",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Application user interface style is clean and polished",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application user interface is not clean and polished",
+                      score: 0
+                    },
+                    {
+                      message: "Application user interface could be much better",
+                      score: 2
+                    },
+                    {
+                      message: "Application user interface could be a bit better",
+                      score: 4
+                    },
+                    {
+                      message: "Application user interface style is clean and polished",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Application resembles the mock-up functionality provided in Challenge instructions",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application doesn't resemble the mock-up functionality provided in Challenge instructions",
+                      score: 0
+                    },
+                    {
+                      message: "Application could resemble mock-up functionality provided with Challenge instructions much more closely",
+                      score: 2
+                    },
+                    {
+                      message: "Application could resemble mock-up functionality provided with Challenge instructions a bit more closely",
+                      score: 4
+                    },
+                    {
+                      message: "Application resembles the mock-up functionality provided in Challenge instructions",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Academic Integrity",
+              subtitle:
+                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+              score: 0,
+              questions: [
+                {
+                  title: "Application contains a low number of commits",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application codebase appears advanced for the student, at this point in the course, or in general",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references a different author and/or repository",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references much older versions of npm packages",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                }
+              ]
+            }
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+            .forEach(category => {
+              category.score = 0;
+              category.questions.forEach(question => {
+                question.category = category.title;
+                question.status = false;
+                question.id = this.questions.length;
+                question.flagged = category.title === "Academic Integrity";
+                question.bonus = category.title === "Bonus";
+                if (question.type === "likert") {
+                  question.message = question.options[0].message.split(" - ").pop();
+                };
+
+                let messageObj = {
+                  message: question.message,
+                  category: question.category
+                };
+
+                question.message && !question.bonus && this.messages.add(messageObj);
+                this.questions.push(question);
+                category.score += question.score;
+              });
+            });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+            if (question.type === "true false") {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector("select[name='" + question.id + "']").value;
+              categoryTotal += parseInt(score);
+            }
+          })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + "-summary";
+              return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + button;
+              return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+              `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+              `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+            }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .map(category => {
+                let score = category.score;
+                let title = category.title;
+                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                  .filter(messageObj => messageObj.category === category.title)
+                  .map(messageObj => `* ${messageObj.message}\n`)
+                  .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+              })
+              .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+            isDarkMode ? "checked" : ""
+            }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                .filter(messageObj => messageObj.category === category.title)
+                .map(messageObj => `* ${messageObj.message}\n`)
+                .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+
+            this.model.questions
+              .filter(question => question.category !== "Academic Integrity")
+              .forEach(question => {
+                const id = question.id;
+                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .forEach(category => {
+                const title = category.title
+                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+  </script>
+</body>
+
+</html>

+ 1292 - 0
rubrics/mod04.html

@@ -0,0 +1,1292 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+  <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+  <title>Module 4 Web APIs Challenge Rubric</title>
+  <style>
+    *,
+    *::before,
+    *::after {
+      box-sizing: border-box;
+    }
+
+    html,
+    body {
+      margin: 0;
+      padding: 0;
+      line-height: 1.15;
+      font-size: 100%;
+      color: #fefefe;
+      transition: all 0.4s;
+    }
+
+    body {
+      background-color: #fbfcfd;
+      font-family: "Inter", sans-serif;
+      color: #4a4a4a;
+    }
+
+    #app.dark {
+      background-color: #121212;
+      color: #e2e2e2;
+    }
+
+    h1 {
+      font-size: 1.9rem;
+    }
+
+    h2 {
+      font-size: 1.6rem;
+    }
+
+    h3 {
+      font-size: 1.2rem;
+    }
+
+    h4 {
+      font-size: 0.9rem;
+    }
+
+    p,
+    legend {
+      font-size: 1.2rem;
+    }
+
+    label {
+      font-size: 0.9rem;
+      line-height: 2;
+    }
+
+    fieldset {
+      border: none;
+      padding: 0;
+      margin-top: 0.9rem;
+    }
+
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute !important;
+      height: 1px;
+      width: 1px;
+      overflow: hidden;
+      clip: rect(1px 1px 1px 1px);
+      clip: rect(1px, 1px, 1px, 1px);
+      white-space: nowrap;
+    }
+
+    .select-lg,
+    .select-lg option {
+      font-family: "Inter", sans-serif;
+      padding: 10px;
+    }
+
+    .narrow {
+      width: 800px;
+    }
+
+    #app {
+      padding: 3%;
+      transition: all 0.4s;
+    }
+
+    #app h1 {
+      font-size: 1.9rem;
+      font-weight: 700;
+      line-height: 1.3;
+    }
+
+    header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .switch-group {
+      display: flex;
+      align-items: center;
+      height: 2.25rem;
+      width: 4.5rem;
+    }
+
+    input[type="checkbox"]+.toggle {
+      height: 100%;
+      width: 100%;
+      background-color: #77a1db;
+      border-radius: 1.9rem;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      padding: 0.35rem;
+      cursor: pointer;
+      overflow: hidden;
+      background-position: 25% 125%;
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]:focus+.toggle {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    input[type="checkbox"]:checked+.toggle {
+      background-color: rgb(85, 89, 204);
+      background-size: 1rem;
+      background-repeat: no-repeat;
+      background-position: 25%;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]+.toggle::after {
+      content: "";
+      display: block;
+      height: 1.7rem;
+      width: 1.7rem;
+      border-radius: 50%;
+      cursor: pointer;
+      border: 4px solid #e1be64;
+      background-color: #fddf74;
+      transform: translateX(0rem);
+      transition: all 0.5s;
+      background-position: center bottom;
+    }
+
+    input[type="checkbox"]:hover+.toggle::after {
+      transform: translateX(0rem) scale(1.1);
+    }
+
+    input[type="checkbox"]:checked+.toggle::after {
+      border: 4px solid #ffffff;
+      background-color: rgba(255, 255, 255, 0.4);
+      transform: translateX(2.1rem);
+      transition: all 0.5s;
+      background-position: center;
+      background-size: 60%;
+      background-repeat: no-repeat;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+    }
+
+    input[type="checkbox"]:checked:hover+.toggle::after {
+      transform: translateX(2.1rem) scale(1.075);
+    }
+
+    .card {
+      background-color: #fff;
+      border-radius: 6px;
+      border-width: 0;
+      box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+      font-size: 1.1rem;
+      height: 100%;
+      line-height: 1.8rem;
+      padding: 1.5rem 2rem;
+      width: 100%;
+      margin-bottom: 1.9rem;
+    }
+
+    .dark .card {
+      background-color: #333333;
+      box-shadow: none;
+    }
+
+    .card h1,
+    .card h2,
+    .card h3,
+    .card h4,
+    .card h5,
+    .card h6 {
+      color: #4a4a4a;
+    }
+
+    .dark .card h1,
+    .dark .card h2,
+    .dark .card h3,
+    .dark .card h4,
+    .dark .card h5,
+    .dark .card h6 {
+      color: #e2e2e2;
+    }
+
+    .card-header h2 {
+      display: inline-block;
+    }
+
+    .card .cat-score {
+      margin-left: 20px;
+      font-size: smaller;
+    }
+
+    .card h3 {
+      font-size: 1.2rem;
+      font-weight: 700;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card p,
+    .card label,
+    .card legend {
+      color: #7a869c;
+      border-width: 0;
+      display: block;
+      font-size: 1.1rem;
+      font-weight: 300;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card legend {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+
+    .card legend .score {
+      font-size: smaller;
+      margin-left: 20px;
+      font-weight: 100;
+      min-width: 5%;
+    }
+
+    .dark .card p,
+    .dark .card label,
+    .dark .card legend {
+      color: #e2e2e2;
+    }
+
+    .card label {
+      display: inline-block;
+    }
+
+    .card input[type="radio"]+label::before {
+      content: "";
+      height: 0.9rem;
+      width: 0.9rem;
+      margin-right: 0.7rem;
+      border-radius: 50%;
+      border: 1px solid rgb(74, 74, 74);
+      background-color: #ffffff;
+      display: inline-block;
+      cursor: pointer;
+      position: relative;
+      top: 0.09rem;
+    }
+
+    .dark .card input[type="radio"]+label::before {
+      border: 1px solid #e2e2e2;
+      background-color: inherit;
+    }
+
+    .card input[type="radio"]:checked+label::before {
+      background-color: rgb(47, 106, 189);
+      border-color: rgb(47, 106, 189);
+    }
+
+    .dark .card input[type="radio"]:checked+label::before {
+      background-color: #d9d9d9;
+      border-color: #d9d9d9;
+    }
+
+    .card input[type="radio"]:checked+label {
+      font-weight: bold;
+      color: #4a4a4a;
+    }
+
+    .dark input[type="radio"]:checked+label {
+      color: #e2e2e2;
+    }
+
+    .card input[type="radio"]:focus+label::before {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    .card legend {
+      margin-bottom: 0.4rem;
+    }
+
+    .totals {
+      font-size: 1.6rem;
+    }
+
+    .totals+h2 {
+      font-size: 1.9rem;
+      margin-top: 0;
+    }
+
+    .grade {
+      font-size: 1.9rem;
+    }
+
+    button {
+      background-color: #fff;
+      border: 1px solid #4a4a4a;
+      border-radius: 6px;
+      box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+      color: #4a4a4a;
+      cursor: pointer;
+      display: inline-block;
+      font-size: 1rem;
+      font-weight: 500;
+      letter-spacing: 0.04em;
+      line-height: 1.8em;
+      margin: 0.75rem 1.1rem 1.1rem 0;
+      outline: none;
+      padding: 0.5rem 1.4rem;
+      text-transform: uppercase;
+      transition-delay: 0s;
+      transition-duration: 0.25s;
+      transition-property: all;
+      transition-timing-function: ease-out;
+      width: fit-content;
+    }
+
+    .dark button {
+      background: #d9d9d9;
+      color: #121212;
+      box-shadow: none;
+    }
+
+    button:hover {
+      border-color: #dbdbdb;
+      color: #dbdbdb;
+    }
+
+    .dark button:hover {
+      color: #121212;
+    }
+
+    button.copy {
+      margin-left: 0.4rem;
+      font-size: .8rem;
+    }
+
+    button:focus {
+      border-color: rgb(56, 56, 56);
+      box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+      color: rgb(56, 56, 56);
+    }
+
+    div[role="alert"] {
+      color: #4a4a4a;
+      font-size: 1.2rem;
+      margin-bottom: 1.2rem;
+    }
+
+    .dark div[role="alert"] {
+      color: #e2e2e2;
+    }
+
+    .summary {
+      font-size: 1.2rem;
+      border: none;
+      font-family: monospace;
+      background-color: #d5d9de;
+      padding: 0.9rem;
+      border-radius: 6px;
+      line-height: 1.5;
+      width: 100%;
+    }
+
+    .summary h2,
+    .dark .summary h2 {
+      font-size: 1.2rem;
+      font-family: "Inter", sans-serif;
+      margin-top: 35px;
+      margin-bottom: 5px;
+    }
+
+    .summary h2:first-of-type,
+    .dark .summary h2:first-of-type {
+      margin-top: 15px;
+    }
+
+    .dark .summary {
+      background: #6b6a6a;
+      color: #e2e2e2;
+    }
+
+    .summary pre,
+    .dark .summary pre {
+      white-space: pre-wrap;
+      border-radius: 5px;
+      padding: 15px;
+      border: solid 2px rgba(0, 0, 0, .4);
+    }
+
+    .summary pre:empty {
+      display: none;
+    }
+
+    .summary pre:empty+button {
+      display: none;
+    }
+
+    .d-none {
+      display: none;
+    }
+
+    @media (max-width: 450px) {
+      .btn-container {
+        text-align: center;
+      }
+
+      button {
+        width: 100%;
+        margin: 0.5rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+        background-size: 50%;
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+        background-size: 50%;
+      }
+    }
+
+    @media (max-width: 515px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.7rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.7rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 460px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.4rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.4rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 400px) {
+      #app h1 {
+        font-size: 1.6rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 340px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(0.9rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(0.9rem) scale(1.075);
+      }
+    }
+  </style>
+  <script>
+    $(function () {
+      let $select2 = $('.select2').select2({
+        containerCssClass: "wrap"
+      })
+
+      function triggerEvent(name, val) {
+        let selected = $("select[name='" + name + "']")
+        let change = new Event('change', { bubbles: true })
+        selected[0].dispatchEvent(change)
+      }
+
+      $('.select2').on('select2:select', function (e) {
+        triggerEvent(e.target.name, e.target.value)
+      });
+    })
+  </script>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script>
+      (function (window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Module 4 Web APIs Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: "Deployment",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Student submitted deployed live application URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title:
+                    "Application loads at deployed URL (no 404, or Heroku/GH Pages error)",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title: "Student submitted application GitHub URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Missing GitHub submission URL",
+                  status: false
+                },
+                {
+                  title: "GitHub Repo contains application code",
+                  score: 8,
+                  type: "true false",
+                  message: "GitHub repo is empty",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Technical Acceptance Criteria",
+              subtitle:
+                "Keep your browser console open and watch out for errors from this point forward.",
+              score: 0,
+              questions: [
+                {
+                  title:
+                    "The first view of the application displays a button used to start the quiz",
+                  score: 6,
+                  type: "true false",
+                  message: "First view of the application doesn't display a button to start the quiz",
+                  status: false
+                },
+                {
+                  title:
+                    "Clicking the 'Start Quiz' button displays a series of questions",
+                  score: 6,
+                  type: "true false",
+                  message:
+                    "Clicking the 'Start Quiz' button doesn't display a series of questions",
+                  status: false
+                },
+                {
+                  title:
+                    "Once the quiz begins, a countdown timer starts",
+                  score: 7,
+                  type: "true false",
+                  message:
+                    "A countdown timer doesn't start when the quiz begins",
+                  status: false
+                },
+                {
+                  title:
+                    "If a question is answered incorrectly, additional time is subtracted from the timer",
+                  score: 7,
+                  type: "true false",
+                  message:
+                    "Additional time isn't subtracted from the timer if a question is answered incorrectly",
+                  status: false
+                },
+                {
+                  title: "The game ends when all questions have been answered or the timer reaches zero",
+                  score: 7,
+                  type: "true false",
+                  message: "The timer doesn't stop when it reaches 0 or when all questions have been answered",
+                  status: false
+                },
+                {
+                  title: "After the game ends, the user can save their initials and score to a highscores view using local storage",
+                  score: 7,
+                  type: "true false",
+                  message: "Unable to save initials and score to highscores view using local storage",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Repository Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Repository has a unique name",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't have a unique name",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for file structure and naming conventions",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository contains multiple descriptive commit messages",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false
+                },
+                {
+                  title: "Repository contains quality README with description, screenshot, link to deployed application",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                      score: 0
+                    },
+                    {
+                      message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                      score: 2
+                    },
+                    {
+                      message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                      score: 4
+                    },
+                    {
+                      message: "Repository contains quality README with description, screenshot, link to deployed application",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Application Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Application user experience is intuitive and easy to navigate",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application user experience is not intuitive and easy to navigate",
+                      score: 0
+                    },
+                    {
+                      message: "Application user experience could be much more intuitive and easy to navigate",
+                      score: 2
+                    },
+                    {
+                      message: "Application user experience could be a lot more intuitive and easy to navigate",
+                      score: 4
+                    },
+                    {
+                      message: "Application user experience is intuitive and easy to navigate",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Application user interface style is clean and polished",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application user interface is not clean and polished",
+                      score: 0
+                    },
+                    {
+                      message: "Application user interface could be much better",
+                      score: 2
+                    },
+                    {
+                      message: "Application user interface could be a bit better",
+                      score: 4
+                    },
+                    {
+                      message: "Application user interface style is clean and polished",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Application resembles the mock-up functionality provided in Challenge instructions",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application doesn't resemble the mock-up functionality provided in Challenge instructions",
+                      score: 0
+                    },
+                    {
+                      message: "Application could resemble mock-up functionality provided with Challenge instructions much more closely",
+                      score: 2
+                    },
+                    {
+                      message: "Application could resemble mock-up functionality provided with Challenge instructions a bit more closely",
+                      score: 4
+                    },
+                    {
+                      message: "Application resembles the mock-up functionality provided in Challenge instructions",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Academic Integrity",
+              subtitle:
+                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+              score: 0,
+              questions: [
+                {
+                  title: "Application contains a low number of commits",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application codebase appears advanced for the student, at this point in the course, or in general",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references a different author and/or repository",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references much older versions of npm packages",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                }
+              ]
+            }
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+            .forEach(category => {
+              category.score = 0;
+              category.questions.forEach(question => {
+                question.category = category.title;
+                question.status = false;
+                question.id = this.questions.length;
+                question.flagged = category.title === "Academic Integrity";
+                question.bonus = category.title === "Bonus";
+                if (question.type === "likert") {
+                  question.message = question.options[0].message.split(" - ").pop();
+                };
+
+                let messageObj = {
+                  message: question.message,
+                  category: question.category
+                };
+
+                question.message && !question.bonus && this.messages.add(messageObj);
+                this.questions.push(question);
+                category.score += question.score;
+              });
+            });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+            if (question.type === "true false") {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector("select[name='" + question.id + "']").value;
+              categoryTotal += parseInt(score);
+            }
+          })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + "-summary";
+              return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + button;
+              return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+              `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+              `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+            }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .map(category => {
+                let score = category.score;
+                let title = category.title;
+                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                  .filter(messageObj => messageObj.category === category.title)
+                  .map(messageObj => `* ${messageObj.message}\n`)
+                  .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+              })
+              .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+            isDarkMode ? "checked" : ""
+            }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                .filter(messageObj => messageObj.category === category.title)
+                .map(messageObj => `* ${messageObj.message}\n`)
+                .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+
+            this.model.questions
+              .filter(question => question.category !== "Academic Integrity")
+              .forEach(question => {
+                const id = question.id;
+                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .forEach(category => {
+                const title = category.title
+                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+  </script>
+</body>
+
+</html>

+ 1304 - 0
rubrics/mod05.html

@@ -0,0 +1,1304 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+  <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+  <title>Module 5 Third-Party APIs Challenge Rubric</title>
+  <style>
+    *,
+    *::before,
+    *::after {
+      box-sizing: border-box;
+    }
+
+    html,
+    body {
+      margin: 0;
+      padding: 0;
+      line-height: 1.15;
+      font-size: 100%;
+      color: #fefefe;
+      transition: all 0.4s;
+    }
+
+    body {
+      background-color: #fbfcfd;
+      font-family: "Inter", sans-serif;
+      color: #4a4a4a;
+    }
+
+    #app.dark {
+      background-color: #121212;
+      color: #e2e2e2;
+    }
+
+    h1 {
+      font-size: 1.9rem;
+    }
+
+    h2 {
+      font-size: 1.6rem;
+    }
+
+    h3 {
+      font-size: 1.2rem;
+    }
+
+    h4 {
+      font-size: 0.9rem;
+    }
+
+    p,
+    legend {
+      font-size: 1.2rem;
+    }
+
+    label {
+      font-size: 0.9rem;
+      line-height: 2;
+    }
+
+    fieldset {
+      border: none;
+      padding: 0;
+      margin-top: 0.9rem;
+    }
+
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute !important;
+      height: 1px;
+      width: 1px;
+      overflow: hidden;
+      clip: rect(1px 1px 1px 1px);
+      clip: rect(1px, 1px, 1px, 1px);
+      white-space: nowrap;
+    }
+
+    .select-lg,
+    .select-lg option {
+      font-family: "Inter", sans-serif;
+      padding: 10px;
+    }
+
+    .narrow {
+      width: 800px;
+    }
+
+    #app {
+      padding: 3%;
+      transition: all 0.4s;
+    }
+
+    #app h1 {
+      font-size: 1.9rem;
+      font-weight: 700;
+      line-height: 1.3;
+    }
+
+    header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .switch-group {
+      display: flex;
+      align-items: center;
+      height: 2.25rem;
+      width: 4.5rem;
+    }
+
+    input[type="checkbox"]+.toggle {
+      height: 100%;
+      width: 100%;
+      background-color: #77a1db;
+      border-radius: 1.9rem;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      padding: 0.35rem;
+      cursor: pointer;
+      overflow: hidden;
+      background-position: 25% 125%;
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]:focus+.toggle {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    input[type="checkbox"]:checked+.toggle {
+      background-color: rgb(85, 89, 204);
+      background-size: 1rem;
+      background-repeat: no-repeat;
+      background-position: 25%;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]+.toggle::after {
+      content: "";
+      display: block;
+      height: 1.7rem;
+      width: 1.7rem;
+      border-radius: 50%;
+      cursor: pointer;
+      border: 4px solid #e1be64;
+      background-color: #fddf74;
+      transform: translateX(0rem);
+      transition: all 0.5s;
+      background-position: center bottom;
+    }
+
+    input[type="checkbox"]:hover+.toggle::after {
+      transform: translateX(0rem) scale(1.1);
+    }
+
+    input[type="checkbox"]:checked+.toggle::after {
+      border: 4px solid #ffffff;
+      background-color: rgba(255, 255, 255, 0.4);
+      transform: translateX(2.1rem);
+      transition: all 0.5s;
+      background-position: center;
+      background-size: 60%;
+      background-repeat: no-repeat;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+    }
+
+    input[type="checkbox"]:checked:hover+.toggle::after {
+      transform: translateX(2.1rem) scale(1.075);
+    }
+
+    .card {
+      background-color: #fff;
+      border-radius: 6px;
+      border-width: 0;
+      box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+      font-size: 1.1rem;
+      height: 100%;
+      line-height: 1.8rem;
+      padding: 1.5rem 2rem;
+      width: 100%;
+      margin-bottom: 1.9rem;
+    }
+
+    .dark .card {
+      background-color: #333333;
+      box-shadow: none;
+    }
+
+    .card h1,
+    .card h2,
+    .card h3,
+    .card h4,
+    .card h5,
+    .card h6 {
+      color: #4a4a4a;
+    }
+
+    .dark .card h1,
+    .dark .card h2,
+    .dark .card h3,
+    .dark .card h4,
+    .dark .card h5,
+    .dark .card h6 {
+      color: #e2e2e2;
+    }
+
+    .card-header h2 {
+      display: inline-block;
+    }
+
+    .card .cat-score {
+      margin-left: 20px;
+      font-size: smaller;
+    }
+
+    .card h3 {
+      font-size: 1.2rem;
+      font-weight: 700;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card p,
+    .card label,
+    .card legend {
+      color: #7a869c;
+      border-width: 0;
+      display: block;
+      font-size: 1.1rem;
+      font-weight: 300;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card legend {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+
+    .card legend .score {
+      font-size: smaller;
+      margin-left: 20px;
+      font-weight: 100;
+      min-width: 5%;
+    }
+
+    .dark .card p,
+    .dark .card label,
+    .dark .card legend {
+      color: #e2e2e2;
+    }
+
+    .card label {
+      display: inline-block;
+    }
+
+    .card input[type="radio"]+label::before {
+      content: "";
+      height: 0.9rem;
+      width: 0.9rem;
+      margin-right: 0.7rem;
+      border-radius: 50%;
+      border: 1px solid rgb(74, 74, 74);
+      background-color: #ffffff;
+      display: inline-block;
+      cursor: pointer;
+      position: relative;
+      top: 0.09rem;
+    }
+
+    .dark .card input[type="radio"]+label::before {
+      border: 1px solid #e2e2e2;
+      background-color: inherit;
+    }
+
+    .card input[type="radio"]:checked+label::before {
+      background-color: rgb(47, 106, 189);
+      border-color: rgb(47, 106, 189);
+    }
+
+    .dark .card input[type="radio"]:checked+label::before {
+      background-color: #d9d9d9;
+      border-color: #d9d9d9;
+    }
+
+    .card input[type="radio"]:checked+label {
+      font-weight: bold;
+      color: #4a4a4a;
+    }
+
+    .dark input[type="radio"]:checked+label {
+      color: #e2e2e2;
+    }
+
+    .card input[type="radio"]:focus+label::before {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    .card legend {
+      margin-bottom: 0.4rem;
+    }
+
+    .totals {
+      font-size: 1.6rem;
+    }
+
+    .totals+h2 {
+      font-size: 1.9rem;
+      margin-top: 0;
+    }
+
+    .grade {
+      font-size: 1.9rem;
+    }
+
+    button {
+      background-color: #fff;
+      border: 1px solid #4a4a4a;
+      border-radius: 6px;
+      box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+      color: #4a4a4a;
+      cursor: pointer;
+      display: inline-block;
+      font-size: 1rem;
+      font-weight: 500;
+      letter-spacing: 0.04em;
+      line-height: 1.8em;
+      margin: 0.75rem 1.1rem 1.1rem 0;
+      outline: none;
+      padding: 0.5rem 1.4rem;
+      text-transform: uppercase;
+      transition-delay: 0s;
+      transition-duration: 0.25s;
+      transition-property: all;
+      transition-timing-function: ease-out;
+      width: fit-content;
+    }
+
+    .dark button {
+      background: #d9d9d9;
+      color: #121212;
+      box-shadow: none;
+    }
+
+    button:hover {
+      border-color: #dbdbdb;
+      color: #dbdbdb;
+    }
+
+    .dark button:hover {
+      color: #121212;
+    }
+
+    button.copy {
+      margin-left: 0.4rem;
+      font-size: .8rem;
+    }
+
+    button:focus {
+      border-color: rgb(56, 56, 56);
+      box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+      color: rgb(56, 56, 56);
+    }
+
+    div[role="alert"] {
+      color: #4a4a4a;
+      font-size: 1.2rem;
+      margin-bottom: 1.2rem;
+    }
+
+    .dark div[role="alert"] {
+      color: #e2e2e2;
+    }
+
+    .summary {
+      font-size: 1.2rem;
+      border: none;
+      font-family: monospace;
+      background-color: #d5d9de;
+      padding: 0.9rem;
+      border-radius: 6px;
+      line-height: 1.5;
+      width: 100%;
+    }
+
+    .summary h2,
+    .dark .summary h2 {
+      font-size: 1.2rem;
+      font-family: "Inter", sans-serif;
+      margin-top: 35px;
+      margin-bottom: 5px;
+    }
+
+    .summary h2:first-of-type,
+    .dark .summary h2:first-of-type {
+      margin-top: 15px;
+    }
+
+    .dark .summary {
+      background: #6b6a6a;
+      color: #e2e2e2;
+    }
+
+    .summary pre,
+    .dark .summary pre {
+      white-space: pre-wrap;
+      border-radius: 5px;
+      padding: 15px;
+      border: solid 2px rgba(0, 0, 0, .4);
+    }
+
+    .summary pre:empty {
+      display: none;
+    }
+
+    .summary pre:empty+button {
+      display: none;
+    }
+
+    .d-none {
+      display: none;
+    }
+
+    @media (max-width: 450px) {
+      .btn-container {
+        text-align: center;
+      }
+
+      button {
+        width: 100%;
+        margin: 0.5rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+        background-size: 50%;
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+        background-size: 50%;
+      }
+    }
+
+    @media (max-width: 515px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.7rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.7rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 460px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.4rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.4rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 400px) {
+      #app h1 {
+        font-size: 1.6rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 340px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(0.9rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(0.9rem) scale(1.075);
+      }
+    }
+  </style>
+  <script>
+    $(function () {
+      let $select2 = $('.select2').select2({
+        containerCssClass: "wrap"
+      })
+
+      function triggerEvent(name, val) {
+        let selected = $("select[name='" + name + "']")
+        let change = new Event('change', { bubbles: true })
+        selected[0].dispatchEvent(change)
+      }
+
+      $('.select2').on('select2:select', function (e) {
+        triggerEvent(e.target.name, e.target.value)
+      });
+    })
+  </script>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script>
+      (function (window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Module 5 Third-Party APIs Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: "Deployment",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Student submitted deployed live application URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title:
+                    "Application loads at deployed URL (no 404, or Heroku/GH Pages error)",
+                  score: 8,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title: "Student submitted application GitHub URL",
+                  score: 8,
+                  type: "true false",
+                  message: "Missing GitHub submission URL",
+                  status: false
+                },
+                {
+                  title: "GitHub Repo contains application code",
+                  score: 8,
+                  type: "true false",
+                  message: "GitHub repo is empty",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Technical Acceptance Criteria",
+              subtitle:
+                "Keep your browser console open and watch out for errors from this point forward.",
+              score: 0,
+              questions: [
+              {
+                  title:
+                    "The application displays timeblocks for standard business hours (9 a.m. to 5 p.m.)",
+                  score: 8,
+                  type: "true false",
+                  message:
+                    "Application doesn't display timeblocks for all standard business hours",
+                  status: false
+                },
+                {
+                  title:
+                    "Each timeblock contains an input field and save button",
+                  score: 8,
+                  type: "true false",
+                  message:
+                    "Timeblocks don't contain an input field and save button",
+                  status: false
+                },
+                {
+                  title:
+                    "Clicking a time block's save button saves the input text to local storage, allowing the text to persist when the application is refreshed",
+                  score: 8,
+                  type: "true false",
+                  message:
+                    "Clicking a time block's save button doesn't save the input text to local storage",
+                  status: false
+                },
+                {
+                  title:
+                    "The current day is displayed at the top of the calendar",
+                  score: 8,
+                  type: "true false",
+                  message:
+                    "The current day isn't displayed at the top of the calendar",
+                  status: false
+                },
+                {
+                  title: "Each timeblock is color coded to indicate whether it is in a past, present, or future hour",
+                  score: 8,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Time blocks aren't color coded to indicate whether they are in the past, present, or future",
+                      score: 0
+                    },
+                    {
+                      message: "Time blocks are only color coded to indicate whether they are in 1 of the following: the past, present, and future",
+                      score: 3
+                    },
+                    {
+                      message: "Time blocks are only color coded to indicate whether they are in 2 of the following: the past, present, and future",
+                      score: 6
+                    },
+                    {
+                      message: "Each timeblock is color coded to indicate whether it is in a past, present, or future hour",
+                      score: 8
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Repository Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Repository has a unique name",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't have a unique name",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for file structure and naming conventions",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository contains multiple descriptive commit messages",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false
+                },
+                {
+                  title: "Repository contains quality README with description, screenshot, link to deployed application",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                      score: 0
+                    },
+                    {
+                      message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                      score: 2
+                    },
+                    {
+                      message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                      score: 4
+                    },
+                    {
+                      message: "Repository contains quality README with description, screenshot, link to deployed application",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Application Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Application user experience is intuitive and easy to navigate",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application user experience is not intuitive and easy to navigate",
+                      score: 0
+                    },
+                    {
+                      message: "Application user experience could be much more intuitive and easy to navigate",
+                      score: 2
+                    },
+                    {
+                      message: "Application user experience could be a lot more intuitive and easy to navigate",
+                      score: 4
+                    },
+                    {
+                      message: "Application user experience is intuitive and easy to navigate",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Application user interface style is clean and polished",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application user interface is not clean and polished",
+                      score: 0
+                    },
+                    {
+                      message: "Application user interface could be much better",
+                      score: 2
+                    },
+                    {
+                      message: "Application user interface could be a bit better",
+                      score: 4
+                    },
+                    {
+                      message: "Application user interface style is clean and polished",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Application resembles the mock-up functionality provided in Challenge instructions",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application doesn't resemble the mock-up functionality provided in Challenge instructions",
+                      score: 0
+                    },
+                    {
+                      message: "Application could resemble mock-up functionality provided with Challenge instructions much more closely",
+                      score: 2
+                    },
+                    {
+                      message: "Application could resemble mock-up functionality provided with Challenge instructions a bit more closely",
+                      score: 4
+                    },
+                    {
+                      message: "Application resembles the mock-up functionality provided in Challenge instructions",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Academic Integrity",
+              subtitle:
+                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+              score: 0,
+              questions: [
+                {
+                  title: "Application contains a low number of commits",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application codebase appears advanced for the student, at this point in the course, or in general",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references a different author and/or repository",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references much older versions of npm packages",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                }
+              ]
+            }
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+            .forEach(category => {
+              category.score = 0;
+              category.questions.forEach(question => {
+                question.category = category.title;
+                question.status = false;
+                question.id = this.questions.length;
+                question.flagged = category.title === "Academic Integrity";
+                question.bonus = category.title === "Bonus";
+                if (question.type === "likert") {
+                  question.message = question.options[0].message.split(" - ").pop();
+                };
+
+                let messageObj = {
+                  message: question.message,
+                  category: question.category
+                };
+
+                question.message && !question.bonus && this.messages.add(messageObj);
+                this.questions.push(question);
+                category.score += question.score;
+              });
+            });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+            if (question.type === "true false") {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector("select[name='" + question.id + "']").value;
+              categoryTotal += parseInt(score);
+            }
+          })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + "-summary";
+              return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + button;
+              return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+              `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+              `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+            }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .map(category => {
+                let score = category.score;
+                let title = category.title;
+                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                  .filter(messageObj => messageObj.category === category.title)
+                  .map(messageObj => `* ${messageObj.message}\n`)
+                  .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+              })
+              .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+            isDarkMode ? "checked" : ""
+            }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                .filter(messageObj => messageObj.category === category.title)
+                .map(messageObj => `* ${messageObj.message}\n`)
+                .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+
+            this.model.questions
+              .filter(question => question.category !== "Academic Integrity")
+              .forEach(question => {
+                const id = question.id;
+                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .forEach(category => {
+                const title = category.title
+                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+  </script>
+</body>
+
+</html>

+ 1430 - 0
rubrics/mod06.html

@@ -0,0 +1,1430 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link
+      href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css"
+      rel="stylesheet"
+    />
+    <title>Module 6 Server-Side APIs Challenge Rubric</title>
+    <style>
+      *,
+      *::before,
+      *::after {
+        box-sizing: border-box;
+      }
+
+      html,
+      body {
+        margin: 0;
+        padding: 0;
+        line-height: 1.15;
+        font-size: 100%;
+        color: #fefefe;
+        transition: all 0.4s;
+      }
+
+      body {
+        background-color: #fbfcfd;
+        font-family: 'Inter', sans-serif;
+        color: #4a4a4a;
+      }
+
+      #app.dark {
+        background-color: #121212;
+        color: #e2e2e2;
+      }
+
+      h1 {
+        font-size: 1.9rem;
+      }
+
+      h2 {
+        font-size: 1.6rem;
+      }
+
+      h3 {
+        font-size: 1.2rem;
+      }
+
+      h4 {
+        font-size: 0.9rem;
+      }
+
+      p,
+      legend {
+        font-size: 1.2rem;
+      }
+
+      label {
+        font-size: 0.9rem;
+        line-height: 2;
+      }
+
+      fieldset {
+        border: none;
+        padding: 0;
+        margin-top: 0.9rem;
+      }
+
+      input[type='radio'],
+      input[type='checkbox'] {
+        position: absolute !important;
+        height: 1px;
+        width: 1px;
+        overflow: hidden;
+        clip: rect(1px 1px 1px 1px);
+        clip: rect(1px, 1px, 1px, 1px);
+        white-space: nowrap;
+      }
+
+      .select-lg,
+      .select-lg option {
+        font-family: 'Inter', sans-serif;
+        padding: 10px;
+      }
+
+      .narrow {
+        width: 800px;
+      }
+
+      #app {
+        padding: 3%;
+        transition: all 0.4s;
+      }
+
+      #app h1 {
+        font-size: 1.9rem;
+        font-weight: 700;
+        line-height: 1.3;
+      }
+
+      header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+      }
+
+      .switch-group {
+        display: flex;
+        align-items: center;
+        height: 2.25rem;
+        width: 4.5rem;
+      }
+
+      input[type='checkbox'] + .toggle {
+        height: 100%;
+        width: 100%;
+        background-color: #77a1db;
+        border-radius: 1.9rem;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        padding: 0.35rem;
+        cursor: pointer;
+        overflow: hidden;
+        background-position: 25% 125%;
+        transition: all 0.5s;
+      }
+
+      input[type='checkbox']:focus + .toggle {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      input[type='checkbox']:checked + .toggle {
+        background-color: rgb(85, 89, 204);
+        background-size: 1rem;
+        background-repeat: no-repeat;
+        background-position: 25%;
+        background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==');
+        transition: all 0.5s;
+      }
+
+      input[type='checkbox'] + .toggle::after {
+        content: '';
+        display: block;
+        height: 1.7rem;
+        width: 1.7rem;
+        border-radius: 50%;
+        cursor: pointer;
+        border: 4px solid #e1be64;
+        background-color: #fddf74;
+        transform: translateX(0rem);
+        transition: all 0.5s;
+        background-position: center bottom;
+      }
+
+      input[type='checkbox']:hover + .toggle::after {
+        transform: translateX(0rem) scale(1.1);
+      }
+
+      input[type='checkbox']:checked + .toggle::after {
+        border: 4px solid #ffffff;
+        background-color: rgba(255, 255, 255, 0.4);
+        transform: translateX(2.1rem);
+        transition: all 0.5s;
+        background-position: center;
+        background-size: 60%;
+        background-repeat: no-repeat;
+        background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
+      }
+
+      input[type='checkbox']:checked:hover + .toggle::after {
+        transform: translateX(2.1rem) scale(1.075);
+      }
+
+      .card {
+        background-color: #fff;
+        border-radius: 6px;
+        border-width: 0;
+        box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+        font-size: 1.1rem;
+        height: 100%;
+        line-height: 1.8rem;
+        padding: 1.5rem 2rem;
+        width: 100%;
+        margin-bottom: 1.9rem;
+      }
+
+      .dark .card {
+        background-color: #333333;
+        box-shadow: none;
+      }
+
+      .card h1,
+      .card h2,
+      .card h3,
+      .card h4,
+      .card h5,
+      .card h6 {
+        color: #4a4a4a;
+      }
+
+      .dark .card h1,
+      .dark .card h2,
+      .dark .card h3,
+      .dark .card h4,
+      .dark .card h5,
+      .dark .card h6 {
+        color: #e2e2e2;
+      }
+
+      .card-header h2 {
+        display: inline-block;
+      }
+
+      .card .cat-score {
+        margin-left: 20px;
+        font-size: smaller;
+      }
+
+      .card h3 {
+        font-size: 1.2rem;
+        font-weight: 700;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card p,
+      .card label,
+      .card legend {
+        color: #7a869c;
+        border-width: 0;
+        display: block;
+        font-size: 1.1rem;
+        font-weight: 300;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card legend {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+      }
+
+      .card legend .score {
+        font-size: smaller;
+        margin-left: 20px;
+        font-weight: 100;
+        min-width: 5%;
+      }
+
+      .dark .card p,
+      .dark .card label,
+      .dark .card legend {
+        color: #e2e2e2;
+      }
+
+      .card label {
+        display: inline-block;
+      }
+
+      .card input[type='radio'] + label::before {
+        content: '';
+        height: 0.9rem;
+        width: 0.9rem;
+        margin-right: 0.7rem;
+        border-radius: 50%;
+        border: 1px solid rgb(74, 74, 74);
+        background-color: #ffffff;
+        display: inline-block;
+        cursor: pointer;
+        position: relative;
+        top: 0.09rem;
+      }
+
+      .dark .card input[type='radio'] + label::before {
+        border: 1px solid #e2e2e2;
+        background-color: inherit;
+      }
+
+      .card input[type='radio']:checked + label::before {
+        background-color: rgb(47, 106, 189);
+        border-color: rgb(47, 106, 189);
+      }
+
+      .dark .card input[type='radio']:checked + label::before {
+        background-color: #d9d9d9;
+        border-color: #d9d9d9;
+      }
+
+      .card input[type='radio']:checked + label {
+        font-weight: bold;
+        color: #4a4a4a;
+      }
+
+      .dark input[type='radio']:checked + label {
+        color: #e2e2e2;
+      }
+
+      .card input[type='radio']:focus + label::before {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      .card legend {
+        margin-bottom: 0.4rem;
+      }
+
+      .totals {
+        font-size: 1.6rem;
+      }
+
+      .totals + h2 {
+        font-size: 1.9rem;
+        margin-top: 0;
+      }
+
+      .grade {
+        font-size: 1.9rem;
+      }
+
+      button {
+        background-color: #fff;
+        border: 1px solid #4a4a4a;
+        border-radius: 6px;
+        box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+        color: #4a4a4a;
+        cursor: pointer;
+        display: inline-block;
+        font-size: 1rem;
+        font-weight: 500;
+        letter-spacing: 0.04em;
+        line-height: 1.8em;
+        margin: 0.75rem 1.1rem 1.1rem 0;
+        outline: none;
+        padding: 0.5rem 1.4rem;
+        text-transform: uppercase;
+        transition-delay: 0s;
+        transition-duration: 0.25s;
+        transition-property: all;
+        transition-timing-function: ease-out;
+        width: fit-content;
+      }
+
+      .dark button {
+        background: #d9d9d9;
+        color: #121212;
+        box-shadow: none;
+      }
+
+      button:hover {
+        border-color: #dbdbdb;
+        color: #dbdbdb;
+      }
+
+      .dark button:hover {
+        color: #121212;
+      }
+
+      button.copy {
+        margin-left: 0.4rem;
+        font-size: 0.8rem;
+      }
+
+      button:focus {
+        border-color: rgb(56, 56, 56);
+        box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+        color: rgb(56, 56, 56);
+      }
+
+      div[role='alert'] {
+        color: #4a4a4a;
+        font-size: 1.2rem;
+        margin-bottom: 1.2rem;
+      }
+
+      .dark div[role='alert'] {
+        color: #e2e2e2;
+      }
+
+      .summary {
+        font-size: 1.2rem;
+        border: none;
+        font-family: monospace;
+        background-color: #d5d9de;
+        padding: 0.9rem;
+        border-radius: 6px;
+        line-height: 1.5;
+        width: 100%;
+      }
+
+      .summary h2,
+      .dark .summary h2 {
+        font-size: 1.2rem;
+        font-family: 'Inter', sans-serif;
+        margin-top: 35px;
+        margin-bottom: 5px;
+      }
+
+      .summary h2:first-of-type,
+      .dark .summary h2:first-of-type {
+        margin-top: 15px;
+      }
+
+      .dark .summary {
+        background: #6b6a6a;
+        color: #e2e2e2;
+      }
+
+      .summary pre,
+      .dark .summary pre {
+        white-space: pre-wrap;
+        border-radius: 5px;
+        padding: 15px;
+        border: solid 2px rgba(0, 0, 0, 0.4);
+      }
+
+      .summary pre:empty {
+        display: none;
+      }
+
+      .summary pre:empty + button {
+        display: none;
+      }
+
+      .d-none {
+        display: none;
+      }
+
+      @media (max-width: 450px) {
+        .btn-container {
+          text-align: center;
+        }
+
+        button {
+          width: 100%;
+          margin: 0.5rem;
+        }
+
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.3rem);
+          background-size: 50%;
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+          background-size: 50%;
+        }
+      }
+
+      @media (max-width: 515px) {
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.7rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.7rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 460px) {
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.4rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.4rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 400px) {
+        #app h1 {
+          font-size: 1.6rem;
+        }
+
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.3rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 340px) {
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(0.9rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(0.9rem) scale(1.075);
+        }
+      }
+    </style>
+    <script>
+      $(function () {
+        let $select2 = $('.select2').select2({
+          containerCssClass: 'wrap',
+        });
+
+        function triggerEvent(name, val) {
+          let selected = $("select[name='" + name + "']");
+          let change = new Event('change', { bubbles: true });
+          selected[0].dispatchEvent(change);
+        }
+
+        $('.select2').on('select2:select', function (e) {
+          triggerEvent(e.target.name, e.target.value);
+        });
+      });
+    </script>
+  </head>
+
+  <body>
+    <div id="app"></div>
+    <script>
+      (function (window) {
+        'use strict';
+
+        function Model() {
+          this.title = `Module 6 Server-Side APIs Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: 'Deployment',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: 'Student submitted deployed live application URL',
+                  score: 8,
+                  type: 'true false',
+                  message: "Application isn't deployed",
+                  status: false,
+                },
+                {
+                  title:
+                    'Application loads at deployed URL (no 404, or Heroku/GH Pages error)',
+                  score: 8,
+                  type: 'true false',
+                  message: "Application isn't deployed",
+                  status: false,
+                },
+                {
+                  title: 'Student submitted application GitHub URL',
+                  score: 8,
+                  type: 'true false',
+                  message: 'Missing GitHub submission URL',
+                  status: false,
+                },
+                {
+                  title: 'GitHub Repo contains application code',
+                  score: 8,
+                  type: 'true false',
+                  message: 'GitHub repo is empty',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Technical Acceptance Criteria',
+              subtitle:
+                'Keep your browser console open and watch out for errors from this point forward.',
+              score: 0,
+              questions: [
+                {
+                  title:
+                    'User can search for weather reports by city using the openweathermap API',
+                  score: 8,
+                  type: 'true false',
+                  message:
+                    "User can't search for weather reports using the openweathermap API",
+                  status: false,
+                },
+                {
+                  title:
+                    'After searching for a city, the following information is displayed: city name, date, an icon representation of weather conditions, current temperature, current humidity, and windspeed',
+                  score: 8,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'After searching for a city, none of the following 6 categories is displayed: city name, date, an icon representation of weather conditions, current temperature, current humidity, and windspeed',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'After searching for a city, 3 of the following 6 categories are displayed: city name, date, an icon representation of weather conditions, current temperature, current humidity, and windspeed',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'After searching for a city, 4 of the following 6 categories are displayed: city name, date, an icon representation of weather conditions, current temperature, current humidity, and windspeed',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'After searching for a city, 5 of the following 6 categories are displayed: city name, date, an icon representation of weather conditions, current temperature, current humidity, and windspeed',
+                      score: 6,
+                    },
+                    {
+                      message:
+                        'After searching for a city, all of the following information is displayed: city name, date, an icon representation of weather conditions, current temperature, current humidity, and windspeed',
+                      score: 8,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'After searching for a city, a 5 day forecast is provided that displays the date, an icon representation of weather conditions, the temperature, windspeed, and the humidity',
+                  score: 8,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'After searching for a city, a 5 day forecast is not provided or the forecast is missing 4 or more of the following 5 categories: the date, an icon representation of weather conditions, the temperature, windspeed, and the humidity',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'After searching for a city, a 5 day forecast is provided but is missing 3 of the following 5 categories: the date, an icon representation of weather conditions, the temperature, windspeed, and the humidity',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'After searching for a city, a 5 day forecast is provided but is missing 2 of the following 5 categories: the date, an icon representation of weather conditions, the temperature, windspeed, and the humidity',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'After searching for a city, a 5 day forecast is provided but is missing 1 of the following 5 categories: the date, an icon representation of weather conditions, the temperature, windspeed, and the humidity',
+                      score: 6,
+                    },
+                    {
+                      message:
+                        'After searching for a city, a 5 day forecast is provided that displays the date, an icon representation of weather conditions, the temperature, windspeed, and the humidity',
+                      score: 8,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Application stores previously searched for cities in local storage and displays them to the user',
+                  score: 8,
+                  type: 'true false',
+                  message:
+                    "Application doesn't store or display previously searched cities for the user",
+                  status: false,
+                },
+                {
+                  title:
+                    "User is able to repeat a search for a city's current and future weather conditions by clicking on it in the search history",
+                  score: 8,
+                  type: 'true false',
+                  message:
+                    "User is unable to repeat a search for a city's current and future weather conditions by clicking on it in the search history",
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Repository Quality',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: 'Repository has a unique name',
+                  score: 2,
+                  type: 'true false',
+                  message: "Repo doesn't have a unique name",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository follows best practices for file structure and naming conventions',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository contains multiple descriptive commit messages',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository contains quality README with description, screenshot, link to deployed application',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Repository README is missing 2 of the following: description, screenshot, or link to deployed application',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Repository README is missing 1 of the following: description, screenshot, or link to deployed application',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'Repository contains quality README with description, screenshot, link to deployed application',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Application Quality',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title:
+                    'Application user experience is intuitive and easy to navigate',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'Application user experience is not intuitive and easy to navigate',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application user experience could be much more intuitive and easy to navigate',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application user experience could be a lot more intuitive and easy to navigate',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'Application user experience is intuitive and easy to navigate',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Application user interface style is clean and polished',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'Application user interface is not clean and polished',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application user interface could be much better',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application user interface could be a bit better',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'Application user interface style is clean and polished',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Application resembles the mock-up functionality provided in Challenge instructions',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Application doesn't resemble the mock-up functionality provided in Challenge instructions",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application could resemble mock-up functionality provided with Challenge instructions much more closely',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application could resemble mock-up functionality provided with Challenge instructions a bit more closely',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'Application resembles the mock-up functionality provided in Challenge instructions',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Academic Integrity',
+              subtitle:
+                'If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.',
+              score: 0,
+              questions: [
+                {
+                  title: 'Application contains a low number of commits',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+                {
+                  title:
+                    'Application codebase appears advanced for the student, at this point in the course, or in general',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+                {
+                  title:
+                    'Application package.json file references a different author and/or repository',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+                {
+                  title:
+                    'Application package.json file references much older versions of npm packages',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+              ],
+            },
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem('isDarkMode')) || false;
+
+          this.categories.forEach((category) => {
+            category.score = 0;
+            category.questions.forEach((question) => {
+              question.category = category.title;
+              question.status = false;
+              question.id = this.questions.length;
+              question.flagged = category.title === 'Academic Integrity';
+              question.bonus = category.title === 'Bonus';
+              if (question.type === 'likert') {
+                question.message = question.options[0].message
+                  .split(' - ')
+                  .pop();
+              }
+
+              let messageObj = {
+                message: question.message,
+                category: question.category,
+              };
+
+              question.message &&
+                !question.bonus &&
+                this.messages.add(messageObj);
+              this.questions.push(question);
+              category.score += question.score;
+            });
+          });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem('isDarkMode', this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute('name'));
+          const question = this.questions[id];
+          const questionScore = document.querySelector(
+            "span[data-id='" + id + "']"
+          );
+          const category = question.category;
+          const categoryScore = document.querySelector(
+            "span[data-cat='" + category + "']"
+          );
+          const resultsCategoryScore = document.querySelector(
+            "span[data-res-cat='" + category + "']"
+          );
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+            if (!question.bonus) {
+              this.messages.forEach((message) => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              });
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category,
+              };
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category,
+                };
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach((message) => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              });
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== 'Academic Integrity') {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(
+              (cat) => cat.title === category
+            )[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach((question) => {
+              if (question.type === 'true false') {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector(
+                  "select[name='" + question.id + "']"
+                ).value;
+                categoryTotal += parseInt(score);
+              }
+            });
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute('name'));
+          const question = this.questions[id];
+          const questionScore = document.querySelector(
+            "span[data-id='" + id + "']"
+          );
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector(
+            "span[data-cat='" + category + "']"
+          );
+          const resultsCategoryScore = document.querySelector(
+            "span[data-res-cat='" + category + "']"
+          );
+          let newMessage = select.selectedOptions[0].text.split(' - ').pop();
+
+          if (newMessage === 'Open this menu') {
+            newMessage = select[1].text.split(' - ').pop();
+          }
+
+          if (question.type === 'likert') {
+            this.messages.forEach((item) => {
+              if (item.message === question.message) {
+                this.messages.delete(item);
+              }
+            });
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category,
+            };
+
+            this.messages.add(messageObj);
+          }
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(
+            (cat) => cat.title === category
+          )[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach((question) => {
+            if (question.type === 'true false') {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector(
+                "select[name='" + question.id + "']"
+              ).value;
+              categoryTotal += parseInt(score);
+            }
+          });
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = '';
+
+          if (roundedScore >= 97) {
+            letterGrade = 'A+';
+          } else if (roundedScore >= 93 && roundedScore <= 96) {
+            letterGrade = 'A';
+          } else if (roundedScore >= 90 && roundedScore <= 92) {
+            letterGrade = 'A-';
+          } else if (roundedScore >= 87 && roundedScore <= 89) {
+            letterGrade = 'B+';
+          } else if (roundedScore >= 83 && roundedScore <= 86) {
+            letterGrade = 'B';
+          } else if (roundedScore >= 80 && roundedScore <= 82) {
+            letterGrade = 'B-';
+          } else if (roundedScore >= 77 && roundedScore <= 79) {
+            letterGrade = 'C+';
+          } else if (roundedScore >= 73 && roundedScore <= 76) {
+            letterGrade = 'C';
+          } else if (roundedScore >= 70 && roundedScore <= 72) {
+            letterGrade = 'C-';
+          } else if (roundedScore >= 67 && roundedScore <= 69) {
+            letterGrade = 'D+';
+          } else if (roundedScore >= 63 && roundedScore <= 66) {
+            letterGrade = 'D';
+          } else if (roundedScore >= 60 && roundedScore <= 62) {
+            letterGrade = 'D-';
+          } else {
+            letterGrade = 'F';
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector('#dark-mode');
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(document.querySelectorAll('select'));
+
+          this.totals = document.querySelector('.totals');
+          this.grade = document.querySelector('.grade');
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector('.summary-heading');
+          this.summary = document.querySelector('.summary');
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .map((category) => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + '-summary';
+              return (this[summaryName] = document.querySelector(
+                '.' + summary
+              ));
+            });
+
+          this.resetButton = document.querySelector('.reset');
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .map((category) => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = 'copyButton-' + button;
+              return (this[buttonName] = document.querySelector(
+                '.copy-' + button
+              ));
+            });
+        };
+
+        View.prototype.createFieldset = function ({
+          title,
+          id,
+          score,
+          status,
+          category,
+          type,
+          options,
+        }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${
+            category === 'Academic Integrity'
+              ? ''
+              : "<span class='score'><span data-id='" +
+                id +
+                "' class='current-score'>0</span> / " +
+                score +
+                '</span>'
+          }
+          </legend>
+          ${
+            type === 'true false'
+              ? `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>`
+              : `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(
+                (option) =>
+                  `<option value="${option.score}">${option.score} - ${option.message}</option>`
+              )}
+            </select>
+          </div>`
+          }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({
+          title,
+          subtitle,
+          questions,
+          score,
+        }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${
+            title !== 'Academic Integrity'
+              ? "<span class='cat-score'><span data-cat='" +
+                title +
+                "'>0</span> / " +
+                score +
+                '</span>'
+              : ''
+          }</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ''}
+          ${questions.map((child) => this.createFieldset(child)).join('')}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+          .filter((category) => category.title !== 'Academic Integrity')
+          .map((category) => {
+            let score = category.score;
+            let title = category.title;
+            let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+            return (
+              '<h2>' +
+              title +
+              "<span class='cat-score'><span data-res-cat='" +
+              title +
+              "'>0</span> / " +
+              score +
+              "</span></h2><pre class='" +
+              catClass +
+              "'>" +
+              [...this.model.messages]
+                .filter((messageObj) => messageObj.category === category.title)
+                .map((messageObj) => `* ${messageObj.message}\n`)
+                .join('') +
+              "</pre><button class='copy copy-" +
+              catClass +
+              "'>Copy To Clipboard</button>"
+            );
+          })
+          .join('')}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+                isDarkMode ? 'checked' : ''
+              }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector('#app');
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add('dark');
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, '');
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove('d-none');
+          } else {
+            this.alert.classList.add('d-none');
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add('dark');
+          } else {
+            this.wrapper.classList.remove('dark');
+          }
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .forEach((category) => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + '-summary'].innerHTML = [...this.model.messages]
+                .filter((messageObj) => messageObj.category === category.title)
+                .map((messageObj) => `* ${messageObj.message}\n`)
+                .join('');
+            });
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener('change', (event) => {
+            const { target } = event;
+
+            if (target.name !== 'dark-mode') {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener('click', () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              (radio) => (radio.checked = radio.hasAttribute('checked'))
+            );
+
+            this.view.selects.forEach(
+              (select) => (select.selectedIndex = null)
+            );
+
+            this.model.questions
+              .filter((question) => question.category !== 'Academic Integrity')
+              .forEach((question) => {
+                const id = question.id;
+                const questionScore = document.querySelector(
+                  "span[data-id='" + id + "']"
+                );
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter((category) => category.title !== 'Academic Integrity')
+              .forEach((category) => {
+                const title = category.title;
+                const categoryScore = document.querySelector(
+                  "span[data-cat='" + title + "']"
+                );
+                const resultsCategoryScore = document.querySelector(
+                  "span[data-res-cat='" + title + "']"
+                );
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .forEach((category) => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = 'copyButton-' + title;
+
+              let summaryName = title + '-summary';
+
+              this.view[buttonName].addEventListener('click', () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand('copy');
+              });
+            });
+
+          this.view.darkModeToggle.addEventListener('change', () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+    </script>
+  </body>
+</html>

+ 1262 - 0
rubrics/mod08.html

@@ -0,0 +1,1262 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+  <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+  <title>Group Project 1 Rubric</title>
+  <style>
+    *,
+    *::before,
+    *::after {
+      box-sizing: border-box;
+    }
+
+    html,
+    body {
+      margin: 0;
+      padding: 0;
+      line-height: 1.15;
+      font-size: 100%;
+      color: #fefefe;
+      transition: all 0.4s;
+    }
+
+    body {
+      background-color: #fbfcfd;
+      font-family: "Inter", sans-serif;
+      color: #4a4a4a;
+    }
+
+    #app.dark {
+      background-color: #121212;
+      color: #e2e2e2;
+    }
+
+    h1 {
+      font-size: 1.9rem;
+    }
+
+    h2 {
+      font-size: 1.6rem;
+    }
+
+    h3 {
+      font-size: 1.2rem;
+    }
+
+    h4 {
+      font-size: 0.9rem;
+    }
+
+    p,
+    legend {
+      font-size: 1.2rem;
+    }
+
+    label {
+      font-size: 0.9rem;
+      line-height: 2;
+    }
+
+    fieldset {
+      border: none;
+      padding: 0;
+      margin-top: 0.9rem;
+    }
+
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute !important;
+      height: 1px;
+      width: 1px;
+      overflow: hidden;
+      clip: rect(1px 1px 1px 1px);
+      clip: rect(1px, 1px, 1px, 1px);
+      white-space: nowrap;
+    }
+
+    .select-lg,
+    .select-lg option {
+      font-family: "Inter", sans-serif;
+      padding: 10px;
+    }
+
+    .narrow {
+      width: 800px;
+    }
+
+    #app {
+      padding: 3%;
+      transition: all 0.4s;
+    }
+
+    #app h1 {
+      font-size: 1.9rem;
+      font-weight: 700;
+      line-height: 1.3;
+    }
+
+    header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .switch-group {
+      display: flex;
+      align-items: center;
+      height: 2.25rem;
+      width: 4.5rem;
+    }
+
+    input[type="checkbox"]+.toggle {
+      height: 100%;
+      width: 100%;
+      background-color: #77a1db;
+      border-radius: 1.9rem;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      padding: 0.35rem;
+      cursor: pointer;
+      overflow: hidden;
+      background-position: 25% 125%;
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]:focus+.toggle {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    input[type="checkbox"]:checked+.toggle {
+      background-color: rgb(85, 89, 204);
+      background-size: 1rem;
+      background-repeat: no-repeat;
+      background-position: 25%;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]+.toggle::after {
+      content: "";
+      display: block;
+      height: 1.7rem;
+      width: 1.7rem;
+      border-radius: 50%;
+      cursor: pointer;
+      border: 4px solid #e1be64;
+      background-color: #fddf74;
+      transform: translateX(0rem);
+      transition: all 0.5s;
+      background-position: center bottom;
+    }
+
+    input[type="checkbox"]:hover+.toggle::after {
+      transform: translateX(0rem) scale(1.1);
+    }
+
+    input[type="checkbox"]:checked+.toggle::after {
+      border: 4px solid #ffffff;
+      background-color: rgba(255, 255, 255, 0.4);
+      transform: translateX(2.1rem);
+      transition: all 0.5s;
+      background-position: center;
+      background-size: 60%;
+      background-repeat: no-repeat;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+    }
+
+    input[type="checkbox"]:checked:hover+.toggle::after {
+      transform: translateX(2.1rem) scale(1.075);
+    }
+
+    .card {
+      background-color: #fff;
+      border-radius: 6px;
+      border-width: 0;
+      box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+      font-size: 1.1rem;
+      height: 100%;
+      line-height: 1.8rem;
+      padding: 1.5rem 2rem;
+      width: 100%;
+      margin-bottom: 1.9rem;
+    }
+
+    .dark .card {
+      background-color: #333333;
+      box-shadow: none;
+    }
+
+    .card h1,
+    .card h2,
+    .card h3,
+    .card h4,
+    .card h5,
+    .card h6 {
+      color: #4a4a4a;
+    }
+
+    .dark .card h1,
+    .dark .card h2,
+    .dark .card h3,
+    .dark .card h4,
+    .dark .card h5,
+    .dark .card h6 {
+      color: #e2e2e2;
+    }
+
+    .card-header h2 {
+      display: inline-block;
+    }
+
+    .card .cat-score {
+      margin-left: 20px;
+      font-size: smaller;
+    }
+
+    .card h3 {
+      font-size: 1.2rem;
+      font-weight: 700;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card p,
+    .card label,
+    .card legend {
+      color: #7a869c;
+      border-width: 0;
+      display: block;
+      font-size: 1.1rem;
+      font-weight: 300;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card legend {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+
+    .card legend .score {
+      font-size: smaller;
+      margin-left: 20px;
+      font-weight: 100;
+      min-width: 5%;
+    }
+
+    .dark .card p,
+    .dark .card label,
+    .dark .card legend {
+      color: #e2e2e2;
+    }
+
+    .card label {
+      display: inline-block;
+    }
+
+    .card input[type="radio"]+label::before {
+      content: "";
+      height: 0.9rem;
+      width: 0.9rem;
+      margin-right: 0.7rem;
+      border-radius: 50%;
+      border: 1px solid rgb(74, 74, 74);
+      background-color: #ffffff;
+      display: inline-block;
+      cursor: pointer;
+      position: relative;
+      top: 0.09rem;
+    }
+
+    .dark .card input[type="radio"]+label::before {
+      border: 1px solid #e2e2e2;
+      background-color: inherit;
+    }
+
+    .card input[type="radio"]:checked+label::before {
+      background-color: rgb(47, 106, 189);
+      border-color: rgb(47, 106, 189);
+    }
+
+    .dark .card input[type="radio"]:checked+label::before {
+      background-color: #d9d9d9;
+      border-color: #d9d9d9;
+    }
+
+    .card input[type="radio"]:checked+label {
+      font-weight: bold;
+      color: #4a4a4a;
+    }
+
+    .dark input[type="radio"]:checked+label {
+      color: #e2e2e2;
+    }
+
+    .card input[type="radio"]:focus+label::before {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    .card legend {
+      margin-bottom: 0.4rem;
+    }
+
+    .totals {
+      font-size: 1.6rem;
+    }
+
+    .totals+h2 {
+      font-size: 1.9rem;
+      margin-top: 0;
+    }
+
+    .grade {
+      font-size: 1.9rem;
+    }
+
+    button {
+      background-color: #fff;
+      border: 1px solid #4a4a4a;
+      border-radius: 6px;
+      box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+      color: #4a4a4a;
+      cursor: pointer;
+      display: inline-block;
+      font-size: 1rem;
+      font-weight: 500;
+      letter-spacing: 0.04em;
+      line-height: 1.8em;
+      margin: 0.75rem 1.1rem 1.1rem 0;
+      outline: none;
+      padding: 0.5rem 1.4rem;
+      text-transform: uppercase;
+      transition-delay: 0s;
+      transition-duration: 0.25s;
+      transition-property: all;
+      transition-timing-function: ease-out;
+      width: fit-content;
+    }
+
+    .dark button {
+      background: #d9d9d9;
+      color: #121212;
+      box-shadow: none;
+    }
+
+    button:hover {
+      border-color: #dbdbdb;
+      color: #dbdbdb;
+    }
+
+    .dark button:hover {
+      color: #121212;
+    }
+
+    button.copy {
+      margin-left: 0.4rem;
+      font-size: .8rem;
+    }
+
+    button:focus {
+      border-color: rgb(56, 56, 56);
+      box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+      color: rgb(56, 56, 56);
+    }
+
+    div[role="alert"] {
+      color: #4a4a4a;
+      font-size: 1.2rem;
+      margin-bottom: 1.2rem;
+    }
+
+    .dark div[role="alert"] {
+      color: #e2e2e2;
+    }
+
+    .summary {
+      font-size: 1.2rem;
+      border: none;
+      font-family: monospace;
+      background-color: #d5d9de;
+      padding: 0.9rem;
+      border-radius: 6px;
+      line-height: 1.5;
+      width: 100%;
+    }
+
+    .summary h2,
+    .dark .summary h2 {
+      font-size: 1.2rem;
+      font-family: "Inter", sans-serif;
+      margin-top: 35px;
+      margin-bottom: 5px;
+    }
+
+    .summary h2:first-of-type,
+    .dark .summary h2:first-of-type {
+      margin-top: 15px;
+    }
+
+    .dark .summary {
+      background: #6b6a6a;
+      color: #e2e2e2;
+    }
+
+    .summary pre,
+    .dark .summary pre {
+      white-space: pre-wrap;
+      border-radius: 5px;
+      padding: 15px;
+      border: solid 2px rgba(0, 0, 0, .4);
+    }
+
+    .summary pre:empty {
+      display: none;
+    }
+
+    .summary pre:empty+button {
+      display: none;
+    }
+
+    .d-none {
+      display: none;
+    }
+
+    @media (max-width: 450px) {
+      .btn-container {
+        text-align: center;
+      }
+
+      button {
+        width: 100%;
+        margin: 0.5rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+        background-size: 50%;
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+        background-size: 50%;
+      }
+    }
+
+    @media (max-width: 515px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.7rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.7rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 460px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.4rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.4rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 400px) {
+      #app h1 {
+        font-size: 1.6rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 340px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(0.9rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(0.9rem) scale(1.075);
+      }
+    }
+  </style>
+  <script>
+    $(function () {
+      let $select2 = $('.select2').select2({
+        containerCssClass: "wrap"
+      })
+
+      function triggerEvent(name, val) {
+        let selected = $("select[name='" + name + "']")
+        let change = new Event('change', { bubbles: true })
+        selected[0].dispatchEvent(change)
+      }
+
+      $('.select2').on('select2:select', function (e) {
+        triggerEvent(e.target.name, e.target.value)
+      });
+    })
+  </script>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script>
+      (function (window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Group Project 1 Rubric (Coding)`;
+          this.categories = [
+            {
+              title: "Technical Acceptance Criteria",
+              subtitle: null,
+              questions: [
+                {
+                  title: "Application uses at least 2 server-side APIs",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application doesn't use at least 2 server-side APIs",
+                  status: false
+                },
+                {
+                  title: "Application uses client-side storage to store persistent data",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application doesn't use client-side storage to store persistent data",
+                  status: false
+                },
+                {
+                  title:
+                    "Application doesn't use JS alerts, prompts, or confirms (uses modals instead)",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application should use modals instead JS alerts, prompts, and confirms",
+                  status: false
+                },
+                {
+                  title: "Application uses CSS framework other than Bootstrap",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application should use CSS framework other than Bootstrap",
+                  status: false
+                },
+                {
+                  title:
+                    "Application is interactive (ie accepts and responds to user input)",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application should be interactive (ie should accept and respond to user input)",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Concept",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Group presented unique and novel project idea",
+                  score: 5,
+                  type: "true false",
+                  message: "The project concept could have been more novel",
+                  status: false
+                },
+                {
+                  title: "Group clearly and concisely articulated project idea",
+                  score: 5,
+                  type: "true false",
+                  message: "Group could have more clearly and concisely articulated project idea",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Deployment",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Group demoed application using deployed live application URL",
+                  score: 10,
+                  type: "true false",
+                  message: "Group did not demo project using live application URL",
+                  status: false
+                },
+                {
+                  title: "Student submitted application GitHub URL",
+                  score: 10,
+                  type: "true false",
+                  message: "Missing GitHub submission URL",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Repository Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Repository has a unique name",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't have a unique name",
+                  status: false
+                },
+                {
+                  title: "Repository follows best practices for file structure and naming conventions",
+                  score: 2,
+                  type: "true false",
+                  message: "Code doesn't follow recommended file/folder structure",
+                  status: false
+                },
+                {
+                  title: "Repository follows best practices for variable and function naming conventions, indentation, quality comments, etc.",
+                  score: 2,
+                  type: "true false",
+                  message: "Code doesn't follow good practices for variable names, indentation, lacks quality comments",
+                  status: false
+                },
+                {
+                  title: "Repository contains multiple descriptive commit messages",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't contain multiple descriptive commit messages",
+                  status: false
+                },
+                {
+                  title: "Repository contains quality README with description, screenshot, link to deployed application",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Application Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Application user experience is intuitive and easy to navigate",
+                  score: 5,
+                  type: "true false",
+                  message: "Application user experience could be more intuitive",
+                  status: false
+                },
+                {
+                  title: "Application user interface style is clean and polished",
+                  score: 5,
+                  type: "true false",
+                  message: "Application user interface needs work",
+                  status: false
+                },
+                {
+                  title: "Application is responsive",
+                  score: 5,
+                  type: "true false",
+                  message: "Application is not responsive",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Presentation",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Group presented using a slide deck",
+                  score: 3,
+                  type: "true false",
+                  message: "Group did not present using a slide deck",
+                  status: false
+                },
+                {
+                  title: "Every group member spoke during presentation",
+                  score: 4,
+                  type: "true false",
+                  message: "Not every group member spoke during the presentation",
+                  status: false
+                },
+                {
+                  title: "Presentation followed Project Presentation Template",
+                  score: 3,
+                  type: "true false",
+                  message: "Presentation didn't follow Project Presentation Template",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Collaboration",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "GitHub contributions",
+                  score: 10,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "There are major disparities in the number of GitHub contributions between group members",
+                      score: 0
+                    },
+                    {
+                      message: "There are some disparities in the number of GitHub contributions between group members",
+                      score: 5
+                    },
+                    {
+                      message: "There are no major disparities in the number of GitHub contributions between group members",
+                      score: 10
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Academic Integrity",
+              subtitle: "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+              score: null,
+              questions: [
+                {
+                  title: "Application contains a low number of commits",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title: "Application codebase appears advanced for the student, at this point in the course, or in general",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title: "Application package.json file references a different author and/or repository",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title: "Application package.json file references much older versions of npm packages",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                }
+              ]
+            }
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+            .forEach(category => {
+              category.score = 0;
+              category.questions.forEach(question => {
+                question.category = category.title;
+                question.status = false;
+                question.id = this.questions.length;
+                question.flagged = category.title === "Academic Integrity";
+                question.bonus = category.title === "Bonus";
+                if (question.type === "likert") {
+                  question.message = question.options[0].message.split(" - ").pop();
+                };
+
+                let messageObj = {
+                  message: question.message,
+                  category: question.category
+                };
+
+                question.message && !question.bonus && this.messages.add(messageObj);
+                this.questions.push(question);
+                category.score += question.score;
+              });
+            });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+            if (question.type === "true false") {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector("select[name='" + question.id + "']").value;
+              categoryTotal += parseInt(score);
+            }
+          })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + "-summary";
+              return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + button;
+              return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+              `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+              `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+            }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .map(category => {
+                let score = category.score;
+                let title = category.title;
+                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                  .filter(messageObj => messageObj.category === category.title)
+                  .map(messageObj => `* ${messageObj.message}\n`)
+                  .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+              })
+              .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
+            }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                .filter(messageObj => messageObj.category === category.title)
+                .map(messageObj => `* ${messageObj.message}\n`)
+                .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+
+            this.model.questions
+              .filter(question => question.category !== "Academic Integrity")
+              .forEach(question => {
+                const id = question.id;
+                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .forEach(category => {
+                const title = category.title
+                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+  </script>
+</body>
+
+</html>

+ 1288 - 0
rubrics/mod09.html

@@ -0,0 +1,1288 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+  <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+  <title>Module 9 Node.js Challenge Rubric</title>
+  <style>
+    *,
+    *::before,
+    *::after {
+      box-sizing: border-box;
+    }
+
+    html,
+    body {
+      margin: 0;
+      padding: 0;
+      line-height: 1.15;
+      font-size: 100%;
+      color: #fefefe;
+      transition: all 0.4s;
+    }
+
+    body {
+      background-color: #fbfcfd;
+      font-family: "Inter", sans-serif;
+      color: #4a4a4a;
+    }
+
+    #app.dark {
+      background-color: #121212;
+      color: #e2e2e2;
+    }
+
+    h1 {
+      font-size: 1.9rem;
+    }
+
+    h2 {
+      font-size: 1.6rem;
+    }
+
+    h3 {
+      font-size: 1.2rem;
+    }
+
+    h4 {
+      font-size: 0.9rem;
+    }
+
+    p,
+    legend {
+      font-size: 1.2rem;
+    }
+
+    label {
+      font-size: 0.9rem;
+      line-height: 2;
+    }
+
+    fieldset {
+      border: none;
+      padding: 0;
+      margin-top: 0.9rem;
+    }
+
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute !important;
+      height: 1px;
+      width: 1px;
+      overflow: hidden;
+      clip: rect(1px 1px 1px 1px);
+      clip: rect(1px, 1px, 1px, 1px);
+      white-space: nowrap;
+    }
+
+    .select-lg,
+    .select-lg option {
+      font-family: "Inter", sans-serif;
+      padding: 10px;
+    }
+
+    .narrow {
+      width: 800px;
+    }
+
+    #app {
+      padding: 3%;
+      transition: all 0.4s;
+    }
+
+    #app h1 {
+      font-size: 1.9rem;
+      font-weight: 700;
+      line-height: 1.3;
+    }
+
+    header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .switch-group {
+      display: flex;
+      align-items: center;
+      height: 2.25rem;
+      width: 4.5rem;
+    }
+
+    input[type="checkbox"]+.toggle {
+      height: 100%;
+      width: 100%;
+      background-color: #77a1db;
+      border-radius: 1.9rem;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      padding: 0.35rem;
+      cursor: pointer;
+      overflow: hidden;
+      background-position: 25% 125%;
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]:focus+.toggle {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    input[type="checkbox"]:checked+.toggle {
+      background-color: rgb(85, 89, 204);
+      background-size: 1rem;
+      background-repeat: no-repeat;
+      background-position: 25%;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]+.toggle::after {
+      content: "";
+      display: block;
+      height: 1.7rem;
+      width: 1.7rem;
+      border-radius: 50%;
+      cursor: pointer;
+      border: 4px solid #e1be64;
+      background-color: #fddf74;
+      transform: translateX(0rem);
+      transition: all 0.5s;
+      background-position: center bottom;
+    }
+
+    input[type="checkbox"]:hover+.toggle::after {
+      transform: translateX(0rem) scale(1.1);
+    }
+
+    input[type="checkbox"]:checked+.toggle::after {
+      border: 4px solid #ffffff;
+      background-color: rgba(255, 255, 255, 0.4);
+      transform: translateX(2.1rem);
+      transition: all 0.5s;
+      background-position: center;
+      background-size: 60%;
+      background-repeat: no-repeat;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+    }
+
+    input[type="checkbox"]:checked:hover+.toggle::after {
+      transform: translateX(2.1rem) scale(1.075);
+    }
+
+    .card {
+      background-color: #fff;
+      border-radius: 6px;
+      border-width: 0;
+      box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+      font-size: 1.1rem;
+      height: 100%;
+      line-height: 1.8rem;
+      padding: 1.5rem 2rem;
+      width: 100%;
+      margin-bottom: 1.9rem;
+    }
+
+    .dark .card {
+      background-color: #333333;
+      box-shadow: none;
+    }
+
+    .card h1,
+    .card h2,
+    .card h3,
+    .card h4,
+    .card h5,
+    .card h6 {
+      color: #4a4a4a;
+    }
+
+    .dark .card h1,
+    .dark .card h2,
+    .dark .card h3,
+    .dark .card h4,
+    .dark .card h5,
+    .dark .card h6 {
+      color: #e2e2e2;
+    }
+
+    .card-header h2 {
+      display: inline-block;
+    }
+
+    .card .cat-score {
+      margin-left: 20px;
+      font-size: smaller;
+    }
+
+    .card h3 {
+      font-size: 1.2rem;
+      font-weight: 700;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card p,
+    .card label,
+    .card legend {
+      color: #7a869c;
+      border-width: 0;
+      display: block;
+      font-size: 1.1rem;
+      font-weight: 300;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card legend {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+
+    .card legend .score {
+      font-size: smaller;
+      margin-left: 20px;
+      font-weight: 100;
+      min-width: 5%;
+    }
+
+    .dark .card p,
+    .dark .card label,
+    .dark .card legend {
+      color: #e2e2e2;
+    }
+
+    .card label {
+      display: inline-block;
+    }
+
+    .card input[type="radio"]+label::before {
+      content: "";
+      height: 0.9rem;
+      width: 0.9rem;
+      margin-right: 0.7rem;
+      border-radius: 50%;
+      border: 1px solid rgb(74, 74, 74);
+      background-color: #ffffff;
+      display: inline-block;
+      cursor: pointer;
+      position: relative;
+      top: 0.09rem;
+    }
+
+    .dark .card input[type="radio"]+label::before {
+      border: 1px solid #e2e2e2;
+      background-color: inherit;
+    }
+
+    .card input[type="radio"]:checked+label::before {
+      background-color: rgb(47, 106, 189);
+      border-color: rgb(47, 106, 189);
+    }
+
+    .dark .card input[type="radio"]:checked+label::before {
+      background-color: #d9d9d9;
+      border-color: #d9d9d9;
+    }
+
+    .card input[type="radio"]:checked+label {
+      font-weight: bold;
+      color: #4a4a4a;
+    }
+
+    .dark input[type="radio"]:checked+label {
+      color: #e2e2e2;
+    }
+
+    .card input[type="radio"]:focus+label::before {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    .card legend {
+      margin-bottom: 0.4rem;
+    }
+
+    .totals {
+      font-size: 1.6rem;
+    }
+
+    .totals+h2 {
+      font-size: 1.9rem;
+      margin-top: 0;
+    }
+
+    .grade {
+      font-size: 1.9rem;
+    }
+
+    button {
+      background-color: #fff;
+      border: 1px solid #4a4a4a;
+      border-radius: 6px;
+      box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+      color: #4a4a4a;
+      cursor: pointer;
+      display: inline-block;
+      font-size: 1rem;
+      font-weight: 500;
+      letter-spacing: 0.04em;
+      line-height: 1.8em;
+      margin: 0.75rem 1.1rem 1.1rem 0;
+      outline: none;
+      padding: 0.5rem 1.4rem;
+      text-transform: uppercase;
+      transition-delay: 0s;
+      transition-duration: 0.25s;
+      transition-property: all;
+      transition-timing-function: ease-out;
+      width: fit-content;
+    }
+
+    .dark button {
+      background: #d9d9d9;
+      color: #121212;
+      box-shadow: none;
+    }
+
+    button:hover {
+      border-color: #dbdbdb;
+      color: #dbdbdb;
+    }
+
+    .dark button:hover {
+      color: #121212;
+    }
+
+    button.copy {
+      margin-left: 0.4rem;
+      font-size: .8rem;
+    }
+
+    button:focus {
+      border-color: rgb(56, 56, 56);
+      box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+      color: rgb(56, 56, 56);
+    }
+
+    div[role="alert"] {
+      color: #4a4a4a;
+      font-size: 1.2rem;
+      margin-bottom: 1.2rem;
+    }
+
+    .dark div[role="alert"] {
+      color: #e2e2e2;
+    }
+
+    .summary {
+      font-size: 1.2rem;
+      border: none;
+      font-family: monospace;
+      background-color: #d5d9de;
+      padding: 0.9rem;
+      border-radius: 6px;
+      line-height: 1.5;
+      width: 100%;
+    }
+
+    .summary h2,
+    .dark .summary h2 {
+      font-size: 1.2rem;
+      font-family: "Inter", sans-serif;
+      margin-top: 35px;
+      margin-bottom: 5px;
+    }
+
+    .summary h2:first-of-type,
+    .dark .summary h2:first-of-type {
+      margin-top: 15px;
+    }
+
+    .dark .summary {
+      background: #6b6a6a;
+      color: #e2e2e2;
+    }
+
+    .summary pre,
+    .dark .summary pre {
+      white-space: pre-wrap;
+      border-radius: 5px;
+      padding: 15px;
+      border: solid 2px rgba(0, 0, 0, .4);
+    }
+
+    .summary pre:empty {
+      display: none;
+    }
+
+    .summary pre:empty+button {
+      display: none;
+    }
+
+    .d-none {
+      display: none;
+    }
+
+    @media (max-width: 450px) {
+      .btn-container {
+        text-align: center;
+      }
+
+      button {
+        width: 100%;
+        margin: 0.5rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+        background-size: 50%;
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+        background-size: 50%;
+      }
+    }
+
+    @media (max-width: 515px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.7rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.7rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 460px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.4rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.4rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 400px) {
+      #app h1 {
+        font-size: 1.6rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 340px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(0.9rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(0.9rem) scale(1.075);
+      }
+    }
+  </style>
+  <script>
+    $(function () {
+      let $select2 = $('.select2').select2({
+        containerCssClass: "wrap"
+      })
+
+      function triggerEvent(name, val) {
+        let selected = $("select[name='" + name + "']")
+        let change = new Event('change', { bubbles: true })
+        selected[0].dispatchEvent(change)
+      }
+
+      $('.select2').on('select2:select', function (e) {
+        triggerEvent(e.target.name, e.target.value)
+      });
+    })
+  </script>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script>
+      (function (window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Module 9 Node.js Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: "Deliverables",
+              subtitle: null,
+              questions: [
+                {
+                  title: "Student submitted a sample README generated from the application",
+                  score: 10,
+                  type: "true false",
+                  message: "Student did not submit a sample README generated from the application",
+                  status: false
+                },
+                {
+                  title: "Student submitted GitHub Repo containing application code",
+                  score: 10,
+                  type: "true false",
+                  message: "GitHub repo is empty",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Walkthrough Video",
+              subtitle: null,
+              questions: [
+                {
+                  title:
+                    "Student includes link to walkthrough video in README",
+                  score: 10,
+                  type: "true false",
+                  message:
+                    "Student does not include link to walkthrough video in README",
+                  status: false
+                },
+                {
+                  title:
+                    "Walkthrough video demonstrates how to invoke the application",
+                  score: 2,
+                  type: "true false",
+                  message: "Walkthrough video does not demonstrate how to invoke the application",
+                  status: false
+                },
+                {
+                  title: "Walkthrough video demonstrates all of the prompts receiving user input",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Walkthrough video doesn't demonstrate any of the prompts receiving user input",
+                      score: 0
+                    },
+                    {
+                      message: "Walkthrough video demonstrates most of the prompts receiving user input",
+                      score: 2
+                    },
+                    {
+                      message: "Walkthrough video demonstrates all of the prompts receiving user input",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title:
+                    "Walkthrough video demonstrates a generated README that matches the user input and has a functioning table of contents",
+                  score: 10,
+                  type: "true false",
+                  message: "Walkthrough video does not demonstrate a generated README that matches the user input and has a functioning table of contents",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Technical Acceptance Criteria",
+              subtitle:
+                "Watch out for errors in the console from this point forward.",
+              questions: [
+                {
+                  title:
+                    "Application generates a README.md file from user input",
+                  score: 8,
+                  type: "true false",
+                  message:
+                    "Application doesn't generate a README.md file from user input",
+                  status: false
+                },
+                {
+                  title:
+                    "Generated README includes Table of Contents that links to corresponding sections",
+                  score: 8,
+                  type: "true false",
+                  message:
+                    "Generated README doesn't include Table of Contents that links to corresponding sections",
+                  status: false
+                },
+                {
+                  title: "Generated README includes title, description, installation, usage, contributing, and tests sections",
+                  score: 8,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Generated README doesn't include title, description, installation, usage, contributing, and tests sections",
+                      score: 0
+                    },
+                    {
+                      message: "Generated README is missing 5 of the following 6 items or sections: title, description, installation, usage, contributing, or tests",
+                      score: 1
+                    },
+                    {
+                      message: "Generated README is missing 4 of the following 6 items or sections: title, description, installation, usage, contributing, or tests",
+                      score: 2
+                    },
+                    {
+                      message: "Generated README is missing 3 of the following 6 items or sections: title, description, installation, usage, contributing, or tests",
+                      score: 4
+                    },
+                    {
+                      message: "Generated README is missing 2 of the following 6 items or sections: title, description, installation, usage, contributing, or tests",
+                      score: 5
+                    },
+                    {
+                      message: "Generated README is missing 1 of the following 6 items or sections: title, description, installation, usage, contributing, or tests",
+                      score: 6
+                    },
+                    {
+                      message: "Generated README includes title, description, installation, usage, contributing, and tests sections",
+                      score: 8
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Generated README includes a badge and license",
+                  score: 8,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Generated README doesn't include a badge and license",
+                      score: 0
+                    },
+                    {
+                      message: "Generated README is missing either a badge or a license",
+                      score: 4
+                    },
+                    {
+                      message: "Generated README includes a badge and license",
+                      score: 8
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title: "Generated README includes a questions section with the user's email and link to GitHub profile",
+                  score: 8,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Generated README doesn't include a questions section with the user's email and link to GitHub profile",
+                      score: 0
+                    },
+                    {
+                      message: "Generated README includes a questions section but is missing either the user's email or a link to the GitHub profile",
+                      score: 4
+                    },
+                    {
+                      message: "Generated README includes a questions section with the user's email and link to GitHub profile",
+                      score: 8
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Repository Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Repository has a unique name",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't have a unique name",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for file structure and naming conventions",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository contains multiple descriptive commit messages",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false
+                },
+                {
+                  title: "Repository contains quality README with description and a link to walkthrough video",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Repo doesn't contain a quality README with description and a link to walkthrough video",
+                      score: 0
+                    },
+                    {
+                      message: "Repository README is missing either the  description or the link to the walkthrough video",
+                      score: 2
+                    },
+                    {
+                      message: "Repository contains quality README with description and a link to walkthrough video",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Academic Integrity",
+              subtitle:
+                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+              score: 0,
+              questions: [
+                {
+                  title: "Application contains a low number of commits",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application codebase appears advanced for the student, at this point in the course, or in general",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references a different author and/or repository",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references much older versions of npm packages",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                }
+              ]
+            }
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+            .forEach(category => {
+              category.score = 0;
+              category.questions.forEach(question => {
+                question.category = category.title;
+                question.status = false;
+                question.id = this.questions.length;
+                question.flagged = category.title === "Academic Integrity";
+                question.bonus = category.title === "Bonus";
+                if (question.type === "likert") {
+                  question.message = question.options[0].message.split(" - ").pop();
+                };
+
+                let messageObj = {
+                  message: question.message,
+                  category: question.category
+                };
+
+                question.message && !question.bonus && this.messages.add(messageObj);
+                this.questions.push(question);
+                category.score += question.score;
+              });
+            });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+            if (question.type === "true false") {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector("select[name='" + question.id + "']").value;
+              categoryTotal += parseInt(score);
+            }
+          })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + "-summary";
+              return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + button;
+              return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+              `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+              `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+            }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .map(category => {
+                let score = category.score;
+                let title = category.title;
+                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                  .filter(messageObj => messageObj.category === category.title)
+                  .map(messageObj => `* ${messageObj.message}\n`)
+                  .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+              })
+              .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
+            }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                .filter(messageObj => messageObj.category === category.title)
+                .map(messageObj => `* ${messageObj.message}\n`)
+                .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+
+            this.model.questions
+              .filter(question => question.category !== "Academic Integrity")
+              .forEach(question => {
+                const id = question.id;
+                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .forEach(category => {
+                const title = category.title
+                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+  </script>
+</body>
+
+</html>

+ 1286 - 0
rubrics/mod10.html

@@ -0,0 +1,1286 @@
+
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+    <title>Challenge 10 OOP Challenge Rubric</title>
+    <style>
+      *,
+      *::before,
+      *::after {
+        box-sizing: border-box;
+      }
+
+      html,
+      body {
+        margin: 0;
+        padding: 0;
+        line-height: 1.15;
+        font-size: 100%;
+        color: #fefefe;
+        transition: all 0.4s;
+      }
+
+      body {
+        background-color: #fbfcfd;
+        font-family: "Inter", sans-serif;
+        color: #4a4a4a;
+      }
+
+      #app.dark {
+        background-color: #121212;
+        color: #e2e2e2;
+      }
+
+      h1 {
+        font-size: 1.9rem;
+      }
+
+      h2 {
+        font-size: 1.6rem;
+      }
+
+      h3 {
+        font-size: 1.2rem;
+      }
+
+      h4 {
+        font-size: 0.9rem;
+      }
+
+      p,
+      legend {
+        font-size: 1.2rem;
+      }
+
+      h3.solution {
+        margin-top: 20px!important;
+      }
+
+      a {
+        color: gray;
+        text-decoration: none;
+      }
+
+      a:hover {
+        transition: all .2s ease-in-out;
+        color: white;
+        text-decoration: underline;
+      }
+
+      label {
+        font-size: 0.9rem;
+        line-height: 2;
+      }
+
+      fieldset {
+        border: none;
+        padding: 0;
+        margin-top: 0.9rem;
+      }
+
+      input[type="radio"],
+      input[type="checkbox"] {
+        position: absolute !important;
+        height: 1px;
+        width: 1px;
+        overflow: hidden;
+        clip: rect(1px 1px 1px 1px);
+        clip: rect(1px, 1px, 1px, 1px);
+        white-space: nowrap;
+      }
+
+      .select-lg,
+      .select-lg option {
+        font-family: "Inter", sans-serif;
+        padding: 10px;
+      }
+
+      .narrow {
+        width: 800px;
+      }
+
+      #app {
+        padding: 3%;
+        transition: all 0.4s;
+      }
+
+      #app h1 {
+        font-size: 1.9rem;
+        font-weight: 700;
+        line-height: 1.3;
+      }
+
+      header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+      }
+
+      .switch-group {
+        display: flex;
+        align-items: center;
+        height: 2.25rem;
+        width: 4.5rem;
+      }
+
+      input[type="checkbox"] + .toggle {
+        height: 100%;
+        width: 100%;
+        background-color: #77a1db;
+        border-radius: 1.9rem;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        padding: 0.35rem;
+        cursor: pointer;
+        overflow: hidden;
+        background-position: 25% 125%;
+        transition: all 0.5s;
+      }
+
+      input[type="checkbox"]:focus + .toggle {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      input[type="checkbox"]:checked + .toggle {
+        background-color: rgb(85, 89, 204);
+        background-size: 1rem;
+        background-repeat: no-repeat;
+        background-position: 25%;
+        background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+        transition: all 0.5s;
+      }
+
+      input[type="checkbox"] + .toggle::after {
+        content: "";
+        display: block;
+        height: 1.7rem;
+        width: 1.7rem;
+        border-radius: 50%;
+        cursor: pointer;
+        border: 4px solid #e1be64;
+        background-color: #fddf74;
+        transform: translateX(0rem);
+        transition: all 0.5s;
+        background-position: center bottom;
+      }
+
+      input[type="checkbox"]:hover + .toggle::after {
+        transform: translateX(0rem) scale(1.1);
+      }
+
+      input[type="checkbox"]:checked + .toggle::after {
+        border: 4px solid #ffffff;
+        background-color: rgba(255, 255, 255, 0.4);
+        transform: translateX(2.1rem);
+        transition: all 0.5s;
+        background-position: center;
+        background-size: 60%;
+        background-repeat: no-repeat;
+        background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+      }
+
+      input[type="checkbox"]:checked:hover + .toggle::after {
+        transform: translateX(2.1rem) scale(1.075);
+      }
+
+      .card {
+        background-color: #fff;
+        border-radius: 6px;
+        border-width: 0;
+        box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+        font-size: 1.1rem;
+        height: 100%;
+        line-height: 1.8rem;
+        padding: 1.5rem 2rem;
+        width: 100%;
+        margin-bottom: 1.9rem;
+      }
+
+      .dark .card {
+        background-color: #333333;
+        box-shadow: none;
+      }
+
+      .card h1,
+      .card h2,
+      .card h3,
+      .card h4,
+      .card h5,
+      .card h6 {
+        color: #4a4a4a;
+      }
+
+      .dark .card h1,
+      .dark .card h2,
+      .dark .card h3,
+      .dark .card h4,
+      .dark .card h5,
+      .dark .card h6 {
+        color: #e2e2e2;
+      }
+
+      .card-header h2 {
+        display: inline-block;
+      }
+
+      .card .cat-score {
+        margin-left: 20px;
+        font-size: smaller;
+      }
+
+      .card h3 {
+        font-size: 1.2rem;
+        font-weight: 700;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card p,
+      .card label,
+      .card legend {
+        color: #7a869c;
+        border-width: 0;
+        display: block;
+        font-size: 1.1rem;
+        font-weight: 300;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card legend {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+      }
+
+      .card legend .score {
+        font-size: smaller;
+        margin-left: 20px;
+        font-weight: 100;
+        min-width: 5%;
+      }
+
+      .dark .card p,
+      .dark .card label,
+      .dark .card legend {
+        color: #e2e2e2;
+      }
+
+      .card label {
+        display: inline-block;
+      }
+
+      .card input[type="radio"] + label::before {
+        content: "";
+        height: 0.9rem;
+        width: 0.9rem;
+        margin-right: 0.7rem;
+        border-radius: 50%;
+        border: 1px solid rgb(74, 74, 74);
+        background-color: #ffffff;
+        display: inline-block;
+        cursor: pointer;
+        position: relative;
+        top: 0.09rem;
+      }
+
+      .dark .card input[type="radio"] + label::before {
+        border: 1px solid #e2e2e2;
+        background-color: inherit;
+      }
+
+      .card input[type="radio"]:checked + label::before {
+        background-color: rgb(47, 106, 189);
+        border-color: rgb(47, 106, 189);
+      }
+
+      .dark .card input[type="radio"]:checked + label::before {
+        background-color: #d9d9d9;
+        border-color: #d9d9d9;
+      }
+
+      .card input[type="radio"]:checked + label {
+        font-weight: bold;
+        color: #4a4a4a;
+      }
+
+      .dark input[type="radio"]:checked + label {
+        color: #e2e2e2;
+      }
+
+      .card input[type="radio"]:focus + label::before {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      .card legend {
+        margin-bottom: 0.4rem;
+      }
+
+      .totals {
+        font-size: 1.6rem;
+      }
+
+      .totals + h2 {
+        font-size: 1.9rem;
+        margin-top: 0;
+      }
+
+      .grade {
+        font-size: 1.9rem;
+      }
+
+      button {
+        background-color: #fff;
+        border: 1px solid #4a4a4a;
+        border-radius: 6px;
+        box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+        color: #4a4a4a;
+        cursor: pointer;
+        display: inline-block;
+        font-size: 1rem;
+        font-weight: 500;
+        letter-spacing: 0.04em;
+        line-height: 1.8em;
+        margin: 0.75rem 1.1rem 1.1rem 0;
+        outline: none;
+        padding: 0.5rem 1.4rem;
+        text-transform: uppercase;
+        transition-delay: 0s;
+        transition-duration: 0.25s;
+        transition-property: all;
+        transition-timing-function: ease-out;
+        width: fit-content;
+      }
+
+      .dark button {
+        background: #d9d9d9;
+        color: #121212;
+        box-shadow: none;
+      }
+
+      button:hover {
+        border-color: #dbdbdb;
+        color: #dbdbdb;
+      }
+
+      .dark button:hover {
+        color: #121212;
+      }
+
+      button.copy {
+        margin-left: 0.4rem;
+        font-size: .8rem;
+      }
+
+      button:focus {
+        border-color: rgb(56, 56, 56);
+        box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+        color: rgb(56, 56, 56);
+      }
+
+      div[role="alert"] {
+        color: #4a4a4a;
+        font-size: 1.2rem;
+        margin-bottom: 1.2rem;
+      }
+
+      .dark div[role="alert"] {
+        color: #e2e2e2;
+      }
+
+      .summary {
+        font-size: 1.2rem;
+        border: none;
+        font-family: monospace;
+        background-color: #d5d9de;
+        padding: 0.9rem;
+        border-radius: 6px;
+        line-height: 1.5;
+        width: 100%;
+      }
+
+      .summary h2,
+      .dark .summary h2 {
+        font-size: 1.2rem;
+        font-family: "Inter", sans-serif;
+        margin-top: 35px;
+        margin-bottom: 5px;
+      }
+
+      .summary h2:first-of-type,
+      .dark .summary h2:first-of-type {
+        margin-top: 15px;
+      }
+
+      .dark .summary {
+        background: #6b6a6a;
+        color: #e2e2e2;
+      }
+
+      .summary pre,
+      .dark .summary pre {
+        white-space: pre-wrap;
+        border-radius: 5px;
+        padding: 15px;
+        border: solid 2px rgba(0,0,0,.4);
+      }
+
+      .summary pre:empty {
+        display: none;
+      }
+
+      .summary pre:empty + button {
+        display: none;
+      }
+
+      .d-none {
+        display: none;
+      }
+
+      @media (max-width: 450px) {
+        .btn-container {
+          text-align: center;
+        }
+
+        button {
+          width: 100%;
+          margin: 0.5rem;
+        }
+
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.3rem);
+          background-size: 50%;
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+          background-size: 50%;
+        }
+      }
+
+      @media (max-width: 515px) {
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.7rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.7rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 460px) {
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.4rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.4rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 400px) {
+        #app h1 {
+          font-size: 1.6rem;
+        }
+
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.3rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 340px) {
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(0.9rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(0.9rem) scale(1.075);
+        }
+      }
+    </style>
+    <script>
+      $(function () {
+        let $select2 = $('.select2').select2({
+          containerCssClass: "wrap"
+        })
+
+        function triggerEvent(name, val) {
+          let selected = $("select[name='" + name + "']")
+          let change = new Event('change', { bubbles: true })
+          selected[0].dispatchEvent(change)
+        }
+
+        $('.select2').on('select2:select', function (e) {
+          triggerEvent(e.target.name, e.target.value)
+        });
+      })
+  </script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script>
+      (function(window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Challenge 10 OOP Challenge Rubric (Coding)`;
+          this.categories = [
+	{
+		title: "Deliverables",
+		subtitle: null,
+		solution: null,
+		previewText: null,
+		score: 0,
+		questions: [
+			{
+				title: "Student submitted a sample SVG logo file generated from the application",
+				score: 8,
+				type: "true false",
+				message: "Student did not submit a sample SVG file generated from the application",
+				status: false
+			},
+			{
+				title: "Student submitted GitHub Repo containing application code",
+				score: 7,
+				type: "true false",
+				message: "GitHub repo is empty",
+				status: false
+			}
+		]
+	},
+	{
+		title: "Walkthrough Video",
+		subtitle: null,
+		solution: null,
+		previewText: null,
+		score: 0,
+		questions: [
+			{
+				title: "Student includes link to walkthrough video in README",
+				score: 8,
+				type: "true false",
+				message: "Student did not include link to walkthrough video in README",
+				status: false
+			},
+			{
+				title: "Walkthrough video shows all tests passing from the command line",
+				score: 10,
+				type: "likert",
+				options: [
+					{
+						message: "Walkthrough video doesn't show all tests passing from the command line",
+						score: 0
+					},
+					{
+						message: "Walkthrough video shows either the shapes OR SVG tests pass",
+						score: 5
+					},
+					{
+						message: "Walkthrough video shows all tests passing from the command line",
+						score: 10
+					}
+				],
+				message: "",
+				status: false
+			},
+			{
+				title: "Walkthrough video demonstrates how to invoke the application",
+				score: 2,
+				type: "true false",
+				message: "Walkthrough video does not demonstrate how to invoke the application",
+				status: false
+			},
+			{
+				title: "Walkthrough video demonstrates all of the prompts receiving user input",
+				score: 4,
+				type: "likert",
+				options: [
+					{
+						message: "Walkthrough video does not demonstrate any of the prompts receiving user input: text for the logo, text color, shape, and shape color",
+						score: 0
+					},
+					{
+						message: "Walkthrough video only demonstrates 1 of 4 of the prompts receiving input: text for the logo, text color, shape, and shape color",
+						score: 1
+					},
+					{
+						message: "Walkthrough video only demonstrates 2 of 4 of the prompts receiving input: text for the logo, text color, shape, and shape color",
+						score: 2
+					},
+					{
+						message: "Walkthrough video only demonstrates 3 of 4 of the prompts receiving input: text for the logo, text color, shape, and shape color",
+						score: 3
+					},
+					{
+						message: "Walkthrough video demonstrates all of the prompts receiving user input: text for the logo, text color, shape, and shape color",
+						score: 4
+					}
+				],
+				message: "",
+				status: false
+			},
+			{
+				title: "Walkthrough video demonstrates a generated SVG file that matches the user input",
+				score: 8,
+				type: "true false",
+				message: "Walkthrough video does not demonstrate a generated SVG file that matches the user input",
+				status: false
+			}
+		]
+	},
+	{
+		title: "Technical Acceptance Criteria",
+		subtitle: "Watch out for errors in the console from this point forward.",
+		solution: null,
+		previewText: null,
+		score: 0,
+		questions: [
+			{
+				title: "Application utilizes 'Triangle', 'Square', and 'Circle' classes to create shape objects as outlined in the Challenge instructions",
+				score: 18,
+				type: "likert",
+				options: [
+					{
+						message: "Application doesn't utilize 'Triangle', 'Square', and 'Circle' classes to create shape objects as outlined in the Challenge instructions",
+						score: 0
+					},
+					{
+						message: "Application only utilizes 1 of the following three classes to create shape objects as outlined in the Challenge instructions: `Triangle`, `Square`, and `Circle`",
+						score: 6
+					},
+					{
+						message: "Application only utilizes 2 of the following three classes to create shape objects as outlined in the Challenge instructions: `Triangle`, `Square`, and `Circle`",
+						score: 12
+					},
+					{
+						message: "Application utilizes 'Triangle', 'Square', and 'Circle' classes to create shape objects as outlined in the Challenge instructions",
+						score: 18
+					}
+				],
+				message: "",
+				status: false
+			},
+			{
+				title: "All application tests pass",
+				score: 12,
+				type: "likert",
+				options: [
+					{
+						message: "None of the application tests pass",
+						score: 0
+					},
+					{
+						message: "Either the shapes OR SVG tests pass",
+						score: 6
+					},
+					{
+						message: "All application tests pass",
+						score: 12
+					}
+				],
+				message: "",
+				status: false
+			},
+			{
+				title: "Application generates an SVG logo displaying the results of the user input",
+				score: 10,
+				type: "true false",
+				message: "Application doesn't generate an SVG logo displaying the results of the user input",
+				status: false
+			}
+		]
+	},
+	{
+		title: "Repository Quality",
+		subtitle: null,
+		solution: null,
+		previewText: null,
+		score: 0,
+		questions: [
+			{
+				title: "Repository has a unique name",
+				score: 2,
+				type: "true false",
+				message: "Repo doesn't have a unique name",
+				status: false
+			},
+			{
+				title: "Repository follows best practices for file structure and naming conventions",
+				score: 2,
+				type: "true false",
+				message: "Code doesn't follow recommended file/folder structure",
+				status: false
+			},
+			{
+				title: "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+				score: 2,
+				type: "true false",
+				message: "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+				status: false
+			},
+			{
+				title: "Repository contains multiple descriptive commit messages",
+				score: 2,
+				type: "true false",
+				message: "Repo doesn't contain multiple descriptive commit messages",
+				status: false
+			},
+			{
+				title: "Repository contains quality README with description and a link to walkthrough video",
+				score: 5,
+				type: "likert",
+				options: [
+					{
+						message: "Repo doesn't contain a quality README with description and a link to walkthrough video",
+						score: 0
+					},
+					{
+						message: "Repository README is missing either the description or the link to the walkthrough video",
+						score: 3
+					},
+					{
+						message: "Repository contains quality README with description and a link to walkthrough video",
+						score: 5
+					}
+				],
+				message: "",
+				status: false
+			}
+		]
+	},
+	{
+		title: "Academic Integrity",
+		subtitle: "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+		solution: null,
+		previewText: null,
+		score: 0,
+		questions: [
+			{
+				title: "Application contains a low number of commits",
+				score: 0,
+				type: "true false",
+				message: null,
+				status: false
+			},
+			{
+				title: "Application codebase appears advanced for the student, at this point in the course, or in general",
+				score: 0,
+				type: "true false",
+				message: null,
+				status: false
+			},
+			{
+				title: "Application package.json file references a different author and/or repository",
+				score: 0,
+				type: "true false",
+				message: null,
+				status: false
+			},
+			{
+				title: "Application package.json file references much older versions of npm packages",
+				score: 0,
+				type: "true false",
+				message: null,
+				status: false
+			}
+		]
+	}
+];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function() {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+          .forEach(category => {
+            category.score = 0;
+            category.questions.forEach(question => {
+              question.category = category.title;
+              question.status = false;
+              question.id = this.questions.length;
+              question.flagged = category.title === "Academic Integrity";
+              question.bonus = category.title === "Bonus";
+              if (question.type === "likert") {
+                question.message = question.options[0].message.split(" - ").pop();
+              };
+
+              let messageObj = {
+                message : question.message,
+                category : question.category
+              };
+
+              question.message && !question.bonus && this.messages.add(messageObj);
+              this.questions.push(question);
+              category.score += question.score;
+            });
+          });
+        };
+
+        Model.prototype.toggleDarkMode = function() {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function(radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+            
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message : question.message,
+                category : category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+          
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+           
+           question.message = newMessage;
+
+           let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+          
+          
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function() {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function() {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function() {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+          
+          this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .map(category => {
+            let summary = category.title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+            let summaryName = summary + "-summary";
+            return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+          
+          this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .map(category => {
+            let button = category.title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+            let buttonName = "copyButton-" + button;
+            return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+          `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+          `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+        }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function({ title, subtitle, solution, previewText, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${solution ? `<h3 class="solution">Solution: <a href=${solution} target="_blank" rel="noopener noreferrer">${previewText !== null ? previewText : solution}</a></h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function() {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .map(category => {
+            let score = category.score;
+            let title = category.title;
+            let catClass = title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+
+            return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+            .filter(messageObj => messageObj.category === category.title)
+            .map(messageObj => `* ${messageObj.message}\n`)
+            .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+            })
+          .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function(title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+                isDarkMode ? "checked" : ""
+              }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function() {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function() {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .forEach(category => {
+            let catClass = category.title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+
+            this[catClass + "-summary"].innerHTML = [...this.model.messages]
+            .filter(messageObj => messageObj.category === category.title)
+            .map(messageObj => `* ${messageObj.message}\n`)
+            .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function() {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+            
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+            
+            this.model.questions
+            .filter(question => question.category !== "Academic Integrity")
+            .forEach(question => {
+              const id = question.id;
+              const questionScore = document.querySelector("span[data-id='" + id + "']");
+              questionScore.textContent = 0;
+            });
+            
+            this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              const title = category.title
+              const categoryScore = document.querySelector("span[data-cat='" + title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+              const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+              categoryScore.textContent = 0;
+              resultsCategoryScore.textContent = 0;
+            });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+              
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+    </script>
+  </body>
+</html>

+ 1237 - 0
rubrics/mod11.html

@@ -0,0 +1,1237 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+  <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+  <title>Module 11 Express.js Challenge Rubric</title>
+  <style>
+    *,
+    *::before,
+    *::after {
+      box-sizing: border-box;
+    }
+
+    html,
+    body {
+      margin: 0;
+      padding: 0;
+      line-height: 1.15;
+      font-size: 100%;
+      color: #fefefe;
+      transition: all 0.4s;
+    }
+
+    body {
+      background-color: #fbfcfd;
+      font-family: "Inter", sans-serif;
+      color: #4a4a4a;
+    }
+
+    #app.dark {
+      background-color: #121212;
+      color: #e2e2e2;
+    }
+
+    h1 {
+      font-size: 1.9rem;
+    }
+
+    h2 {
+      font-size: 1.6rem;
+    }
+
+    h3 {
+      font-size: 1.2rem;
+    }
+
+    h4 {
+      font-size: 0.9rem;
+    }
+
+    p,
+    legend {
+      font-size: 1.2rem;
+    }
+
+    label {
+      font-size: 0.9rem;
+      line-height: 2;
+    }
+
+    fieldset {
+      border: none;
+      padding: 0;
+      margin-top: 0.9rem;
+    }
+
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute !important;
+      height: 1px;
+      width: 1px;
+      overflow: hidden;
+      clip: rect(1px 1px 1px 1px);
+      clip: rect(1px, 1px, 1px, 1px);
+      white-space: nowrap;
+    }
+
+    .select-lg,
+    .select-lg option {
+      font-family: "Inter", sans-serif;
+      padding: 10px;
+    }
+
+    .narrow {
+      width: 800px;
+    }
+
+    #app {
+      padding: 3%;
+      transition: all 0.4s;
+    }
+
+    #app h1 {
+      font-size: 1.9rem;
+      font-weight: 700;
+      line-height: 1.3;
+    }
+
+    header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .switch-group {
+      display: flex;
+      align-items: center;
+      height: 2.25rem;
+      width: 4.5rem;
+    }
+
+    input[type="checkbox"]+.toggle {
+      height: 100%;
+      width: 100%;
+      background-color: #77a1db;
+      border-radius: 1.9rem;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      padding: 0.35rem;
+      cursor: pointer;
+      overflow: hidden;
+      background-position: 25% 125%;
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]:focus+.toggle {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    input[type="checkbox"]:checked+.toggle {
+      background-color: rgb(85, 89, 204);
+      background-size: 1rem;
+      background-repeat: no-repeat;
+      background-position: 25%;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]+.toggle::after {
+      content: "";
+      display: block;
+      height: 1.7rem;
+      width: 1.7rem;
+      border-radius: 50%;
+      cursor: pointer;
+      border: 4px solid #e1be64;
+      background-color: #fddf74;
+      transform: translateX(0rem);
+      transition: all 0.5s;
+      background-position: center bottom;
+    }
+
+    input[type="checkbox"]:hover+.toggle::after {
+      transform: translateX(0rem) scale(1.1);
+    }
+
+    input[type="checkbox"]:checked+.toggle::after {
+      border: 4px solid #ffffff;
+      background-color: rgba(255, 255, 255, 0.4);
+      transform: translateX(2.1rem);
+      transition: all 0.5s;
+      background-position: center;
+      background-size: 60%;
+      background-repeat: no-repeat;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+    }
+
+    input[type="checkbox"]:checked:hover+.toggle::after {
+      transform: translateX(2.1rem) scale(1.075);
+    }
+
+    .card {
+      background-color: #fff;
+      border-radius: 6px;
+      border-width: 0;
+      box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+      font-size: 1.1rem;
+      height: 100%;
+      line-height: 1.8rem;
+      padding: 1.5rem 2rem;
+      width: 100%;
+      margin-bottom: 1.9rem;
+    }
+
+    .dark .card {
+      background-color: #333333;
+      box-shadow: none;
+    }
+
+    .card h1,
+    .card h2,
+    .card h3,
+    .card h4,
+    .card h5,
+    .card h6 {
+      color: #4a4a4a;
+    }
+
+    .dark .card h1,
+    .dark .card h2,
+    .dark .card h3,
+    .dark .card h4,
+    .dark .card h5,
+    .dark .card h6 {
+      color: #e2e2e2;
+    }
+
+    .card-header h2 {
+      display: inline-block;
+    }
+
+    .card .cat-score {
+      margin-left: 20px;
+      font-size: smaller;
+    }
+
+    .card h3 {
+      font-size: 1.2rem;
+      font-weight: 700;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card p,
+    .card label,
+    .card legend {
+      color: #7a869c;
+      border-width: 0;
+      display: block;
+      font-size: 1.1rem;
+      font-weight: 300;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card legend {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+
+    .card legend .score {
+      font-size: smaller;
+      margin-left: 20px;
+      font-weight: 100;
+      min-width: 5%;
+    }
+
+    .dark .card p,
+    .dark .card label,
+    .dark .card legend {
+      color: #e2e2e2;
+    }
+
+    .card label {
+      display: inline-block;
+    }
+
+    .card input[type="radio"]+label::before {
+      content: "";
+      height: 0.9rem;
+      width: 0.9rem;
+      margin-right: 0.7rem;
+      border-radius: 50%;
+      border: 1px solid rgb(74, 74, 74);
+      background-color: #ffffff;
+      display: inline-block;
+      cursor: pointer;
+      position: relative;
+      top: 0.09rem;
+    }
+
+    .dark .card input[type="radio"]+label::before {
+      border: 1px solid #e2e2e2;
+      background-color: inherit;
+    }
+
+    .card input[type="radio"]:checked+label::before {
+      background-color: rgb(47, 106, 189);
+      border-color: rgb(47, 106, 189);
+    }
+
+    .dark .card input[type="radio"]:checked+label::before {
+      background-color: #d9d9d9;
+      border-color: #d9d9d9;
+    }
+
+    .card input[type="radio"]:checked+label {
+      font-weight: bold;
+      color: #4a4a4a;
+    }
+
+    .dark input[type="radio"]:checked+label {
+      color: #e2e2e2;
+    }
+
+    .card input[type="radio"]:focus+label::before {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    .card legend {
+      margin-bottom: 0.4rem;
+    }
+
+    .totals {
+      font-size: 1.6rem;
+    }
+
+    .totals+h2 {
+      font-size: 1.9rem;
+      margin-top: 0;
+    }
+
+    .grade {
+      font-size: 1.9rem;
+    }
+
+    button {
+      background-color: #fff;
+      border: 1px solid #4a4a4a;
+      border-radius: 6px;
+      box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+      color: #4a4a4a;
+      cursor: pointer;
+      display: inline-block;
+      font-size: 1rem;
+      font-weight: 500;
+      letter-spacing: 0.04em;
+      line-height: 1.8em;
+      margin: 0.75rem 1.1rem 1.1rem 0;
+      outline: none;
+      padding: 0.5rem 1.4rem;
+      text-transform: uppercase;
+      transition-delay: 0s;
+      transition-duration: 0.25s;
+      transition-property: all;
+      transition-timing-function: ease-out;
+      width: fit-content;
+    }
+
+    .dark button {
+      background: #d9d9d9;
+      color: #121212;
+      box-shadow: none;
+    }
+
+    button:hover {
+      border-color: #dbdbdb;
+      color: #dbdbdb;
+    }
+
+    .dark button:hover {
+      color: #121212;
+    }
+
+    button.copy {
+      margin-left: 0.4rem;
+      font-size: .8rem;
+    }
+
+    button:focus {
+      border-color: rgb(56, 56, 56);
+      box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+      color: rgb(56, 56, 56);
+    }
+
+    div[role="alert"] {
+      color: #4a4a4a;
+      font-size: 1.2rem;
+      margin-bottom: 1.2rem;
+    }
+
+    .dark div[role="alert"] {
+      color: #e2e2e2;
+    }
+
+    .summary {
+      font-size: 1.2rem;
+      border: none;
+      font-family: monospace;
+      background-color: #d5d9de;
+      padding: 0.9rem;
+      border-radius: 6px;
+      line-height: 1.5;
+      width: 100%;
+    }
+
+    .summary h2,
+    .dark .summary h2 {
+      font-size: 1.2rem;
+      font-family: "Inter", sans-serif;
+      margin-top: 35px;
+      margin-bottom: 5px;
+    }
+
+    .summary h2:first-of-type,
+    .dark .summary h2:first-of-type {
+      margin-top: 15px;
+    }
+
+    .dark .summary {
+      background: #6b6a6a;
+      color: #e2e2e2;
+    }
+
+    .summary pre,
+    .dark .summary pre {
+      white-space: pre-wrap;
+      border-radius: 5px;
+      padding: 15px;
+      border: solid 2px rgba(0, 0, 0, .4);
+    }
+
+    .summary pre:empty {
+      display: none;
+    }
+
+    .summary pre:empty+button {
+      display: none;
+    }
+
+    .d-none {
+      display: none;
+    }
+
+    @media (max-width: 450px) {
+      .btn-container {
+        text-align: center;
+      }
+
+      button {
+        width: 100%;
+        margin: 0.5rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+        background-size: 50%;
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+        background-size: 50%;
+      }
+    }
+
+    @media (max-width: 515px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.7rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.7rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 460px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.4rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.4rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 400px) {
+      #app h1 {
+        font-size: 1.6rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 340px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(0.9rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(0.9rem) scale(1.075);
+      }
+    }
+  </style>
+  <script>
+    $(function () {
+      let $select2 = $('.select2').select2({
+        containerCssClass: "wrap"
+      })
+
+      function triggerEvent(name, val) {
+        let selected = $("select[name='" + name + "']")
+        let change = new Event('change', { bubbles: true })
+        selected[0].dispatchEvent(change)
+      }
+
+      $('.select2').on('select2:select', function (e) {
+        triggerEvent(e.target.name, e.target.value)
+      });
+    })
+  </script>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script>
+      (function (window) {
+        "use strict";
+
+        function Model() {
+          this.title = "Module 11 Express.js Challenge Rubric (Coding)";
+          this.categories = [
+            {
+              title: "Deployment",
+              subtitle: null,
+              questions: [
+                {
+                  title: "Student submitted deployed live application URL",
+                  score: 9,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title:
+                    "Application loads at deployed URL (no 404, or Heroku/GH Pages error)",
+                  score: 9,
+                  type: "true false",
+                  message: "Application isn't deployed",
+                  status: false
+                },
+                {
+                  title: "Student submitted application GitHub URL",
+                  score: 9,
+                  type: "true false",
+                  message: "Missing GitHub submission URL",
+                  status: false
+                },
+                {
+                  title: "GitHub Repo contains application code",
+                  score: 9,
+                  type: "true false",
+                  message: "GitHub repo is empty",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Technical Acceptance Criteria",
+              subtitle:
+                "Keep your browser console open and watch out for errors from this point forward.",
+              questions: [
+                {
+                  title:
+                    "Application allows user to create and save persistent notes",
+                  score: 10,
+                  type: "true false",
+                  message: "Application doesn't allow user to create and save persistent notes",
+                  status: false
+                },
+                {
+                  title:
+                    "Application allows user to view previously saved notes",
+                  score: 10,
+                  type: "true false",
+                  message:
+                    "Application doesn't allow user to view previously saved notes",
+                  status: false
+                },
+                {
+                  title:
+                    "Application front-end connects to Express.js backend",
+                  score: 10,
+                  type: "true false",
+                  message:
+                    "Application front-end doesn't connect to Express.js backend",
+                  status: false
+                },
+                {
+                  title: "Application backend stores notes with unique IDs in JSON file",
+                  score: 10,
+                  type: "true false",
+                  message: "Application backend doesn't store notes with unique IDs in JSON file",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Repository Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Repository has a unique name",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't have a unique name",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for file structure and naming conventions",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository contains multiple descriptive commit messages",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false
+                },
+                {
+                  title: "Repository contains quality README with description, screenshot, and link to deployed application",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Repo doesn't include a high quality README with screenshots, a description, or link to deployed application",
+                      score: 0
+                    },
+                    {
+                      message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                      score: 2
+                    },
+                    {
+                      message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                      score: 4
+                    },
+                    {
+                      message: "Repository contains quality README with description, screenshot, and link to deployed application",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Application Quality",
+              subtitle: null,
+              questions: [
+                {
+                  title: "Application console is free of errors",
+                  score: 11,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application console contains 4 or more errors",
+                      score: 0
+                    },
+                    {
+                      message: "Application console contains 2-3 errors",
+                      score: 5
+                    },
+                    {
+                      message: "Application console contains 1 error",
+                      score: 9
+                    },
+                    {
+                      message: "Application console is free of errors",
+                      score: 11
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Bonus",
+              subtitle: null,
+              solution: null,
+              previewText: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Application allows user to delete notes",
+                  score: 10,
+                  type: "true false",
+                  message: "Application allows user to delete notes",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Academic Integrity",
+              subtitle:
+                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+              questions: [
+                {
+                  title: "Application contains a low number of commits",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application codebase appears advanced for the student, at this point in the course, or in general",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references a different author and/or repository",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references much older versions of npm packages",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                }
+              ]
+            }
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+            .forEach(category => {
+              category.score = 0;
+              category.questions.forEach(question => {
+                question.category = category.title;
+                question.status = false;
+                question.id = this.questions.length;
+                question.flagged = category.title === "Academic Integrity";
+                question.bonus = category.title === "Bonus";
+                if (question.type === "likert") {
+                  question.message = question.options[0].message.split(" - ").pop();
+                };
+
+                let messageObj = {
+                  message: question.message,
+                  category: question.category
+                };
+
+                question.message && !question.bonus && this.messages.add(messageObj);
+                this.questions.push(question);
+                category.score += question.score;
+              });
+            });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+            if (question.type === "true false") {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector("select[name='" + question.id + "']").value;
+              categoryTotal += parseInt(score);
+            }
+          })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + "-summary";
+              return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + button;
+              return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+              `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+              `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+            }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .map(category => {
+                let score = category.score;
+                let title = category.title;
+                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                  .filter(messageObj => messageObj.category === category.title)
+                  .map(messageObj => `* ${messageObj.message}\n`)
+                  .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+              })
+              .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
+            }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                .filter(messageObj => messageObj.category === category.title)
+                .map(messageObj => `* ${messageObj.message}\n`)
+                .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+
+            this.model.questions
+              .filter(question => question.category !== "Academic Integrity")
+              .forEach(question => {
+                const id = question.id;
+                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .forEach(category => {
+                const title = category.title
+                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+  </script>
+</body>
+
+</html>

+ 1362 - 0
rubrics/mod12.html

@@ -0,0 +1,1362 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+  <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+  <title>Module 12 SQL Challenge Rubric</title>
+  <style>
+    *,
+    *::before,
+    *::after {
+      box-sizing: border-box;
+    }
+
+    html,
+    body {
+      margin: 0;
+      padding: 0;
+      line-height: 1.15;
+      font-size: 100%;
+      color: #fefefe;
+      transition: all 0.4s;
+    }
+
+    body {
+      background-color: #fbfcfd;
+      font-family: "Inter", sans-serif;
+      color: #4a4a4a;
+    }
+
+    #app.dark {
+      background-color: #121212;
+      color: #e2e2e2;
+    }
+
+    h1 {
+      font-size: 1.9rem;
+    }
+
+    h2 {
+      font-size: 1.6rem;
+    }
+
+    h3 {
+      font-size: 1.2rem;
+    }
+
+    h4 {
+      font-size: 0.9rem;
+    }
+
+    p,
+    legend {
+      font-size: 1.2rem;
+    }
+
+    label {
+      font-size: 0.9rem;
+      line-height: 2;
+    }
+
+    fieldset {
+      border: none;
+      padding: 0;
+      margin-top: 0.9rem;
+    }
+
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute !important;
+      height: 1px;
+      width: 1px;
+      overflow: hidden;
+      clip: rect(1px 1px 1px 1px);
+      clip: rect(1px, 1px, 1px, 1px);
+      white-space: nowrap;
+    }
+
+    .select-lg,
+    .select-lg option {
+      font-family: "Inter", sans-serif;
+      padding: 10px;
+    }
+
+    .narrow {
+      width: 800px;
+    }
+
+    #app {
+      padding: 3%;
+      transition: all 0.4s;
+    }
+
+    #app h1 {
+      font-size: 1.9rem;
+      font-weight: 700;
+      line-height: 1.3;
+    }
+
+    header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .switch-group {
+      display: flex;
+      align-items: center;
+      height: 2.25rem;
+      width: 4.5rem;
+    }
+
+    input[type="checkbox"]+.toggle {
+      height: 100%;
+      width: 100%;
+      background-color: #77a1db;
+      border-radius: 1.9rem;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      padding: 0.35rem;
+      cursor: pointer;
+      overflow: hidden;
+      background-position: 25% 125%;
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]:focus+.toggle {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    input[type="checkbox"]:checked+.toggle {
+      background-color: rgb(85, 89, 204);
+      background-size: 1rem;
+      background-repeat: no-repeat;
+      background-position: 25%;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+      transition: all 0.5s;
+    }
+
+    input[type="checkbox"]+.toggle::after {
+      content: "";
+      display: block;
+      height: 1.7rem;
+      width: 1.7rem;
+      border-radius: 50%;
+      cursor: pointer;
+      border: 4px solid #e1be64;
+      background-color: #fddf74;
+      transform: translateX(0rem);
+      transition: all 0.5s;
+      background-position: center bottom;
+    }
+
+    input[type="checkbox"]:hover+.toggle::after {
+      transform: translateX(0rem) scale(1.1);
+    }
+
+    input[type="checkbox"]:checked+.toggle::after {
+      border: 4px solid #ffffff;
+      background-color: rgba(255, 255, 255, 0.4);
+      transform: translateX(2.1rem);
+      transition: all 0.5s;
+      background-position: center;
+      background-size: 60%;
+      background-repeat: no-repeat;
+      background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+    }
+
+    input[type="checkbox"]:checked:hover+.toggle::after {
+      transform: translateX(2.1rem) scale(1.075);
+    }
+
+    .card {
+      background-color: #fff;
+      border-radius: 6px;
+      border-width: 0;
+      box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+      font-size: 1.1rem;
+      height: 100%;
+      line-height: 1.8rem;
+      padding: 1.5rem 2rem;
+      width: 100%;
+      margin-bottom: 1.9rem;
+    }
+
+    .dark .card {
+      background-color: #333333;
+      box-shadow: none;
+    }
+
+    .card h1,
+    .card h2,
+    .card h3,
+    .card h4,
+    .card h5,
+    .card h6 {
+      color: #4a4a4a;
+    }
+
+    .dark .card h1,
+    .dark .card h2,
+    .dark .card h3,
+    .dark .card h4,
+    .dark .card h5,
+    .dark .card h6 {
+      color: #e2e2e2;
+    }
+
+    .card-header h2 {
+      display: inline-block;
+    }
+
+    .card .cat-score {
+      margin-left: 20px;
+      font-size: smaller;
+    }
+
+    .card h3 {
+      font-size: 1.2rem;
+      font-weight: 700;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card p,
+    .card label,
+    .card legend {
+      color: #7a869c;
+      border-width: 0;
+      display: block;
+      font-size: 1.1rem;
+      font-weight: 300;
+      line-height: 1.8em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .card legend {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+
+    .card legend .score {
+      font-size: smaller;
+      margin-left: 20px;
+      font-weight: 100;
+      min-width: 5%;
+    }
+
+    .dark .card p,
+    .dark .card label,
+    .dark .card legend {
+      color: #e2e2e2;
+    }
+
+    .card label {
+      display: inline-block;
+    }
+
+    .card input[type="radio"]+label::before {
+      content: "";
+      height: 0.9rem;
+      width: 0.9rem;
+      margin-right: 0.7rem;
+      border-radius: 50%;
+      border: 1px solid rgb(74, 74, 74);
+      background-color: #ffffff;
+      display: inline-block;
+      cursor: pointer;
+      position: relative;
+      top: 0.09rem;
+    }
+
+    .dark .card input[type="radio"]+label::before {
+      border: 1px solid #e2e2e2;
+      background-color: inherit;
+    }
+
+    .card input[type="radio"]:checked+label::before {
+      background-color: rgb(47, 106, 189);
+      border-color: rgb(47, 106, 189);
+    }
+
+    .dark .card input[type="radio"]:checked+label::before {
+      background-color: #d9d9d9;
+      border-color: #d9d9d9;
+    }
+
+    .card input[type="radio"]:checked+label {
+      font-weight: bold;
+      color: #4a4a4a;
+    }
+
+    .dark input[type="radio"]:checked+label {
+      color: #e2e2e2;
+    }
+
+    .card input[type="radio"]:focus+label::before {
+      outline: rgb(59, 153, 252) auto 5px;
+    }
+
+    .card legend {
+      margin-bottom: 0.4rem;
+    }
+
+    .totals {
+      font-size: 1.6rem;
+    }
+
+    .totals+h2 {
+      font-size: 1.9rem;
+      margin-top: 0;
+    }
+
+    .grade {
+      font-size: 1.9rem;
+    }
+
+    button {
+      background-color: #fff;
+      border: 1px solid #4a4a4a;
+      border-radius: 6px;
+      box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+      color: #4a4a4a;
+      cursor: pointer;
+      display: inline-block;
+      font-size: 1rem;
+      font-weight: 500;
+      letter-spacing: 0.04em;
+      line-height: 1.8em;
+      margin: 0.75rem 1.1rem 1.1rem 0;
+      outline: none;
+      padding: 0.5rem 1.4rem;
+      text-transform: uppercase;
+      transition-delay: 0s;
+      transition-duration: 0.25s;
+      transition-property: all;
+      transition-timing-function: ease-out;
+      width: fit-content;
+    }
+
+    .dark button {
+      background: #d9d9d9;
+      color: #121212;
+      box-shadow: none;
+    }
+
+    button:hover {
+      border-color: #dbdbdb;
+      color: #dbdbdb;
+    }
+
+    .dark button:hover {
+      color: #121212;
+    }
+
+    button.copy {
+      margin-left: 0.4rem;
+      font-size: .8rem;
+    }
+
+    button:focus {
+      border-color: rgb(56, 56, 56);
+      box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+      color: rgb(56, 56, 56);
+    }
+
+    div[role="alert"] {
+      color: #4a4a4a;
+      font-size: 1.2rem;
+      margin-bottom: 1.2rem;
+    }
+
+    .dark div[role="alert"] {
+      color: #e2e2e2;
+    }
+
+    .summary {
+      font-size: 1.2rem;
+      border: none;
+      font-family: monospace;
+      background-color: #d5d9de;
+      padding: 0.9rem;
+      border-radius: 6px;
+      line-height: 1.5;
+      width: 100%;
+    }
+
+    .summary h2,
+    .dark .summary h2 {
+      font-size: 1.2rem;
+      font-family: "Inter", sans-serif;
+      margin-top: 35px;
+      margin-bottom: 5px;
+    }
+
+    .summary h2:first-of-type,
+    .dark .summary h2:first-of-type {
+      margin-top: 15px;
+    }
+
+    .dark .summary {
+      background: #6b6a6a;
+      color: #e2e2e2;
+    }
+
+    .summary pre,
+    .dark .summary pre {
+      white-space: pre-wrap;
+      border-radius: 5px;
+      padding: 15px;
+      border: solid 2px rgba(0, 0, 0, .4);
+    }
+
+    .summary pre:empty {
+      display: none;
+    }
+
+    .summary pre:empty+button {
+      display: none;
+    }
+
+    .d-none {
+      display: none;
+    }
+
+    @media (max-width: 450px) {
+      .btn-container {
+        text-align: center;
+      }
+
+      button {
+        width: 100%;
+        margin: 0.5rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+        background-size: 50%;
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+        background-size: 50%;
+      }
+    }
+
+    @media (max-width: 515px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.7rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.7rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 460px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.4rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.4rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 400px) {
+      #app h1 {
+        font-size: 1.6rem;
+      }
+
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(1.3rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(1.3rem) scale(1.1);
+      }
+    }
+
+    @media (max-width: 340px) {
+      input[type="checkbox"]:checked+.toggle::after {
+        transform: translateX(0.9rem);
+      }
+
+      input[type="checkbox"]:checked:hover+.toggle::after {
+        transform: translateX(0.9rem) scale(1.075);
+      }
+    }
+  </style>
+  <script>
+    $(function () {
+      let $select2 = $('.select2').select2({
+        containerCssClass: "wrap"
+      })
+
+      function triggerEvent(name, val) {
+        let selected = $("select[name='" + name + "']")
+        let change = new Event('change', { bubbles: true })
+        selected[0].dispatchEvent(change)
+      }
+
+      $('.select2').on('select2:select', function (e) {
+        triggerEvent(e.target.name, e.target.value)
+      });
+    })
+  </script>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script>
+      (function (window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Module 12 SQL Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: "Deliverables",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Student submitted GitHub Repo containing application code",
+                  score: 10,
+                  type: "true false",
+                  message: "GitHub repo is empty",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Walkthrough Video",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title:
+                    "Student includes link to walkthrough video in README",
+                  score: 4,
+                  type: "true false",
+                  message:
+                    "Student does not include link to walkthrough video in README",
+                  status: false
+                },
+                {
+                  title: "Walkthrough video shows all of the technical acceptance criteria being met",
+                  score: 16,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Walkthrough video doesn't show any of the 8 technical acceptance criteria listed in the next section being met",
+                      score: 0
+                    },
+                    {
+                      message: "Walkthrough video only shows 1 of the 8 technical acceptance criteria listed in the next section being met",
+                      score: 2
+                    },
+                    {
+                      message: "Walkthrough video only shows 2 of the 8 technical acceptance criteria listed in the next section being met",
+                      score: 4
+                    },
+                    {
+                      message: "Walkthrough video only shows 3 of the 8 technical acceptance criteria listed in the next section being met",
+                      score: 6
+                    },
+                    {
+                      message: "Walkthrough video only shows 4 of the 8 technical acceptance criteria listed in the next section being met",
+                      score: 8
+                    },
+                    {
+                      message: "Walkthrough video only shows 5 of the 8 technical acceptance criteria listed in the next section being met",
+                      score: 10
+                    },
+                    {
+                      message: "Walkthrough video only shows 6 of the 8 technical acceptance criteria listed in the next section being met",
+                      score: 12
+                    },
+                    {
+                      message: "Walkthrough video only shows 7 of the 8 technical acceptance criteria listed in the next section being met",
+                      score: 14
+                    },
+                    {
+                      message: "Walkthrough video shows all of the 8 technical acceptance criteria listed in the next section being met",
+                      score: 16
+                    }
+                  ],
+                  message: "",
+                  status: false
+                },
+                {
+                  title:
+                    "Walkthrough video demonstrates how to invoke the application",
+                  score: 3,
+                  type: "true false",
+                  message: "Walkthrough video does not demonstrate how to invoke the application",
+                  status: false
+                },
+                {
+                  title:
+                    "Walkthrough video demonstrates a functional menu",
+                  score: 4,
+                  type: "true false",
+                  message: "Walkthrough video does not demonstrate a functional menu",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Technical Acceptance Criteria",
+              subtitle:
+                "Watch out for errors in the console from this point forward.",
+              score: 0,
+              questions: [
+                {
+                  title:
+                    "Application follows table schema outlined in Challenge instructions",
+                  score: 5,
+                  type: "true false",
+                  message: "Application doesn't follow table schema outlined in Challenge instructions",
+                  status: false
+                },
+                {
+                  title:
+                    "Application allows users to add departments",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application doesn't allow users to add departments",
+                  status: false
+                },
+                {
+                  title:
+                    "Application allows users to add roles",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application doesn't allow users to add roles",
+                  status: false
+                },
+                {
+                  title:
+                    "Application allows users to add employees",
+                  score: 5,
+                  type: "true false",
+                  message:
+                    "Application doesn't allow users to add employees",
+                  status: false
+                },
+                {
+                  title: "Application allows users to view departments",
+                  score: 5,
+                  type: "true false",
+                  message: "Application doesn't allow users to view departments",
+                  status: false
+                },
+                {
+                  title: "Application allows users to view roles",
+                  score: 5,
+                  type: "true false",
+                  message: "Application doesn't allow users to view roles",
+                  status: false
+                },
+                {
+                  title: "Application allows users to view employees",
+                  score: 5,
+                  type: "true false",
+                  message: "Application doesn't allow users to view employees",
+                  status: false
+                },
+                {
+                  title: "Application allows users to update employee roles",
+                  score: 5,
+                  type: "true false",
+                  message: "Application doesn't allow users to update employee roles",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Repository Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Repository has a unique name",
+                  score: 2,
+                  type: "true false",
+                  message: "Repo doesn't have a unique name",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for file structure and naming conventions",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false
+                },
+                {
+                  title:
+                    "Repository contains multiple descriptive commit messages",
+                  score: 2,
+                  type: "true false",
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false
+                },
+                {
+                  title: "Repository contains quality README with description and a link to walkthrough video",
+                  score: 5,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Repo doesn't contain a quality README with description and a link to walkthrough video",
+                      score: 0
+                    },
+                    {
+                      message: "Repository README is missing either the  description or the link to the walkthrough video",
+                      score: 2
+                    },
+                    {
+                      message: "Repository contains quality README with description and a link to walkthrough video",
+                      score: 5
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Application Quality",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Application user experience is intuitive and easy to navigate",
+                  score: 10,
+                  type: "likert",
+                  options: [
+                    {
+                      message: "Application user experience is not intuitive and easy to navigate",
+                      score: 0
+                    },
+                    {
+                      message: "Application user experience could be much more intuitive and easy to navigate",
+                      score: 4
+                    },
+                    {
+                      message: "Application user experience could be a lot more intuitive and easy to navigate",
+                      score: 8
+                    },
+                    {
+                      message: "Application user experience is intuitive and easy to navigate",
+                      score: 10
+                    }
+                  ],
+                  message: "",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Bonus",
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: "Application allows users to update an employee's managers",
+                  score: 2,
+                  type: "true false",
+                  message: "Application allows users to update an employee's managers",
+                  status: false
+                },
+                {
+                  title: "Application allows users to view employees by manager",
+                  score: 2,
+                  type: "true false",
+                  message: "Application allows users to view employees by manager",
+                  status: false
+                },
+                {
+                  title: "Application allows users to view employees by department",
+                  score: 2,
+                  type: "true false",
+                  message: "Application allows users to view employees by department",
+                  status: false
+                },
+                {
+                  title: "Application allows users to delete departments",
+                  score: 2,
+                  type: "true false",
+                  message: "Application allows users to delete departments",
+                  status: false
+                },
+                {
+                  title: "Application allows users to delete roles",
+                  score: 2,
+                  type: "true false",
+                  message: "Application allows users to delete roles",
+                  status: false
+                },
+                {
+                  title: "Application allows users to delete employees",
+                  score: 2,
+                  type: "true false",
+                  message: "Application allows users to delete employees",
+                  status: false
+                },
+                {
+                  title: "Application allows users to view the total utilized budget of a department (the combined salaries of all employees in that department)",
+                  score: 8,
+                  type: "true false",
+                  message: "Application allows users to view the total utilized budget of a department (the combined salaries of all employees in that department)",
+                  status: false
+                }
+              ]
+            },
+            {
+              title: "Academic Integrity",
+              subtitle:
+                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+              score: 0,
+              questions: [
+                {
+                  title: "Application contains a low number of commits",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application codebase appears advanced for the student, at this point in the course, or in general",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references a different author and/or repository",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                },
+                {
+                  title:
+                    "Application package.json file references much older versions of npm packages",
+                  score: 0,
+                  type: "true false",
+                  message: null,
+                  status: false
+                }
+              ]
+            }
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+            .forEach(category => {
+              category.score = 0;
+              category.questions.forEach(question => {
+                question.category = category.title;
+                question.status = false;
+                question.id = this.questions.length;
+                question.flagged = category.title === "Academic Integrity";
+                question.bonus = category.title === "Bonus";
+                if (question.type === "likert") {
+                  question.message = question.options[0].message.split(" - ").pop();
+                };
+
+                let messageObj = {
+                  message: question.message,
+                  category: question.category
+                };
+
+                question.message && !question.bonus && this.messages.add(messageObj);
+                this.questions.push(question);
+                category.score += question.score;
+              });
+            });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+            if (question.type === "true false") {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector("select[name='" + question.id + "']").value;
+              categoryTotal += parseInt(score);
+            }
+          })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + "-summary";
+              return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .map(category => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + button;
+              return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+              `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+              `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+            }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .map(category => {
+                let score = category.score;
+                let title = category.title;
+                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                  .filter(messageObj => messageObj.category === category.title)
+                  .map(messageObj => `* ${messageObj.message}\n`)
+                  .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+              })
+              .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
+            }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                .filter(messageObj => messageObj.category === category.title)
+                .map(messageObj => `* ${messageObj.message}\n`)
+                .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+
+            this.model.questions
+              .filter(question => question.category !== "Academic Integrity")
+              .forEach(question => {
+                const id = question.id;
+                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter(category => category.title !== "Academic Integrity")
+              .forEach(category => {
+                const title = category.title
+                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+  </script>
+</body>
+
+</html>

+ 1430 - 0
rubrics/mod13.html

@@ -0,0 +1,1430 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+    <title>Module 13 ORM Challenge Rubric</title>
+    <style>
+        *,
+        *::before,
+        *::after {
+            box-sizing: border-box;
+        }
+
+        html,
+        body {
+            margin: 0;
+            padding: 0;
+            line-height: 1.15;
+            font-size: 100%;
+            color: #fefefe;
+            transition: all 0.4s;
+        }
+
+        body {
+            background-color: #fbfcfd;
+            font-family: "Inter", sans-serif;
+            color: #4a4a4a;
+        }
+
+        #app.dark {
+            background-color: #121212;
+            color: #e2e2e2;
+        }
+
+        h1 {
+            font-size: 1.9rem;
+        }
+
+        h2 {
+            font-size: 1.6rem;
+        }
+
+        h3 {
+            font-size: 1.2rem;
+        }
+
+        h4 {
+            font-size: 0.9rem;
+        }
+
+        p,
+        legend {
+            font-size: 1.2rem;
+        }
+
+        label {
+            font-size: 0.9rem;
+            line-height: 2;
+        }
+
+        fieldset {
+            border: none;
+            padding: 0;
+            margin-top: 0.9rem;
+        }
+
+        input[type="radio"],
+        input[type="checkbox"] {
+            position: absolute !important;
+            height: 1px;
+            width: 1px;
+            overflow: hidden;
+            clip: rect(1px 1px 1px 1px);
+            clip: rect(1px, 1px, 1px, 1px);
+            white-space: nowrap;
+        }
+
+        .select-lg,
+        .select-lg option {
+            font-family: "Inter", sans-serif;
+            padding: 10px;
+        }
+
+        .narrow {
+            width: 800px;
+        }
+
+        #app {
+            padding: 3%;
+            transition: all 0.4s;
+        }
+
+        #app h1 {
+            font-size: 1.9rem;
+            font-weight: 700;
+            line-height: 1.3;
+        }
+
+        header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .switch-group {
+            display: flex;
+            align-items: center;
+            height: 2.25rem;
+            width: 4.5rem;
+        }
+
+        input[type="checkbox"]+.toggle {
+            height: 100%;
+            width: 100%;
+            background-color: #77a1db;
+            border-radius: 1.9rem;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            padding: 0.35rem;
+            cursor: pointer;
+            overflow: hidden;
+            background-position: 25% 125%;
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]:focus+.toggle {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        input[type="checkbox"]:checked+.toggle {
+            background-color: rgb(85, 89, 204);
+            background-size: 1rem;
+            background-repeat: no-repeat;
+            background-position: 25%;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]+.toggle::after {
+            content: "";
+            display: block;
+            height: 1.7rem;
+            width: 1.7rem;
+            border-radius: 50%;
+            cursor: pointer;
+            border: 4px solid #e1be64;
+            background-color: #fddf74;
+            transform: translateX(0rem);
+            transition: all 0.5s;
+            background-position: center bottom;
+        }
+
+        input[type="checkbox"]:hover+.toggle::after {
+            transform: translateX(0rem) scale(1.1);
+        }
+
+        input[type="checkbox"]:checked+.toggle::after {
+            border: 4px solid #ffffff;
+            background-color: rgba(255, 255, 255, 0.4);
+            transform: translateX(2.1rem);
+            transition: all 0.5s;
+            background-position: center;
+            background-size: 60%;
+            background-repeat: no-repeat;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+        }
+
+        input[type="checkbox"]:checked:hover+.toggle::after {
+            transform: translateX(2.1rem) scale(1.075);
+        }
+
+        .card {
+            background-color: #fff;
+            border-radius: 6px;
+            border-width: 0;
+            box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+            font-size: 1.1rem;
+            height: 100%;
+            line-height: 1.8rem;
+            padding: 1.5rem 2rem;
+            width: 100%;
+            margin-bottom: 1.9rem;
+        }
+
+        .dark .card {
+            background-color: #333333;
+            box-shadow: none;
+        }
+
+        .card h1,
+        .card h2,
+        .card h3,
+        .card h4,
+        .card h5,
+        .card h6 {
+            color: #4a4a4a;
+        }
+
+        .dark .card h1,
+        .dark .card h2,
+        .dark .card h3,
+        .dark .card h4,
+        .dark .card h5,
+        .dark .card h6 {
+            color: #e2e2e2;
+        }
+
+        .card-header h2 {
+            display: inline-block;
+        }
+
+        .card .cat-score {
+            margin-left: 20px;
+            font-size: smaller;
+        }
+
+        .card h3 {
+            font-size: 1.2rem;
+            font-weight: 700;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card p,
+        .card label,
+        .card legend {
+            color: #7a869c;
+            border-width: 0;
+            display: block;
+            font-size: 1.1rem;
+            font-weight: 300;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card legend {
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+        }
+
+        .card legend .score {
+            font-size: smaller;
+            margin-left: 20px;
+            font-weight: 100;
+            min-width: 5%;
+        }
+
+        .dark .card p,
+        .dark .card label,
+        .dark .card legend {
+            color: #e2e2e2;
+        }
+
+        .card label {
+            display: inline-block;
+        }
+
+        .card input[type="radio"]+label::before {
+            content: "";
+            height: 0.9rem;
+            width: 0.9rem;
+            margin-right: 0.7rem;
+            border-radius: 50%;
+            border: 1px solid rgb(74, 74, 74);
+            background-color: #ffffff;
+            display: inline-block;
+            cursor: pointer;
+            position: relative;
+            top: 0.09rem;
+        }
+
+        .dark .card input[type="radio"]+label::before {
+            border: 1px solid #e2e2e2;
+            background-color: inherit;
+        }
+
+        .card input[type="radio"]:checked+label::before {
+            background-color: rgb(47, 106, 189);
+            border-color: rgb(47, 106, 189);
+        }
+
+        .dark .card input[type="radio"]:checked+label::before {
+            background-color: #d9d9d9;
+            border-color: #d9d9d9;
+        }
+
+        .card input[type="radio"]:checked+label {
+            font-weight: bold;
+            color: #4a4a4a;
+        }
+
+        .dark input[type="radio"]:checked+label {
+            color: #e2e2e2;
+        }
+
+        .card input[type="radio"]:focus+label::before {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        .card legend {
+            margin-bottom: 0.4rem;
+        }
+
+        .totals {
+            font-size: 1.6rem;
+        }
+
+        .totals+h2 {
+            font-size: 1.9rem;
+            margin-top: 0;
+        }
+
+        .grade {
+            font-size: 1.9rem;
+        }
+
+        button {
+            background-color: #fff;
+            border: 1px solid #4a4a4a;
+            border-radius: 6px;
+            box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+            color: #4a4a4a;
+            cursor: pointer;
+            display: inline-block;
+            font-size: 1rem;
+            font-weight: 500;
+            letter-spacing: 0.04em;
+            line-height: 1.8em;
+            margin: 0.75rem 1.1rem 1.1rem 0;
+            outline: none;
+            padding: 0.5rem 1.4rem;
+            text-transform: uppercase;
+            transition-delay: 0s;
+            transition-duration: 0.25s;
+            transition-property: all;
+            transition-timing-function: ease-out;
+            width: fit-content;
+        }
+
+        .dark button {
+            background: #d9d9d9;
+            color: #121212;
+            box-shadow: none;
+        }
+
+        button:hover {
+            border-color: #dbdbdb;
+            color: #dbdbdb;
+        }
+
+        .dark button:hover {
+            color: #121212;
+        }
+
+        button.copy {
+            margin-left: 0.4rem;
+            font-size: .8rem;
+        }
+
+        button:focus {
+            border-color: rgb(56, 56, 56);
+            box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+            color: rgb(56, 56, 56);
+        }
+
+        div[role="alert"] {
+            color: #4a4a4a;
+            font-size: 1.2rem;
+            margin-bottom: 1.2rem;
+        }
+
+        .dark div[role="alert"] {
+            color: #e2e2e2;
+        }
+
+        .summary {
+            font-size: 1.2rem;
+            border: none;
+            font-family: monospace;
+            background-color: #d5d9de;
+            padding: 0.9rem;
+            border-radius: 6px;
+            line-height: 1.5;
+            width: 100%;
+        }
+
+        .summary h2,
+        .dark .summary h2 {
+            font-size: 1.2rem;
+            font-family: "Inter", sans-serif;
+            margin-top: 35px;
+            margin-bottom: 5px;
+        }
+
+        .summary h2:first-of-type,
+        .dark .summary h2:first-of-type {
+            margin-top: 15px;
+        }
+
+        .dark .summary {
+            background: #6b6a6a;
+            color: #e2e2e2;
+        }
+
+        .summary pre,
+        .dark .summary pre {
+            white-space: pre-wrap;
+            border-radius: 5px;
+            padding: 15px;
+            border: solid 2px rgba(0, 0, 0, .4);
+        }
+
+        .summary pre:empty {
+            display: none;
+        }
+
+        .summary pre:empty+button {
+            display: none;
+        }
+
+        .d-none {
+            display: none;
+        }
+
+        @media (max-width: 450px) {
+            .btn-container {
+                text-align: center;
+            }
+
+            button {
+                width: 100%;
+                margin: 0.5rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+                background-size: 50%;
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+                background-size: 50%;
+            }
+        }
+
+        @media (max-width: 515px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.7rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.7rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 460px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.4rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.4rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 400px) {
+            #app h1 {
+                font-size: 1.6rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 340px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(0.9rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(0.9rem) scale(1.075);
+            }
+        }
+    </style>
+    <script>
+        $(function () {
+            let $select2 = $('.select2').select2({
+                containerCssClass: "wrap"
+            })
+
+            function triggerEvent(name, val) {
+                let selected = $("select[name='" + name + "']")
+                let change = new Event('change', { bubbles: true })
+                selected[0].dispatchEvent(change)
+            }
+
+            $('.select2').on('select2:select', function (e) {
+                triggerEvent(e.target.name, e.target.value)
+            });
+        })
+    </script>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script>
+            (function (window) {
+                "use strict";
+
+                function Model() {
+                    this.title = `Module 13 ORM Challenge Rubric (Coding)`;
+                    this.categories = [
+                        {
+                            title: "Deliverables",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Student submitted GitHub Repo containing application code",
+                                    score: 10,
+                                    type: "true false",
+                                    message: "GitHub repo is empty",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Walkthrough Video",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title:
+                                        "Student includes link to walkthrough video in README",
+                                    score: 6,
+                                    type: "true false",
+                                    message:
+                                        "Student does not include link to walkthrough video in README",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Walkthrough video shows all of the technical acceptance criteria being met",
+                                    score: 10,
+                                    type: "true false",
+                                    message:
+                                        "Walkthrough video doesn't show all of the technical acceptance criteria being met",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Walkthrough video demonstrates how to create the schema from the MySQL shell",
+                                    score: 2,
+                                    type: "true false",
+                                    message: "Walkthrough video does not demonstrate how to create the schema from the MySQL shell",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Walkthrough video demonstrates how to seed the database from the command line",
+                                    score: 2,
+                                    type: "true false",
+                                    message: "Walkthrough video does not demonstrate how to seed the database from the command line",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Walkthrough video demonstrates how to start the server",
+                                    score: 2,
+                                    type: "true false",
+                                    message: "Walkthrough video does not demonstrate how to start the server",
+                                    status: false
+                                },
+                                {
+                                    title: "Walkthrough video demonstrates GET routes for all categories, all products, and all tags being tested in Insomnia",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Walkthrough video does not demonstrate GET routes for all categories, all products, and all tags being tested in Insomnia",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Walkthrough video demonstrates GET routes for 1 of the 3 following being tested in Insomnia: all categories, all products, and all tags",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Walkthrough video demonstrates GET routes for 2 of the 3 following being tested in Insomnia: all categories, all products, and all tags",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Walkthrough video demonstrates GET routes for all categories, all products, and all tags being tested in Insomnia",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Walkthrough video demonstrates GET routes for a single category, a single product, and a single tag being tested in Insomnia",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Walkthrough video does not demonstrate GET routes for a single category, a single product, and a single tag being tested in Insomnia",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Walkthrough video demonstrates GET routes for 1 of the 3 following being tested in Insomnia: a single category, a single product, and a single tag",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Walkthrough video demonstrates GET routes for 2 of the 3 following being tested in Insomnia: a single category, a single product, and a single tag",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Walkthrough video demonstrates GET routes for a single category, a single product, and a single tag being tested in Insomnia",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Walkthrough video demonstrates POST, PUT, and DELETE routes for categories, products, and tags being tested in Insomnia",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Walkthrough video does not demonstrate POST, PUT, and DELETE routes for categories, products, and tags being tested in Insomnia",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Walkthrough video demonstrates POST, PUT, and DELETE routes for 1 of the 3 following being tested in Insomnia: categories, products, and tags",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Walkthrough video demonstrates POST, PUT, and DELETE routes for 2 of the 3 following being tested in Insomnia: categories, products, and tags",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Walkthrough video demonstrates POST, PUT, and DELETE routes for categories, products, and tags being tested in Insomnia",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Technical Acceptance Criteria",
+                            subtitle:
+                                "Watch out for errors in the console from this point forward.",
+                            score: 0,
+                            questions: [
+                                {
+                                    title:
+                                        "Application syncs Sequelize models to MySQL database on server start",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application doesn't sync Sequelize models to MySQL database on server start",
+                                    status: false
+                                },
+                                {
+                                    title: "Application includes column definitions for all 4 models that match Challenge instructions",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't include column definitions for all 4 models that match Challenge instructions",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application includes column definitions for 1 of the 4 models that match Challenge instructions",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application includes column definitions for 2 of the 4 models that match Challenge instructions",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application includes column definitions for 3 of the 4 models that match Challenge instructions",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application includes column definitions for all 4 models that match Challenge instructions",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application includes model associations that match Challenge instructions",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application doesn't include model associations that match Challenge instructions",
+                                    status: false
+                                },
+                                {
+                                    title: "Application includes GET routes that return all categories, all products, and all tags",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't include GET routes that return all categories, all products, and all tags",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application includes GET routes that return 1 of the 3 following: all categories, all products, and all tags",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application includes GET routes that return 2 of the 3 following: all categories, all products, and all tags",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application includes GET routes that return all categories, all products, and all tags",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application includes GET routes that return a single category, a single product, and a single tag",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't include GET routes that return a single category, a single product, and a single tag",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application includes GET routes that return 1 of the 3 following: a single category, a single product, and a single tag",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application includes GET routes that return 2 of the 3 following: a single category, a single product, and a single tag",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application includes GET routes that return a single category, a single product, and a single tag",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application includes POST routes that can create a new category, a new product, or a new tag",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't include POST routes that can create a new category, a new product, or a new tag",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application includes POST routes that can create 1 of the 3 following: a new category, a new product, or a new tag",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application includes POST routes that can create 2 of the 3 following: a new category, a new product, or a new tag",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application includes POST routes that can create a new category, a new product, or a new tag",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application includes PUT routes that can update a category, a product, or a tag",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't include PUT routes that can update a category, a product, or a tag",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application includes PUT routes that can update 1 of the 3 following: a category, a product, or a tag",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application includes PUT routes that can update 2 of the 3 following: a category, a product, or a tag",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application includes PUT routes that can update a category, a product, or a tag",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application includes DELETE routes that can delete a category, a product, or a tag",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't include DELETE routes that can delete a category, a product, or a tag",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application includes DELETE routes that can delete 1 of the 3 following: a category, a product, or a tag",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application includes DELETE routes that can delete 2 of the 3 following: a category, a product, or a tag",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application includes DELETE routes that can delete a category, a product, or a tag",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Repository Quality",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Repository has a unique name",
+                                    score: 2,
+                                    type: "true false",
+                                    message: "Repo doesn't have a unique name",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository follows best practices for file structure and naming conventions",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Code doesn't follow recommended file/folder structure",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository contains multiple descriptive commit messages",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Repo doesn't contain multiple descriptive commit messages",
+                                    status: false
+                                },
+                                {
+                                    title: "Repository contains quality README with description and a link to walkthrough video",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Repo doesn't contain a quality README with description and a link to walkthrough video",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Repository README is missing either the  description or the link to the walkthrough video",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Repository contains quality README with description and a link to walkthrough video",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Academic Integrity",
+                            subtitle:
+                                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Application contains a low number of commits",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application codebase appears advanced for the student, at this point in the course, or in general",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application package.json file references a different author and/or repository",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application package.json file references much older versions of npm packages",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                }
+                            ]
+                        }
+                    ];
+
+                    this.initStorage();
+                }
+
+                Model.prototype.initStorage = function () {
+                    this.score = 0;
+                    this.questions = [];
+                    this.messages = new Set();
+                    this.flagged = new Set();
+                    this.isDarkMode =
+                        JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+                    this.categories
+                        .forEach(category => {
+                            category.score = 0;
+                            category.questions.forEach(question => {
+                                question.category = category.title;
+                                question.status = false;
+                                question.id = this.questions.length;
+                                question.flagged = category.title === "Academic Integrity";
+                                question.bonus = category.title === "Bonus";
+                                if (question.type === "likert") {
+                                    question.message = question.options[0].message.split(" - ").pop();
+                                };
+
+                                let messageObj = {
+                                    message: question.message,
+                                    category: question.category
+                                };
+
+                                question.message && !question.bonus && this.messages.add(messageObj);
+                                this.questions.push(question);
+                                category.score += question.score;
+                            });
+                        });
+                };
+
+                Model.prototype.toggleDarkMode = function () {
+                    this.isDarkMode = !this.isDarkMode;
+                    localStorage.setItem("isDarkMode", this.isDarkMode);
+                };
+
+                Model.prototype.toggleRadio = function (radio) {
+                    const id = Number(radio.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+                    question.status = !question.status;
+
+                    if (question.status === true) {
+
+                        if (!question.bonus) {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        } else {
+                            let messageObj = {
+                                message: question.message,
+                                category: category
+                            }
+                            this.messages.add(messageObj);
+                        }
+
+                        this.score += question.score;
+
+                        if (question.flagged) {
+                            this.flagged.add(question.id);
+                        }
+                    } else {
+                        if (question.flagged) {
+                            this.flagged.delete(question.id);
+                        }
+
+                        if (!question.bonus) {
+                            if (question.message) {
+                                let messageObj = {
+                                    message: question.message,
+                                    category: category
+                                }
+                                this.messages.add(messageObj);
+                            }
+                        } else {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        }
+
+                        this.score -= question.score;
+                    }
+
+                    if (category !== "Academic Integrity") {
+                        if (question.status) {
+                            questionScore.textContent = question.score;
+                        } else {
+                            questionScore.textContent = 0;
+                        }
+
+                        const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                        let categoryTotal = 0;
+
+                        catQuestions.forEach(question => {
+                            if (question.type === "true false") {
+                                if (question.status) {
+                                    categoryTotal += question.score;
+                                }
+                            } else {
+                                let score = document.querySelector("select[name='" + question.id + "']").value;
+                                categoryTotal += parseInt(score);
+                            }
+                        })
+
+                        categoryScore.textContent = categoryTotal;
+                        resultsCategoryScore.textContent = categoryTotal;
+                    }
+
+                };
+
+                Model.prototype.handleSelect = function (select) {
+                    const id = Number(select.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const oldScore = questionScore.textContent;
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+                    let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+                    if (newMessage === "Open this menu") {
+                        newMessage = select[1].text.split(" - ").pop();
+                    };
+
+                    if (question.type === "likert") {
+
+                        this.messages.forEach(item => {
+                            if (item.message === question.message) {
+                                this.messages.delete(item)
+                            }
+                        })
+
+                        question.message = newMessage;
+
+                        let messageObj = {
+                            message: newMessage,
+                            category: category
+                        }
+
+                        this.messages.add(messageObj);
+
+                    };
+
+
+
+                    this.score -= parseInt(oldScore);
+                    this.score += parseInt(select.value);
+                    questionScore.textContent = select.value;
+
+                    const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                    let categoryTotal = 0;
+
+                    catQuestions.forEach(question => {
+                        if (question.type === "true false") {
+                            if (question.status) {
+                                categoryTotal += question.score;
+                            }
+                        } else {
+                            let score = document.querySelector("select[name='" + question.id + "']").value;
+                            categoryTotal += parseInt(score);
+                        }
+                    })
+
+                    categoryScore.textContent = categoryTotal;
+                    resultsCategoryScore.textContent = categoryTotal;
+
+                };
+
+                Model.prototype.calcGrade = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.score));
+
+                    let letterGrade = "";
+
+                    if (roundedScore >= 99.5) {
+                        letterGrade = "A+";
+                    } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+                        letterGrade = "A";
+                    } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+                        letterGrade = "A-";
+                    } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+                        letterGrade = "B+";
+                    } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+                        letterGrade = "B";
+                    } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+                        letterGrade = "B-";
+                    } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+                        letterGrade = "C+";
+                    } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+                        letterGrade = "C";
+                    } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+                        letterGrade = "C-";
+                    } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+                        letterGrade = "D+";
+                    } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+                        letterGrade = "D";
+                    } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+                        letterGrade = "D-";
+                    } else {
+                        letterGrade = "F";
+                    }
+
+                    return letterGrade;
+                };
+
+                Model.prototype.hasFlags = function () {
+                    return this.flagged.size > 0;
+                };
+
+                function View(model) {
+                    this.model = model;
+                    this.generateHTML();
+                    this.cacheDom();
+                }
+
+                View.prototype.cacheDom = function () {
+                    this.darkModeToggle = document.querySelector("#dark-mode");
+
+                    this.radios = Array.from(
+                        document.querySelectorAll("input[type='radio']")
+                    );
+
+                    this.selects = Array.from(
+                        document.querySelectorAll("select")
+                    );
+
+                    this.totals = document.querySelector(".totals");
+                    this.grade = document.querySelector(".grade");
+                    this.alert = document.querySelector("div[role='alert']");
+                    this.summaryHeading = document.querySelector(".summary-heading");
+                    this.summary = document.querySelector(".summary");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let summaryName = summary + "-summary";
+                            return this[summaryName] = document.querySelector("." + summary);
+                        })
+
+                    this.resetButton = document.querySelector(".reset");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let button = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + button;
+                            return this[buttonName] = document.querySelector(".copy-" + button);
+                        })
+                };
+
+                View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+                    return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+                            `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+                            `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+                        }
+        </fieldset>`;
+                };
+
+                View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+                    return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+                };
+
+                View.prototype.createResults = function () {
+                    return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .map(category => {
+                                let score = category.score;
+                                let title = category.title;
+                                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                                    .filter(messageObj => messageObj.category === category.title)
+                                    .map(messageObj => `* ${messageObj.message}\n`)
+                                    .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+                            })
+                            .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+                };
+
+                View.prototype.createHeader = function (title, isDarkMode = false) {
+                    return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
+                        }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+                };
+
+                View.prototype.generateHTML = function () {
+                    this.wrapper = document.querySelector("#app");
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    }
+
+                    const survey = this.model.categories.reduce((final, current) => {
+                        final += this.createCard(current);
+                        return final;
+                    }, "");
+
+                    const results = this.createResults();
+
+                    const header = this.createHeader(
+                        this.model.title,
+                        this.model.isDarkMode
+                    );
+
+                    this.wrapper.innerHTML = header + survey + results;
+                };
+
+                View.prototype.update = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.model.score));
+                    this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+                    const letterGrade = this.model.calcGrade();
+                    this.grade.textContent = letterGrade;
+
+                    if (this.model.hasFlags()) {
+                        this.alert.classList.remove("d-none");
+                    } else {
+                        this.alert.classList.add("d-none");
+                    }
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    } else {
+                        this.wrapper.classList.remove("dark");
+                    }
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+                            this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                                .filter(messageObj => messageObj.category === category.title)
+                                .map(messageObj => `* ${messageObj.message}\n`)
+                                .join("")
+                        })
+
+                };
+
+                function Controller(model, view) {
+                    this.model = model;
+                    this.view = view;
+                    this.addEventListeners();
+                }
+
+                Controller.prototype.addEventListeners = function () {
+                    this.view.wrapper.addEventListener("change", event => {
+                        const { target } = event;
+
+                        if (target.name !== "dark-mode") {
+                            if (!target.matches("input[type='radio']")) {
+                                this.model.handleSelect(target);
+                            } else {
+                                this.model.toggleRadio(target);
+                            }
+                        }
+
+                        this.view.update();
+                    });
+
+                    this.view.resetButton.addEventListener("click", () => {
+                        this.model.initStorage();
+
+                        this.view.radios.forEach(
+                            radio => (radio.checked = radio.hasAttribute("checked"))
+                        );
+
+                        this.view.selects.forEach(
+                            select => select.selectedIndex = null
+                        )
+
+                        this.model.questions
+                            .filter(question => question.category !== "Academic Integrity")
+                            .forEach(question => {
+                                const id = question.id;
+                                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                                questionScore.textContent = 0;
+                            });
+
+                        this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .forEach(category => {
+                                const title = category.title
+                                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                                categoryScore.textContent = 0;
+                                resultsCategoryScore.textContent = 0;
+                            });
+
+                        this.view.update();
+                        window.scrollTo(0, 0);
+                    });
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let title = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + title;
+
+                            let summaryName = title + "-summary";
+
+                            this.view[buttonName].addEventListener("click", () => {
+                                const selection = window.getSelection();
+                                const range = document.createRange();
+                                range.selectNodeContents(this.view[summaryName]);
+                                selection.removeAllRanges();
+                                selection.addRange(range);
+                                document.execCommand("copy");
+                            });
+                        });
+
+
+                    this.view.darkModeToggle.addEventListener("change", () => {
+                        this.model.toggleDarkMode();
+                        this.view.update();
+                    });
+                };
+
+                function App(Model, View, Controller) {
+                    this.model = new Model();
+                    this.view = new View(this.model);
+                    this.controller = new Controller(this.model, this.view);
+                }
+
+                return new App(Model, View, Controller);
+            })(window);
+    </script>
+</body>
+
+</html>

+ 1406 - 0
rubrics/mod14.html

@@ -0,0 +1,1406 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+    <title>Module 14 MVC Challenge Rubric</title>
+    <style>
+        *,
+        *::before,
+        *::after {
+            box-sizing: border-box;
+        }
+
+        html,
+        body {
+            margin: 0;
+            padding: 0;
+            line-height: 1.15;
+            font-size: 100%;
+            color: #fefefe;
+            transition: all 0.4s;
+        }
+
+        body {
+            background-color: #fbfcfd;
+            font-family: "Inter", sans-serif;
+            color: #4a4a4a;
+        }
+
+        #app.dark {
+            background-color: #121212;
+            color: #e2e2e2;
+        }
+
+        h1 {
+            font-size: 1.9rem;
+        }
+
+        h2 {
+            font-size: 1.6rem;
+        }
+
+        h3 {
+            font-size: 1.2rem;
+        }
+
+        h4 {
+            font-size: 0.9rem;
+        }
+
+        p,
+        legend {
+            font-size: 1.2rem;
+        }
+
+        label {
+            font-size: 0.9rem;
+            line-height: 2;
+        }
+
+        fieldset {
+            border: none;
+            padding: 0;
+            margin-top: 0.9rem;
+        }
+
+        input[type="radio"],
+        input[type="checkbox"] {
+            position: absolute !important;
+            height: 1px;
+            width: 1px;
+            overflow: hidden;
+            clip: rect(1px 1px 1px 1px);
+            clip: rect(1px, 1px, 1px, 1px);
+            white-space: nowrap;
+        }
+
+        .select-lg,
+        .select-lg option {
+            font-family: "Inter", sans-serif;
+            padding: 10px;
+        }
+
+        .narrow {
+            width: 800px;
+        }
+
+        #app {
+            padding: 3%;
+            transition: all 0.4s;
+        }
+
+        #app h1 {
+            font-size: 1.9rem;
+            font-weight: 700;
+            line-height: 1.3;
+        }
+
+        header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .switch-group {
+            display: flex;
+            align-items: center;
+            height: 2.25rem;
+            width: 4.5rem;
+        }
+
+        input[type="checkbox"]+.toggle {
+            height: 100%;
+            width: 100%;
+            background-color: #77a1db;
+            border-radius: 1.9rem;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            padding: 0.35rem;
+            cursor: pointer;
+            overflow: hidden;
+            background-position: 25% 125%;
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]:focus+.toggle {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        input[type="checkbox"]:checked+.toggle {
+            background-color: rgb(85, 89, 204);
+            background-size: 1rem;
+            background-repeat: no-repeat;
+            background-position: 25%;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]+.toggle::after {
+            content: "";
+            display: block;
+            height: 1.7rem;
+            width: 1.7rem;
+            border-radius: 50%;
+            cursor: pointer;
+            border: 4px solid #e1be64;
+            background-color: #fddf74;
+            transform: translateX(0rem);
+            transition: all 0.5s;
+            background-position: center bottom;
+        }
+
+        input[type="checkbox"]:hover+.toggle::after {
+            transform: translateX(0rem) scale(1.1);
+        }
+
+        input[type="checkbox"]:checked+.toggle::after {
+            border: 4px solid #ffffff;
+            background-color: rgba(255, 255, 255, 0.4);
+            transform: translateX(2.1rem);
+            transition: all 0.5s;
+            background-position: center;
+            background-size: 60%;
+            background-repeat: no-repeat;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+        }
+
+        input[type="checkbox"]:checked:hover+.toggle::after {
+            transform: translateX(2.1rem) scale(1.075);
+        }
+
+        .card {
+            background-color: #fff;
+            border-radius: 6px;
+            border-width: 0;
+            box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+            font-size: 1.1rem;
+            height: 100%;
+            line-height: 1.8rem;
+            padding: 1.5rem 2rem;
+            width: 100%;
+            margin-bottom: 1.9rem;
+        }
+
+        .dark .card {
+            background-color: #333333;
+            box-shadow: none;
+        }
+
+        .card h1,
+        .card h2,
+        .card h3,
+        .card h4,
+        .card h5,
+        .card h6 {
+            color: #4a4a4a;
+        }
+
+        .dark .card h1,
+        .dark .card h2,
+        .dark .card h3,
+        .dark .card h4,
+        .dark .card h5,
+        .dark .card h6 {
+            color: #e2e2e2;
+        }
+
+        .card-header h2 {
+            display: inline-block;
+        }
+
+        .card .cat-score {
+            margin-left: 20px;
+            font-size: smaller;
+        }
+
+        .card h3 {
+            font-size: 1.2rem;
+            font-weight: 700;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card p,
+        .card label,
+        .card legend {
+            color: #7a869c;
+            border-width: 0;
+            display: block;
+            font-size: 1.1rem;
+            font-weight: 300;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card legend {
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+        }
+
+        .card legend .score {
+            font-size: smaller;
+            margin-left: 20px;
+            font-weight: 100;
+            min-width: 5%;
+        }
+
+        .dark .card p,
+        .dark .card label,
+        .dark .card legend {
+            color: #e2e2e2;
+        }
+
+        .card label {
+            display: inline-block;
+        }
+
+        .card input[type="radio"]+label::before {
+            content: "";
+            height: 0.9rem;
+            width: 0.9rem;
+            margin-right: 0.7rem;
+            border-radius: 50%;
+            border: 1px solid rgb(74, 74, 74);
+            background-color: #ffffff;
+            display: inline-block;
+            cursor: pointer;
+            position: relative;
+            top: 0.09rem;
+        }
+
+        .dark .card input[type="radio"]+label::before {
+            border: 1px solid #e2e2e2;
+            background-color: inherit;
+        }
+
+        .card input[type="radio"]:checked+label::before {
+            background-color: rgb(47, 106, 189);
+            border-color: rgb(47, 106, 189);
+        }
+
+        .dark .card input[type="radio"]:checked+label::before {
+            background-color: #d9d9d9;
+            border-color: #d9d9d9;
+        }
+
+        .card input[type="radio"]:checked+label {
+            font-weight: bold;
+            color: #4a4a4a;
+        }
+
+        .dark input[type="radio"]:checked+label {
+            color: #e2e2e2;
+        }
+
+        .card input[type="radio"]:focus+label::before {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        .card legend {
+            margin-bottom: 0.4rem;
+        }
+
+        .totals {
+            font-size: 1.6rem;
+        }
+
+        .totals+h2 {
+            font-size: 1.9rem;
+            margin-top: 0;
+        }
+
+        .grade {
+            font-size: 1.9rem;
+        }
+
+        button {
+            background-color: #fff;
+            border: 1px solid #4a4a4a;
+            border-radius: 6px;
+            box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+            color: #4a4a4a;
+            cursor: pointer;
+            display: inline-block;
+            font-size: 1rem;
+            font-weight: 500;
+            letter-spacing: 0.04em;
+            line-height: 1.8em;
+            margin: 0.75rem 1.1rem 1.1rem 0;
+            outline: none;
+            padding: 0.5rem 1.4rem;
+            text-transform: uppercase;
+            transition-delay: 0s;
+            transition-duration: 0.25s;
+            transition-property: all;
+            transition-timing-function: ease-out;
+            width: fit-content;
+        }
+
+        .dark button {
+            background: #d9d9d9;
+            color: #121212;
+            box-shadow: none;
+        }
+
+        button:hover {
+            border-color: #dbdbdb;
+            color: #dbdbdb;
+        }
+
+        .dark button:hover {
+            color: #121212;
+        }
+
+        button.copy {
+            margin-left: 0.4rem;
+            font-size: .8rem;
+        }
+
+        button:focus {
+            border-color: rgb(56, 56, 56);
+            box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+            color: rgb(56, 56, 56);
+        }
+
+        div[role="alert"] {
+            color: #4a4a4a;
+            font-size: 1.2rem;
+            margin-bottom: 1.2rem;
+        }
+
+        .dark div[role="alert"] {
+            color: #e2e2e2;
+        }
+
+        .summary {
+            font-size: 1.2rem;
+            border: none;
+            font-family: monospace;
+            background-color: #d5d9de;
+            padding: 0.9rem;
+            border-radius: 6px;
+            line-height: 1.5;
+            width: 100%;
+        }
+
+        .summary h2,
+        .dark .summary h2 {
+            font-size: 1.2rem;
+            font-family: "Inter", sans-serif;
+            margin-top: 35px;
+            margin-bottom: 5px;
+        }
+
+        .summary h2:first-of-type,
+        .dark .summary h2:first-of-type {
+            margin-top: 15px;
+        }
+
+        .dark .summary {
+            background: #6b6a6a;
+            color: #e2e2e2;
+        }
+
+        .summary pre,
+        .dark .summary pre {
+            white-space: pre-wrap;
+            border-radius: 5px;
+            padding: 15px;
+            border: solid 2px rgba(0, 0, 0, .4);
+        }
+
+        .summary pre:empty {
+            display: none;
+        }
+
+        .summary pre:empty+button {
+            display: none;
+        }
+
+        .d-none {
+            display: none;
+        }
+
+        @media (max-width: 450px) {
+            .btn-container {
+                text-align: center;
+            }
+
+            button {
+                width: 100%;
+                margin: 0.5rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+                background-size: 50%;
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+                background-size: 50%;
+            }
+        }
+
+        @media (max-width: 515px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.7rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.7rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 460px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.4rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.4rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 400px) {
+            #app h1 {
+                font-size: 1.6rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 340px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(0.9rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(0.9rem) scale(1.075);
+            }
+        }
+    </style>
+    <script>
+        $(function () {
+            let $select2 = $('.select2').select2({
+                containerCssClass: "wrap"
+            })
+
+            function triggerEvent(name, val) {
+                let selected = $("select[name='" + name + "']")
+                let change = new Event('change', { bubbles: true })
+                selected[0].dispatchEvent(change)
+            }
+
+            $('.select2').on('select2:select', function (e) {
+                triggerEvent(e.target.name, e.target.value)
+            });
+        })
+    </script>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script>
+            (function (window) {
+                "use strict";
+
+                function Model() {
+                    this.title = `Module 14 MVC Challenge Rubric (Coding)`;
+                    this.categories = [
+                        {
+                            title: "Deployment",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Student submitted deployed live application URL",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "Application isn't deployed",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application loads at deployed URL (no 404, or Heroku/GH Pages error)",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "Application isn't deployed",
+                                    status: false
+                                },
+                                {
+                                    title: "Student submitted application GitHub URL",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "Missing GitHub submission URL",
+                                    status: false
+                                },
+                                {
+                                    title: "GitHub Repo contains application code",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "GitHub repo is empty",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Technical Acceptance Criteria",
+                            subtitle:
+                                "Keep your browser console open and watch out for errors from this point forward.",
+                            score: 0,
+                            questions: [
+                                {
+                                    title:
+                                        "Application allows users to sign up and log in",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application doesn't allow users to sign up and log in",
+                                    status: false
+                                },
+                                {
+                                    title: "Application includes navigation options for home, a dashboard, and the login page, and the dashboard page redirects to the login page if the user is not logged in",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't include navigation options for home, a dashboard, and the login page, and the dashboard page doesn't redirect to the login page if the user is not logged in",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application includes navigation options for home, a dashboard, and the login page, but the dashboard page doesn't redirect to the login page if the user is not logged in",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application includes navigation options for home, a dashboard, and the login page, and the dashboard page redirects to the login page if the user is not logged in",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application displays all existing blog posts (post title, contents, post creator’s username, and date created) on the home page and the user's blog posts on their dashboard",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't display all existing blog posts (post title, contents, post creator’s username, and date created) on the home page and the user's blog posts on their dashboard",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application either doesn't display all existing blog posts (post title, contents, post creator’s username, and date created) on the home page or doesn't display the user's blog posts on their dashboard",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application displays all existing blog posts (post title, contents, post creator’s username, and date created) on the home page and the user's blog posts on their dashboard",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application dashboard includes the option to create new blog posts, including a title and content, and both the homepage and dashboard are updated to include the new post",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application dashboard doesn't include the option to create new blog posts",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application dashboard includes the option to create new blog posts, but is missing either the option to include a title or the content",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application dashboard includes the option to create new blog posts, including a title and content, but doesn't update both the homepage and dashboard to include the new post",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application dashboard includes the option to create new blog posts, including a title and content, but either doesn't update the homepage to include the new post or doesn't update the dashboard to include the new post",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application dashboard includes the option to create new blog posts, including a title and content, and both the homepage and dashboard are updated to include the new post",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application dashboard allows user to click into their blog posts and either update or delete them",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application dashboard doesn't allow user to click into their blog posts and either update or delete them",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application dashboard allows user to click into their blog posts but is missing either the option to update them or delete them",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application dashboard allows user to click into their blog posts and either update or delete them",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application allows all users to click on existing blog posts on the home page and see the post title, contents, post creator’s username, date created for that post, and any existing comments (comment, comment creator’s username, and the date it was created)",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't allow users to click on existing blog posts on the home page and see the post title, contents, post creator’s username, date created for that post, and any existing comments (comment, comment creator’s username, and the date it was created)",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application allows all users to click on existing blog posts on the home page, but 4 of the following 5 items are missing: the post title, contents, post creator’s username, date created for that post, and any existing comments (comment, comment creator’s username, and the date it was created)",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application allows all users to click on existing blog posts on the home page, but 3 of the following 5 items are missing: the post title, contents, post creator’s username, date created for that post, and any existing comments (comment, comment creator’s username, and the date it was created)",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application allows all users to click on existing blog posts on the home page, but 2 of the following 5 items are missing: the post title, contents, post creator’s username, date created for that post, and any existing comments (comment, comment creator’s username, and the date it was created)",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application allows all users to click on existing blog posts on the home page, but 1 of the following 5 items is missing: the post title, contents, post creator’s username, date created for that post, and any existing comments (comment, comment creator’s username, and the date it was created)",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application allows all users to click on existing blog posts on the home page and see the post title, contents, post creator’s username, date created for that post, and any existing comments (comment, comment creator’s username, and the date it was created)",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application allows logged in users to leave a comment on a post that they click on and updates post to include comment",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application doesn't allow logged in users to leave a comment on a post that they click on and doesn't update post to include comment",
+                                    status: false
+                                },
+                                {
+                                    title: "Application navigation includes a link for users to log out of application and the user is logged out automatically when they are idle on the site for more than a set time",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application navigation doesn't include a link for users to log out of application and doesn't log the user out automatically when they are idle on the site for more than a set time",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application navigation includes a link for users to log out of application but doesn't log the user out automatically when they are idle on the site for more than a set time",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application navigation includes a link for users to log out of application and the user is logged out automatically when they are idle on the site for more than a set time",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Repository Quality",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Repository has a unique name",
+                                    score: 2,
+                                    type: "true false",
+                                    message: "Repo doesn't have a unique name",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository follows best practices for file structure and naming conventions",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Code doesn't follow recommended file/folder structure",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository contains multiple descriptive commit messages",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Repo doesn't contain multiple descriptive commit messages",
+                                    status: false
+                                },
+                                {
+                                    title: "Repository contains quality README with description, screenshot, link to deployed application",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Repository contains quality README with description, screenshot, link to deployed application",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Application Quality",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Application user experience is intuitive and easy to navigate",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application user experience is not intuitive and easy to navigate",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application user experience could be much more intuitive and easy to navigate",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application user experience could be a lot more intuitive and easy to navigate",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application user experience is intuitive and easy to navigate",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application user interface style is clean and polished",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application user interface is not clean and polished",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application user interface could be much better",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application user interface could be a bit better",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application user interface style is clean and polished",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application resembles the mock-up functionality provided in Challenge instructions",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't resemble the mock-up functionality provided in Challenge instructions",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application could resemble mock-up functionality provided with Challenge instructions much more closely",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application could resemble mock-up functionality provided with Challenge instructions a bit more closely",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application resembles the mock-up functionality provided in Challenge instructions",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Academic Integrity",
+                            subtitle:
+                                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Application contains a low number of commits",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application codebase appears advanced for the student, at this point in the course, or in general",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application package.json file references a different author and/or repository",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application package.json file references much older versions of npm packages",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                }
+                            ]
+                        }
+                    ];
+
+                    this.initStorage();
+                }
+
+                Model.prototype.initStorage = function () {
+                    this.score = 0;
+                    this.questions = [];
+                    this.messages = new Set();
+                    this.flagged = new Set();
+                    this.isDarkMode =
+                        JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+                    this.categories
+                        .forEach(category => {
+                            category.score = 0;
+                            category.questions.forEach(question => {
+                                question.category = category.title;
+                                question.status = false;
+                                question.id = this.questions.length;
+                                question.flagged = category.title === "Academic Integrity";
+                                question.bonus = category.title === "Bonus";
+                                if (question.type === "likert") {
+                                    question.message = question.options[0].message.split(" - ").pop();
+                                };
+
+                                let messageObj = {
+                                    message: question.message,
+                                    category: question.category
+                                };
+
+                                question.message && !question.bonus && this.messages.add(messageObj);
+                                this.questions.push(question);
+                                category.score += question.score;
+                            });
+                        });
+                };
+
+                Model.prototype.toggleDarkMode = function () {
+                    this.isDarkMode = !this.isDarkMode;
+                    localStorage.setItem("isDarkMode", this.isDarkMode);
+                };
+
+                Model.prototype.toggleRadio = function (radio) {
+                    const id = Number(radio.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+                    question.status = !question.status;
+
+                    if (question.status === true) {
+
+                        if (!question.bonus) {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        } else {
+                            let messageObj = {
+                                message: question.message,
+                                category: category
+                            }
+                            this.messages.add(messageObj);
+                        }
+
+                        this.score += question.score;
+
+                        if (question.flagged) {
+                            this.flagged.add(question.id);
+                        }
+                    } else {
+                        if (question.flagged) {
+                            this.flagged.delete(question.id);
+                        }
+
+                        if (!question.bonus) {
+                            if (question.message) {
+                                let messageObj = {
+                                    message: question.message,
+                                    category: category
+                                }
+                                this.messages.add(messageObj);
+                            }
+                        } else {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        }
+
+                        this.score -= question.score;
+                    }
+
+                    if (category !== "Academic Integrity") {
+                        if (question.status) {
+                            questionScore.textContent = question.score;
+                        } else {
+                            questionScore.textContent = 0;
+                        }
+
+                        const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                        let categoryTotal = 0;
+
+                        catQuestions.forEach(question => {
+                            if (question.type === "true false") {
+                                if (question.status) {
+                                    categoryTotal += question.score;
+                                }
+                            } else {
+                                let score = document.querySelector("select[name='" + question.id + "']").value;
+                                categoryTotal += parseInt(score);
+                            }
+                        })
+
+                        categoryScore.textContent = categoryTotal;
+                        resultsCategoryScore.textContent = categoryTotal;
+                    }
+
+                };
+
+                Model.prototype.handleSelect = function (select) {
+                    const id = Number(select.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const oldScore = questionScore.textContent;
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+                    let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+                    if (newMessage === "Open this menu") {
+                        newMessage = select[1].text.split(" - ").pop();
+                    };
+
+                    if (question.type === "likert") {
+
+                        this.messages.forEach(item => {
+                            if (item.message === question.message) {
+                                this.messages.delete(item)
+                            }
+                        })
+
+                        question.message = newMessage;
+
+                        let messageObj = {
+                            message: newMessage,
+                            category: category
+                        }
+
+                        this.messages.add(messageObj);
+
+                    };
+
+
+
+                    this.score -= parseInt(oldScore);
+                    this.score += parseInt(select.value);
+                    questionScore.textContent = select.value;
+
+                    const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                    let categoryTotal = 0;
+
+                    catQuestions.forEach(question => {
+                        if (question.type === "true false") {
+                            if (question.status) {
+                                categoryTotal += question.score;
+                            }
+                        } else {
+                            let score = document.querySelector("select[name='" + question.id + "']").value;
+                            categoryTotal += parseInt(score);
+                        }
+                    })
+
+                    categoryScore.textContent = categoryTotal;
+                    resultsCategoryScore.textContent = categoryTotal;
+
+                };
+
+                Model.prototype.calcGrade = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.score));
+
+                    let letterGrade = "";
+
+                    if (roundedScore >= 99.5) {
+                        letterGrade = "A+";
+                    } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+                        letterGrade = "A";
+                    } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+                        letterGrade = "A-";
+                    } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+                        letterGrade = "B+";
+                    } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+                        letterGrade = "B";
+                    } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+                        letterGrade = "B-";
+                    } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+                        letterGrade = "C+";
+                    } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+                        letterGrade = "C";
+                    } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+                        letterGrade = "C-";
+                    } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+                        letterGrade = "D+";
+                    } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+                        letterGrade = "D";
+                    } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+                        letterGrade = "D-";
+                    } else {
+                        letterGrade = "F";
+                    }
+
+                    return letterGrade;
+                };
+
+                Model.prototype.hasFlags = function () {
+                    return this.flagged.size > 0;
+                };
+
+                function View(model) {
+                    this.model = model;
+                    this.generateHTML();
+                    this.cacheDom();
+                }
+
+                View.prototype.cacheDom = function () {
+                    this.darkModeToggle = document.querySelector("#dark-mode");
+
+                    this.radios = Array.from(
+                        document.querySelectorAll("input[type='radio']")
+                    );
+
+                    this.selects = Array.from(
+                        document.querySelectorAll("select")
+                    );
+
+                    this.totals = document.querySelector(".totals");
+                    this.grade = document.querySelector(".grade");
+                    this.alert = document.querySelector("div[role='alert']");
+                    this.summaryHeading = document.querySelector(".summary-heading");
+                    this.summary = document.querySelector(".summary");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let summaryName = summary + "-summary";
+                            return this[summaryName] = document.querySelector("." + summary);
+                        })
+
+                    this.resetButton = document.querySelector(".reset");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let button = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + button;
+                            return this[buttonName] = document.querySelector(".copy-" + button);
+                        })
+                };
+
+                View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+                    return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+                            `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+                            `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+                        }
+        </fieldset>`;
+                };
+
+                View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+                    return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+                };
+
+                View.prototype.createResults = function () {
+                    return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .map(category => {
+                                let score = category.score;
+                                let title = category.title;
+                                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                                    .filter(messageObj => messageObj.category === category.title)
+                                    .map(messageObj => `* ${messageObj.message}\n`)
+                                    .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+                            })
+                            .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+                };
+
+                View.prototype.createHeader = function (title, isDarkMode = false) {
+                    return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
+                        }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+                };
+
+                View.prototype.generateHTML = function () {
+                    this.wrapper = document.querySelector("#app");
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    }
+
+                    const survey = this.model.categories.reduce((final, current) => {
+                        final += this.createCard(current);
+                        return final;
+                    }, "");
+
+                    const results = this.createResults();
+
+                    const header = this.createHeader(
+                        this.model.title,
+                        this.model.isDarkMode
+                    );
+
+                    this.wrapper.innerHTML = header + survey + results;
+                };
+
+                View.prototype.update = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.model.score));
+                    this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+                    const letterGrade = this.model.calcGrade();
+                    this.grade.textContent = letterGrade;
+
+                    if (this.model.hasFlags()) {
+                        this.alert.classList.remove("d-none");
+                    } else {
+                        this.alert.classList.add("d-none");
+                    }
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    } else {
+                        this.wrapper.classList.remove("dark");
+                    }
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+                            this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                                .filter(messageObj => messageObj.category === category.title)
+                                .map(messageObj => `* ${messageObj.message}\n`)
+                                .join("")
+                        })
+
+                };
+
+                function Controller(model, view) {
+                    this.model = model;
+                    this.view = view;
+                    this.addEventListeners();
+                }
+
+                Controller.prototype.addEventListeners = function () {
+                    this.view.wrapper.addEventListener("change", event => {
+                        const { target } = event;
+
+                        if (target.name !== "dark-mode") {
+                            if (!target.matches("input[type='radio']")) {
+                                this.model.handleSelect(target);
+                            } else {
+                                this.model.toggleRadio(target);
+                            }
+                        }
+
+                        this.view.update();
+                    });
+
+                    this.view.resetButton.addEventListener("click", () => {
+                        this.model.initStorage();
+
+                        this.view.radios.forEach(
+                            radio => (radio.checked = radio.hasAttribute("checked"))
+                        );
+
+                        this.view.selects.forEach(
+                            select => select.selectedIndex = null
+                        )
+
+                        this.model.questions
+                            .filter(question => question.category !== "Academic Integrity")
+                            .forEach(question => {
+                                const id = question.id;
+                                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                                questionScore.textContent = 0;
+                            });
+
+                        this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .forEach(category => {
+                                const title = category.title
+                                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                                categoryScore.textContent = 0;
+                                resultsCategoryScore.textContent = 0;
+                            });
+
+                        this.view.update();
+                        window.scrollTo(0, 0);
+                    });
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let title = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + title;
+
+                            let summaryName = title + "-summary";
+
+                            this.view[buttonName].addEventListener("click", () => {
+                                const selection = window.getSelection();
+                                const range = document.createRange();
+                                range.selectNodeContents(this.view[summaryName]);
+                                selection.removeAllRanges();
+                                selection.addRange(range);
+                                document.execCommand("copy");
+                            });
+                        });
+
+
+                    this.view.darkModeToggle.addEventListener("change", () => {
+                        this.model.toggleDarkMode();
+                        this.view.update();
+                    });
+                };
+
+                function App(Model, View, Controller) {
+                    this.model = new Model();
+                    this.view = new View(this.model);
+                    this.controller = new Controller(this.model, this.view);
+                }
+
+                return new App(Model, View, Controller);
+            })(window);
+    </script>
+</body>
+
+</html>

+ 1284 - 0
rubrics/mod17.html

@@ -0,0 +1,1284 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+    <title>Module 17 CS Challenge Rubric</title>
+    <style>
+        *,
+        *::before,
+        *::after {
+            box-sizing: border-box;
+        }
+
+        html,
+        body {
+            margin: 0;
+            padding: 0;
+            line-height: 1.15;
+            font-size: 100%;
+            color: #fefefe;
+            transition: all 0.4s;
+        }
+
+        body {
+            background-color: #fbfcfd;
+            font-family: "Inter", sans-serif;
+            color: #4a4a4a;
+        }
+
+        #app.dark {
+            background-color: #121212;
+            color: #e2e2e2;
+        }
+
+        h1 {
+            font-size: 1.9rem;
+        }
+
+        h2 {
+            font-size: 1.6rem;
+        }
+
+        h3 {
+            font-size: 1.2rem;
+        }
+
+        h4 {
+            font-size: 0.9rem;
+        }
+
+        p,
+        legend {
+            font-size: 1.2rem;
+        }
+
+        label {
+            font-size: 0.9rem;
+            line-height: 2;
+        }
+
+        fieldset {
+            border: none;
+            padding: 0;
+            margin-top: 0.9rem;
+        }
+
+        input[type="radio"],
+        input[type="checkbox"] {
+            position: absolute !important;
+            height: 1px;
+            width: 1px;
+            overflow: hidden;
+            clip: rect(1px 1px 1px 1px);
+            clip: rect(1px, 1px, 1px, 1px);
+            white-space: nowrap;
+        }
+
+        .select-lg,
+        .select-lg option {
+            font-family: "Inter", sans-serif;
+            padding: 10px;
+        }
+
+        .narrow {
+            width: 800px;
+        }
+
+        #app {
+            padding: 3%;
+            transition: all 0.4s;
+        }
+
+        #app h1 {
+            font-size: 1.9rem;
+            font-weight: 700;
+            line-height: 1.3;
+        }
+
+        header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .switch-group {
+            display: flex;
+            align-items: center;
+            height: 2.25rem;
+            width: 4.5rem;
+        }
+
+        input[type="checkbox"]+.toggle {
+            height: 100%;
+            width: 100%;
+            background-color: #77a1db;
+            border-radius: 1.9rem;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            padding: 0.35rem;
+            cursor: pointer;
+            overflow: hidden;
+            background-position: 25% 125%;
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]:focus+.toggle {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        input[type="checkbox"]:checked+.toggle {
+            background-color: rgb(85, 89, 204);
+            background-size: 1rem;
+            background-repeat: no-repeat;
+            background-position: 25%;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]+.toggle::after {
+            content: "";
+            display: block;
+            height: 1.7rem;
+            width: 1.7rem;
+            border-radius: 50%;
+            cursor: pointer;
+            border: 4px solid #e1be64;
+            background-color: #fddf74;
+            transform: translateX(0rem);
+            transition: all 0.5s;
+            background-position: center bottom;
+        }
+
+        input[type="checkbox"]:hover+.toggle::after {
+            transform: translateX(0rem) scale(1.1);
+        }
+
+        input[type="checkbox"]:checked+.toggle::after {
+            border: 4px solid #ffffff;
+            background-color: rgba(255, 255, 255, 0.4);
+            transform: translateX(2.1rem);
+            transition: all 0.5s;
+            background-position: center;
+            background-size: 60%;
+            background-repeat: no-repeat;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+        }
+
+        input[type="checkbox"]:checked:hover+.toggle::after {
+            transform: translateX(2.1rem) scale(1.075);
+        }
+
+        .card {
+            background-color: #fff;
+            border-radius: 6px;
+            border-width: 0;
+            box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+            font-size: 1.1rem;
+            height: 100%;
+            line-height: 1.8rem;
+            padding: 1.5rem 2rem;
+            width: 100%;
+            margin-bottom: 1.9rem;
+        }
+
+        .dark .card {
+            background-color: #333333;
+            box-shadow: none;
+        }
+
+        .card h1,
+        .card h2,
+        .card h3,
+        .card h4,
+        .card h5,
+        .card h6 {
+            color: #4a4a4a;
+        }
+
+        .dark .card h1,
+        .dark .card h2,
+        .dark .card h3,
+        .dark .card h4,
+        .dark .card h5,
+        .dark .card h6 {
+            color: #e2e2e2;
+        }
+
+        .card-header h2 {
+            display: inline-block;
+        }
+
+        .card .cat-score {
+            margin-left: 20px;
+            font-size: smaller;
+        }
+
+        .card h3 {
+            font-size: 1.2rem;
+            font-weight: 700;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card p,
+        .card label,
+        .card legend {
+            color: #7a869c;
+            border-width: 0;
+            display: block;
+            font-size: 1.1rem;
+            font-weight: 300;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card legend {
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+        }
+
+        .card legend .score {
+            font-size: smaller;
+            margin-left: 20px;
+            font-weight: 100;
+            min-width: 5%;
+        }
+
+        .dark .card p,
+        .dark .card label,
+        .dark .card legend {
+            color: #e2e2e2;
+        }
+
+        .card label {
+            display: inline-block;
+        }
+
+        .card input[type="radio"]+label::before {
+            content: "";
+            height: 0.9rem;
+            width: 0.9rem;
+            margin-right: 0.7rem;
+            border-radius: 50%;
+            border: 1px solid rgb(74, 74, 74);
+            background-color: #ffffff;
+            display: inline-block;
+            cursor: pointer;
+            position: relative;
+            top: 0.09rem;
+        }
+
+        .dark .card input[type="radio"]+label::before {
+            border: 1px solid #e2e2e2;
+            background-color: inherit;
+        }
+
+        .card input[type="radio"]:checked+label::before {
+            background-color: rgb(47, 106, 189);
+            border-color: rgb(47, 106, 189);
+        }
+
+        .dark .card input[type="radio"]:checked+label::before {
+            background-color: #d9d9d9;
+            border-color: #d9d9d9;
+        }
+
+        .card input[type="radio"]:checked+label {
+            font-weight: bold;
+            color: #4a4a4a;
+        }
+
+        .dark input[type="radio"]:checked+label {
+            color: #e2e2e2;
+        }
+
+        .card input[type="radio"]:focus+label::before {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        .card legend {
+            margin-bottom: 0.4rem;
+        }
+
+        .totals {
+            font-size: 1.6rem;
+        }
+
+        .totals+h2 {
+            font-size: 1.9rem;
+            margin-top: 0;
+        }
+
+        .grade {
+            font-size: 1.9rem;
+        }
+
+        button {
+            background-color: #fff;
+            border: 1px solid #4a4a4a;
+            border-radius: 6px;
+            box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+            color: #4a4a4a;
+            cursor: pointer;
+            display: inline-block;
+            font-size: 1rem;
+            font-weight: 500;
+            letter-spacing: 0.04em;
+            line-height: 1.8em;
+            margin: 0.75rem 1.1rem 1.1rem 0;
+            outline: none;
+            padding: 0.5rem 1.4rem;
+            text-transform: uppercase;
+            transition-delay: 0s;
+            transition-duration: 0.25s;
+            transition-property: all;
+            transition-timing-function: ease-out;
+            width: fit-content;
+        }
+
+        .dark button {
+            background: #d9d9d9;
+            color: #121212;
+            box-shadow: none;
+        }
+
+        button:hover {
+            border-color: #dbdbdb;
+            color: #dbdbdb;
+        }
+
+        .dark button:hover {
+            color: #121212;
+        }
+
+        button.copy {
+            margin-left: 0.4rem;
+            font-size: .8rem;
+        }
+
+        button:focus {
+            border-color: rgb(56, 56, 56);
+            box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+            color: rgb(56, 56, 56);
+        }
+
+        div[role="alert"] {
+            color: #4a4a4a;
+            font-size: 1.2rem;
+            margin-bottom: 1.2rem;
+        }
+
+        .dark div[role="alert"] {
+            color: #e2e2e2;
+        }
+
+        .summary {
+            font-size: 1.2rem;
+            border: none;
+            font-family: monospace;
+            background-color: #d5d9de;
+            padding: 0.9rem;
+            border-radius: 6px;
+            line-height: 1.5;
+            width: 100%;
+        }
+
+        .summary h2,
+        .dark .summary h2 {
+            font-size: 1.2rem;
+            font-family: "Inter", sans-serif;
+            margin-top: 35px;
+            margin-bottom: 5px;
+        }
+
+        .summary h2:first-of-type,
+        .dark .summary h2:first-of-type {
+            margin-top: 15px;
+        }
+
+        .dark .summary {
+            background: #6b6a6a;
+            color: #e2e2e2;
+        }
+
+        .summary pre,
+        .dark .summary pre {
+            white-space: pre-wrap;
+            border-radius: 5px;
+            padding: 15px;
+            border: solid 2px rgba(0, 0, 0, .4);
+        }
+
+        .summary pre:empty {
+            display: none;
+        }
+
+        .summary pre:empty+button {
+            display: none;
+        }
+
+        .d-none {
+            display: none;
+        }
+
+        @media (max-width: 450px) {
+            .btn-container {
+                text-align: center;
+            }
+
+            button {
+                width: 100%;
+                margin: 0.5rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+                background-size: 50%;
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+                background-size: 50%;
+            }
+        }
+
+        @media (max-width: 515px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.7rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.7rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 460px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.4rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.4rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 400px) {
+            #app h1 {
+                font-size: 1.6rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 340px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(0.9rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(0.9rem) scale(1.075);
+            }
+        }
+    </style>
+    <script>
+        $(function () {
+            let $select2 = $('.select2').select2({
+                containerCssClass: "wrap"
+            })
+
+            function triggerEvent(name, val) {
+                let selected = $("select[name='" + name + "']")
+                let change = new Event('change', { bubbles: true })
+                selected[0].dispatchEvent(change)
+            }
+
+            $('.select2').on('select2:select', function (e) {
+                triggerEvent(e.target.name, e.target.value)
+            });
+        })
+    </script>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script>
+            (function (window) {
+                "use strict";
+
+                function Model() {
+                    this.title = `Module 17 CS Challenge Rubric (Coding)`;
+                    this.categories = [
+                        {
+                            title: "Deliverables",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Student submitted GitHub gist URL",
+                                    score: 15,
+                                    type: "true false",
+                                    message: "Missing GitHub gist submission URL",
+                                    status: false
+                                },
+                                {
+                                    title: "GitHub gist contains tutorial Markdown",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "GitHub gist is empty",
+                                    status: false
+                                },
+                                {
+                                    title: "GitHub gist renders as Markdown in the browser",
+                                    score: 7,
+                                    type: "true false",
+                                    message: "GitHub gist doesn't render as Markdown in the broswer",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Technical Acceptance Criteria",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title:
+                                        "Student chooses a valid regex other than the one covered in the Regex Tutorial blog post (Matching a Username)",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Student did not choose a valid regex or chose the regex covered in the Regex Tutorial blog post (Matching a Username)",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Tutorial includes a unique name specific to the tutorial",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Tutorial doesn't include a unique name specific to the tutorial",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Tutorial includes an introductory paragraph explaining the purpose of the tutorial",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Tutorial doesn't include an introductory paragraph explaining the purpose of the tutorial",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Tutorial includes a summary describing the regex featured in the tutorial",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Tutorial doesn't include a summary describing the regex featured in the tutorial",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Tutorial includes a functioning table of contents linking to different sections that break down each component of the regex and explain what it does",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Tutorial doesn't include a functioning table of contents linking to different sections that break down each component of the regex and explain what it does",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Tutorial includes a section at the end of the tutorial about the author with a link to the author’s GitHub profile",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Tutorial doesn't include a section at the end of the tutorial about the author with a link to the author’s GitHub profile",
+                                    status: false
+                                },
+                                {
+                                    title: "Tutorial includes sections that correspond to each of the components that make up the regex that they chose",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Tutorial omits three or more of the components that make up the regex that they chose",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Tutorial omits two of the components that make up the regex that they chose",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Tutorial omits one of the components that make up the regex that they chose",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Tutorial includes sections that correspond to each of the components that make up the regex that they chose",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Each section of the tutorial that describes a different component of the regex includes more than just one sentence explaining what it does",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Three or more sections of the tutorial that describe a different component of the regex only include one sentence explaining what it does",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Two sections of the tutorial that describe a different component of the regex only include one sentence explaining what it does",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "One section of the tutorial that describes a different component of the regex only includes one sentence explaining what it does",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Each section of the tutorial that describes a different component of the regex includes more than just one sentence explaining what it does",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Each section of the tutorial that describes a different component of the regex includes a code snippet of that particular component of the regex",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Three or more sections of the tutorial that describe a different component of the regex don't include a code snippet of that particular component of the regex",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Two sections of the tutorial that describe a different component of the regex don't include a code snippet of that particular component of the regex",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "One section of the tutorial that describes a different component of the regex doesn't include a code snippet of that particular component of the regex",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Each section of the tutorial that describes a different component of the regex includes a code snippet of that particular component of the regex",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Each section of the tutorial that describes a different component of the regex includes at least one example that would meet the requirements of that component",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Three or more sections of the tutorial that describe a different component of the regex don't include at least one example that would meet the requirements of that component",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Two sections of the tutorial that describe a different component of the regex don't include at least one example that would meet the requirements of that component",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "One section of the tutorial that describes a different component of the regex doesn't include at least one example that would meet the requirements of that component",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Each section of the tutorial that describes a different component of the regex includes at least one example that would meet the requirements of that component",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Tutorial Clarity and Quality",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Student provides a clear explanation of how this regex works",
+                                    score: 10,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Tutorial clarity is poor",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Tutorial clarity is fair",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Tutorial clarity is good",
+                                            score: 7
+                                        },
+                                        {
+                                            message: "Tutorial clarity is excellent",
+                                            score: 10
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Student describes each component of the regex in a separate section",
+                                    score: 10,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Student combines the description of three or more of the components in one section",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Student combines the description of two of the components in one section",
+                                            score: 5
+                                        },
+                                        {
+                                            message: "Student describes each component of the regex in a separate section",
+                                            score: 10
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Academic Integrity",
+                            subtitle:
+                                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+                            score: 0,
+                            questions: [
+                                {
+                                    title:
+                                        "Tutorial uses regex covered in the Regex Tutorial blog post (Matching a Username)",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title: "Gist revision history contains a low number of edits or very large additions (copy/paste)",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Tutorial appears advanced for the student at this point in the course, or in general",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                }
+                            ]
+                        }
+                    ];
+
+                    this.initStorage();
+                }
+
+                Model.prototype.initStorage = function () {
+                    this.score = 0;
+                    this.questions = [];
+                    this.messages = new Set();
+                    this.flagged = new Set();
+                    this.isDarkMode =
+                        JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+                    this.categories
+                        .forEach(category => {
+                            category.score = 0;
+                            category.questions.forEach(question => {
+                                question.category = category.title;
+                                question.status = false;
+                                question.id = this.questions.length;
+                                question.flagged = category.title === "Academic Integrity";
+                                question.bonus = category.title === "Bonus";
+                                if (question.type === "likert") {
+                                    question.message = question.options[0].message.split(" - ").pop();
+                                };
+
+                                let messageObj = {
+                                    message: question.message,
+                                    category: question.category
+                                };
+
+                                question.message && !question.bonus && this.messages.add(messageObj);
+                                this.questions.push(question);
+                                category.score += question.score;
+                            });
+                        });
+                };
+
+                Model.prototype.toggleDarkMode = function () {
+                    this.isDarkMode = !this.isDarkMode;
+                    localStorage.setItem("isDarkMode", this.isDarkMode);
+                };
+
+                Model.prototype.toggleRadio = function (radio) {
+                    const id = Number(radio.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+                    question.status = !question.status;
+
+                    if (question.status === true) {
+
+                        if (!question.bonus) {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        } else {
+                            let messageObj = {
+                                message: question.message,
+                                category: category
+                            }
+                            this.messages.add(messageObj);
+                        }
+
+                        this.score += question.score;
+
+                        if (question.flagged) {
+                            this.flagged.add(question.id);
+                        }
+                    } else {
+                        if (question.flagged) {
+                            this.flagged.delete(question.id);
+                        }
+
+                        if (!question.bonus) {
+                            if (question.message) {
+                                let messageObj = {
+                                    message: question.message,
+                                    category: category
+                                }
+                                this.messages.add(messageObj);
+                            }
+                        } else {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        }
+
+                        this.score -= question.score;
+                    }
+
+                    if (category !== "Academic Integrity") {
+                        if (question.status) {
+                            questionScore.textContent = question.score;
+                        } else {
+                            questionScore.textContent = 0;
+                        }
+
+                        const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                        let categoryTotal = 0;
+
+                        catQuestions.forEach(question => {
+                            if (question.type === "true false") {
+                                if (question.status) {
+                                    categoryTotal += question.score;
+                                }
+                            } else {
+                                let score = document.querySelector("select[name='" + question.id + "']").value;
+                                categoryTotal += parseInt(score);
+                            }
+                        })
+
+                        categoryScore.textContent = categoryTotal;
+                        resultsCategoryScore.textContent = categoryTotal;
+                    }
+
+                };
+
+                Model.prototype.handleSelect = function (select) {
+                    const id = Number(select.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const oldScore = questionScore.textContent;
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+                    let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+                    if (newMessage === "Open this menu") {
+                        newMessage = select[1].text.split(" - ").pop();
+                    };
+
+                    if (question.type === "likert") {
+
+                        this.messages.forEach(item => {
+                            if (item.message === question.message) {
+                                this.messages.delete(item)
+                            }
+                        })
+
+                        question.message = newMessage;
+
+                        let messageObj = {
+                            message: newMessage,
+                            category: category
+                        }
+
+                        this.messages.add(messageObj);
+
+                    };
+
+
+
+                    this.score -= parseInt(oldScore);
+                    this.score += parseInt(select.value);
+                    questionScore.textContent = select.value;
+
+                    const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                    let categoryTotal = 0;
+
+                    catQuestions.forEach(question => {
+                        if (question.type === "true false") {
+                            if (question.status) {
+                                categoryTotal += question.score;
+                            }
+                        } else {
+                            let score = document.querySelector("select[name='" + question.id + "']").value;
+                            categoryTotal += parseInt(score);
+                        }
+                    })
+
+                    categoryScore.textContent = categoryTotal;
+                    resultsCategoryScore.textContent = categoryTotal;
+
+                };
+
+                Model.prototype.calcGrade = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.score));
+
+                    let letterGrade = "";
+
+                    if (roundedScore >= 99.5) {
+                        letterGrade = "A+";
+                    } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+                        letterGrade = "A";
+                    } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+                        letterGrade = "A-";
+                    } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+                        letterGrade = "B+";
+                    } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+                        letterGrade = "B";
+                    } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+                        letterGrade = "B-";
+                    } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+                        letterGrade = "C+";
+                    } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+                        letterGrade = "C";
+                    } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+                        letterGrade = "C-";
+                    } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+                        letterGrade = "D+";
+                    } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+                        letterGrade = "D";
+                    } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+                        letterGrade = "D-";
+                    } else {
+                        letterGrade = "F";
+                    }
+
+                    return letterGrade;
+                };
+
+                Model.prototype.hasFlags = function () {
+                    return this.flagged.size > 0;
+                };
+
+                function View(model) {
+                    this.model = model;
+                    this.generateHTML();
+                    this.cacheDom();
+                }
+
+                View.prototype.cacheDom = function () {
+                    this.darkModeToggle = document.querySelector("#dark-mode");
+
+                    this.radios = Array.from(
+                        document.querySelectorAll("input[type='radio']")
+                    );
+
+                    this.selects = Array.from(
+                        document.querySelectorAll("select")
+                    );
+
+                    this.totals = document.querySelector(".totals");
+                    this.grade = document.querySelector(".grade");
+                    this.alert = document.querySelector("div[role='alert']");
+                    this.summaryHeading = document.querySelector(".summary-heading");
+                    this.summary = document.querySelector(".summary");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let summaryName = summary + "-summary";
+                            return this[summaryName] = document.querySelector("." + summary);
+                        })
+
+                    this.resetButton = document.querySelector(".reset");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let button = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + button;
+                            return this[buttonName] = document.querySelector(".copy-" + button);
+                        })
+                };
+
+                View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+                    return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+                            `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+                            `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+                        }
+        </fieldset>`;
+                };
+
+                View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+                    return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+                };
+
+                View.prototype.createResults = function () {
+                    return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .map(category => {
+                                let score = category.score;
+                                let title = category.title;
+                                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                                    .filter(messageObj => messageObj.category === category.title)
+                                    .map(messageObj => `* ${messageObj.message}\n`)
+                                    .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+                            })
+                            .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+                };
+
+                View.prototype.createHeader = function (title, isDarkMode = false) {
+                    return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
+                        }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+                };
+
+                View.prototype.generateHTML = function () {
+                    this.wrapper = document.querySelector("#app");
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    }
+
+                    const survey = this.model.categories.reduce((final, current) => {
+                        final += this.createCard(current);
+                        return final;
+                    }, "");
+
+                    const results = this.createResults();
+
+                    const header = this.createHeader(
+                        this.model.title,
+                        this.model.isDarkMode
+                    );
+
+                    this.wrapper.innerHTML = header + survey + results;
+                };
+
+                View.prototype.update = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.model.score));
+                    this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+                    const letterGrade = this.model.calcGrade();
+                    this.grade.textContent = letterGrade;
+
+                    if (this.model.hasFlags()) {
+                        this.alert.classList.remove("d-none");
+                    } else {
+                        this.alert.classList.add("d-none");
+                    }
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    } else {
+                        this.wrapper.classList.remove("dark");
+                    }
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+                            this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                                .filter(messageObj => messageObj.category === category.title)
+                                .map(messageObj => `* ${messageObj.message}\n`)
+                                .join("")
+                        })
+
+                };
+
+                function Controller(model, view) {
+                    this.model = model;
+                    this.view = view;
+                    this.addEventListeners();
+                }
+
+                Controller.prototype.addEventListeners = function () {
+                    this.view.wrapper.addEventListener("change", event => {
+                        const { target } = event;
+
+                        if (target.name !== "dark-mode") {
+                            if (!target.matches("input[type='radio']")) {
+                                this.model.handleSelect(target);
+                            } else {
+                                this.model.toggleRadio(target);
+                            }
+                        }
+
+                        this.view.update();
+                    });
+
+                    this.view.resetButton.addEventListener("click", () => {
+                        this.model.initStorage();
+
+                        this.view.radios.forEach(
+                            radio => (radio.checked = radio.hasAttribute("checked"))
+                        );
+
+                        this.view.selects.forEach(
+                            select => select.selectedIndex = null
+                        )
+
+                        this.model.questions
+                            .filter(question => question.category !== "Academic Integrity")
+                            .forEach(question => {
+                                const id = question.id;
+                                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                                questionScore.textContent = 0;
+                            });
+
+                        this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .forEach(category => {
+                                const title = category.title
+                                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                                categoryScore.textContent = 0;
+                                resultsCategoryScore.textContent = 0;
+                            });
+
+                        this.view.update();
+                        window.scrollTo(0, 0);
+                    });
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let title = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + title;
+
+                            let summaryName = title + "-summary";
+
+                            this.view[buttonName].addEventListener("click", () => {
+                                const selection = window.getSelection();
+                                const range = document.createRange();
+                                range.selectNodeContents(this.view[summaryName]);
+                                selection.removeAllRanges();
+                                selection.addRange(range);
+                                document.execCommand("copy");
+                            });
+                        });
+
+
+                    this.view.darkModeToggle.addEventListener("change", () => {
+                        this.model.toggleDarkMode();
+                        this.view.update();
+                    });
+                };
+
+                function App(Model, View, Controller) {
+                    this.model = new Model();
+                    this.view = new View(this.model);
+                    this.controller = new Controller(this.model, this.view);
+                }
+
+                return new App(Model, View, Controller);
+            })(window);
+    </script>
+</body>
+
+</html>

+ 1588 - 0
rubrics/mod18.html

@@ -0,0 +1,1588 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link
+      href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css"
+      rel="stylesheet"
+    />
+    <title>Module 18 NoSQL Challenge Rubric</title>
+    <style>
+      *,
+      *::before,
+      *::after {
+        box-sizing: border-box;
+      }
+
+      html,
+      body {
+        margin: 0;
+        padding: 0;
+        line-height: 1.15;
+        font-size: 100%;
+        color: #fefefe;
+        transition: all 0.4s;
+      }
+
+      body {
+        background-color: #fbfcfd;
+        font-family: 'Inter', sans-serif;
+        color: #4a4a4a;
+      }
+
+      #app.dark {
+        background-color: #121212;
+        color: #e2e2e2;
+      }
+
+      h1 {
+        font-size: 1.9rem;
+      }
+
+      h2 {
+        font-size: 1.6rem;
+      }
+
+      h3 {
+        font-size: 1.2rem;
+      }
+
+      h4 {
+        font-size: 0.9rem;
+      }
+
+      p,
+      legend {
+        font-size: 1.2rem;
+      }
+
+      label {
+        font-size: 0.9rem;
+        line-height: 2;
+      }
+
+      fieldset {
+        border: none;
+        padding: 0;
+        margin-top: 0.9rem;
+      }
+
+      input[type='radio'],
+      input[type='checkbox'] {
+        position: absolute !important;
+        height: 1px;
+        width: 1px;
+        overflow: hidden;
+        clip: rect(1px 1px 1px 1px);
+        clip: rect(1px, 1px, 1px, 1px);
+        white-space: nowrap;
+      }
+
+      .select-lg,
+      .select-lg option {
+        font-family: 'Inter', sans-serif;
+        padding: 10px;
+      }
+
+      .narrow {
+        width: 800px;
+      }
+
+      #app {
+        padding: 3%;
+        transition: all 0.4s;
+      }
+
+      #app h1 {
+        font-size: 1.9rem;
+        font-weight: 700;
+        line-height: 1.3;
+      }
+
+      header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+      }
+
+      .switch-group {
+        display: flex;
+        align-items: center;
+        height: 2.25rem;
+        width: 4.5rem;
+      }
+
+      input[type='checkbox'] + .toggle {
+        height: 100%;
+        width: 100%;
+        background-color: #77a1db;
+        border-radius: 1.9rem;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        padding: 0.35rem;
+        cursor: pointer;
+        overflow: hidden;
+        background-position: 25% 125%;
+        transition: all 0.5s;
+      }
+
+      input[type='checkbox']:focus + .toggle {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      input[type='checkbox']:checked + .toggle {
+        background-color: rgb(85, 89, 204);
+        background-size: 1rem;
+        background-repeat: no-repeat;
+        background-position: 25%;
+        background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==');
+        transition: all 0.5s;
+      }
+
+      input[type='checkbox'] + .toggle::after {
+        content: '';
+        display: block;
+        height: 1.7rem;
+        width: 1.7rem;
+        border-radius: 50%;
+        cursor: pointer;
+        border: 4px solid #e1be64;
+        background-color: #fddf74;
+        transform: translateX(0rem);
+        transition: all 0.5s;
+        background-position: center bottom;
+      }
+
+      input[type='checkbox']:hover + .toggle::after {
+        transform: translateX(0rem) scale(1.1);
+      }
+
+      input[type='checkbox']:checked + .toggle::after {
+        border: 4px solid #ffffff;
+        background-color: rgba(255, 255, 255, 0.4);
+        transform: translateX(2.1rem);
+        transition: all 0.5s;
+        background-position: center;
+        background-size: 60%;
+        background-repeat: no-repeat;
+        background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
+      }
+
+      input[type='checkbox']:checked:hover + .toggle::after {
+        transform: translateX(2.1rem) scale(1.075);
+      }
+
+      .card {
+        background-color: #fff;
+        border-radius: 6px;
+        border-width: 0;
+        box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+        font-size: 1.1rem;
+        height: 100%;
+        line-height: 1.8rem;
+        padding: 1.5rem 2rem;
+        width: 100%;
+        margin-bottom: 1.9rem;
+      }
+
+      .dark .card {
+        background-color: #333333;
+        box-shadow: none;
+      }
+
+      .card h1,
+      .card h2,
+      .card h3,
+      .card h4,
+      .card h5,
+      .card h6 {
+        color: #4a4a4a;
+      }
+
+      .dark .card h1,
+      .dark .card h2,
+      .dark .card h3,
+      .dark .card h4,
+      .dark .card h5,
+      .dark .card h6 {
+        color: #e2e2e2;
+      }
+
+      .card-header h2 {
+        display: inline-block;
+      }
+
+      .card .cat-score {
+        margin-left: 20px;
+        font-size: smaller;
+      }
+
+      .card h3 {
+        font-size: 1.2rem;
+        font-weight: 700;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card p,
+      .card label,
+      .card legend {
+        color: #7a869c;
+        border-width: 0;
+        display: block;
+        font-size: 1.1rem;
+        font-weight: 300;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card legend {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+      }
+
+      .card legend .score {
+        font-size: smaller;
+        margin-left: 20px;
+        font-weight: 100;
+        min-width: 5%;
+      }
+
+      .dark .card p,
+      .dark .card label,
+      .dark .card legend {
+        color: #e2e2e2;
+      }
+
+      .card label {
+        display: inline-block;
+      }
+
+      .card input[type='radio'] + label::before {
+        content: '';
+        height: 0.9rem;
+        width: 0.9rem;
+        margin-right: 0.7rem;
+        border-radius: 50%;
+        border: 1px solid rgb(74, 74, 74);
+        background-color: #ffffff;
+        display: inline-block;
+        cursor: pointer;
+        position: relative;
+        top: 0.09rem;
+      }
+
+      .dark .card input[type='radio'] + label::before {
+        border: 1px solid #e2e2e2;
+        background-color: inherit;
+      }
+
+      .card input[type='radio']:checked + label::before {
+        background-color: rgb(47, 106, 189);
+        border-color: rgb(47, 106, 189);
+      }
+
+      .dark .card input[type='radio']:checked + label::before {
+        background-color: #d9d9d9;
+        border-color: #d9d9d9;
+      }
+
+      .card input[type='radio']:checked + label {
+        font-weight: bold;
+        color: #4a4a4a;
+      }
+
+      .dark input[type='radio']:checked + label {
+        color: #e2e2e2;
+      }
+
+      .card input[type='radio']:focus + label::before {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      .card legend {
+        margin-bottom: 0.4rem;
+      }
+
+      .totals {
+        font-size: 1.6rem;
+      }
+
+      .totals + h2 {
+        font-size: 1.9rem;
+        margin-top: 0;
+      }
+
+      .grade {
+        font-size: 1.9rem;
+      }
+
+      button {
+        background-color: #fff;
+        border: 1px solid #4a4a4a;
+        border-radius: 6px;
+        box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+        color: #4a4a4a;
+        cursor: pointer;
+        display: inline-block;
+        font-size: 1rem;
+        font-weight: 500;
+        letter-spacing: 0.04em;
+        line-height: 1.8em;
+        margin: 0.75rem 1.1rem 1.1rem 0;
+        outline: none;
+        padding: 0.5rem 1.4rem;
+        text-transform: uppercase;
+        transition-delay: 0s;
+        transition-duration: 0.25s;
+        transition-property: all;
+        transition-timing-function: ease-out;
+        width: fit-content;
+      }
+
+      .dark button {
+        background: #d9d9d9;
+        color: #121212;
+        box-shadow: none;
+      }
+
+      button:hover {
+        border-color: #dbdbdb;
+        color: #dbdbdb;
+      }
+
+      .dark button:hover {
+        color: #121212;
+      }
+
+      button.copy {
+        margin-left: 0.4rem;
+        font-size: 0.8rem;
+      }
+
+      button:focus {
+        border-color: rgb(56, 56, 56);
+        box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+        color: rgb(56, 56, 56);
+      }
+
+      div[role='alert'] {
+        color: #4a4a4a;
+        font-size: 1.2rem;
+        margin-bottom: 1.2rem;
+      }
+
+      .dark div[role='alert'] {
+        color: #e2e2e2;
+      }
+
+      .summary {
+        font-size: 1.2rem;
+        border: none;
+        font-family: monospace;
+        background-color: #d5d9de;
+        padding: 0.9rem;
+        border-radius: 6px;
+        line-height: 1.5;
+        width: 100%;
+      }
+
+      .summary h2,
+      .dark .summary h2 {
+        font-size: 1.2rem;
+        font-family: 'Inter', sans-serif;
+        margin-top: 35px;
+        margin-bottom: 5px;
+      }
+
+      .summary h2:first-of-type,
+      .dark .summary h2:first-of-type {
+        margin-top: 15px;
+      }
+
+      .dark .summary {
+        background: #6b6a6a;
+        color: #e2e2e2;
+      }
+
+      .summary pre,
+      .dark .summary pre {
+        white-space: pre-wrap;
+        border-radius: 5px;
+        padding: 15px;
+        border: solid 2px rgba(0, 0, 0, 0.4);
+      }
+
+      .summary pre:empty {
+        display: none;
+      }
+
+      .summary pre:empty + button {
+        display: none;
+      }
+
+      .d-none {
+        display: none;
+      }
+
+      @media (max-width: 450px) {
+        .btn-container {
+          text-align: center;
+        }
+
+        button {
+          width: 100%;
+          margin: 0.5rem;
+        }
+
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.3rem);
+          background-size: 50%;
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+          background-size: 50%;
+        }
+      }
+
+      @media (max-width: 515px) {
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.7rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.7rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 460px) {
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.4rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.4rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 400px) {
+        #app h1 {
+          font-size: 1.6rem;
+        }
+
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.3rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 340px) {
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(0.9rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(0.9rem) scale(1.075);
+        }
+      }
+    </style>
+    <script>
+      $(function () {
+        let $select2 = $('.select2').select2({
+          containerCssClass: 'wrap',
+        });
+
+        function triggerEvent(name, val) {
+          let selected = $("select[name='" + name + "']");
+          let change = new Event('change', { bubbles: true });
+          selected[0].dispatchEvent(change);
+        }
+
+        $('.select2').on('select2:select', function (e) {
+          triggerEvent(e.target.name, e.target.value);
+        });
+      });
+    </script>
+  </head>
+
+  <body>
+    <div id="app"></div>
+    <script>
+      (function (window) {
+        'use strict';
+
+        function Model() {
+          this.title = `Module 18 NoSQL Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: 'Deliverables',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title:
+                    'Student submitted GitHub Repo containing application code',
+                  score: 10,
+                  type: 'true false',
+                  message: 'GitHub repo is empty',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Walkthrough Video',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: 'Student includes link to walkthrough video in README',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    'Student does not include link to walkthrough video in README',
+                  status: false,
+                },
+                {
+                  title:
+                    'Walkthrough video shows all of the technical acceptance criteria being met',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    "Walkthrough video doesn't show all of the technical acceptance criteria being met",
+                  status: false,
+                },
+                {
+                  title:
+                    'Walkthrough video demonstrates how to start the server',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    'Walkthrough video does not demonstrate how to start the server',
+                  status: false,
+                },
+                {
+                  title:
+                    'Walkthrough video demonstrates GET routes for all users and all thoughts being tested in Insomnia',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'Walkthrough video does not demonstrate GET routes for all users and all thoughts being tested in Insomnia',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates GET routes for 1 of the 2 following being tested in Insomnia: all users or all thoughts',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates GET routes for all users and all thoughts being tested in Insomnia',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Walkthrough video demonstrates GET routes for a single user and a single thought being tested in Insomnia',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'Walkthrough video does not demonstrate GET routes for a single user and a single thought being tested in Insomnia',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates GET routes for 1 of the 2 following being tested in Insomnia: a single user or a single thought',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates GET routes for a single user and a single thought being tested in Insomnia',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Walkthrough video demonstrates POST, PUT, and DELETE routes for users and thoughts being tested in Insomnia',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'Walkthrough video does not demonstrate POST, PUT, and DELETE routes for users and thoughts being tested in Insomnia',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates POST, PUT, and DELETE routes for 1 of the 2 following being tested in Insomnia: users or thoughts',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates POST, PUT, and DELETE routes for users and thoughts being tested in Insomnia',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Walkthrough video demonstrates POST and DELETE routes for a user’s friend list being tested in Insomnia',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'Walkthrough video does not demonstrate POST and DELETE routes for a user’s friend list being tested in Insomnia',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates 1 of the 2 following being tested in Insomnia: POST or DELETE routes for a user’s friend list',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates POST and DELETE routes for a user’s friend list being tested in Insomnia',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Walkthrough video demonstrates POST and DELETE routes for reactions to thoughts being tested in Insomnia',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'Walkthrough video does not demonstrate POST and DELETE routes for reactions to thoughts being tested in Insomnia',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates 1 of the 2 following being tested in Insomnia: POST or DELETE routes for reactions to thoughts',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Walkthrough video demonstrates POST and DELETE routes for reactions to thoughts being tested in Insomnia',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Technical Acceptance Criteria',
+              subtitle:
+                'Watch out for errors in the console from this point forward.',
+              score: 0,
+              questions: [
+                {
+                  title:
+                    'Application includes User and Thought models that match Challenge instructions',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Application doesn't include User and Thought models that match Challenge instructions",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application includes 1 of the 2 following: User or Thought models that match Challenge instructions',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application includes User and Thought models that match Challenge instructions',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Application includes schema settings for User and Thought models as outlined in the Challenge instructions',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Application doesn't include schema settings for User and Thought models as outlined in the Challenge instructions",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application includes schema settings as outlined in the Challenge instructions for 1 of the 2 following: User or Thought models',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application includes schema settings for User and Thought models as outlined in the Challenge instructions',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    "Application includes Reactions as the reaction field's subdocument schema in the Thought model",
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    "Application doesn't include Reactions as the reaction field's subdocument schema in the Thought model",
+                  status: false,
+                },
+                {
+                  title:
+                    'Application includes GET routes that return all users and all thoughts',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Application doesn't include GET routes that return all users and all thoughts",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application includes GET routes that return 1 of the 2 following: all users or all thoughts',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application includes GET routes that return all users and all thoughts',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Application includes GET routes that return a single user and a single thought',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Application doesn't include GET routes that return a single user and a single thought",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application includes GET routes that return 1 of the 2 following: a single user or a single thought',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application includes GET routes that return a single user and a single thought',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    "Application includes POST routes that can create a new user, a new thought, a reaction to a thought, and add a friend to a user's friend list",
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Application doesn't include POST routes that can create a new user, a new thought, a reaction to a thought, or add a friend to a user's friend list",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        "Application includes POST routes that can do 1 of the 4 following: create a new user, create a new thought, create a reaction to a thought, or add a friend to a user's friend list",
+                      score: 2,
+                    },
+                    {
+                      message:
+                        "Application includes POST routes that can do 2 of the 4 following: create a new user, create a new thought, create a reaction to a thought, or add a friend to a user's friend list",
+                      score: 3,
+                    },
+                    {
+                      message:
+                        "Application includes POST routes that can do 3 of the 4 following: create a new user, create a new thought, create a reaction to a thought, or add a friend to a user's friend list",
+                      score: 4,
+                    },
+                    {
+                      message:
+                        "Application includes POST routes that can create a new user, a new thought, a reaction to a thought, and add a friend to a user's friend list",
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Application includes PUT routes that can update a user or a thought',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Application doesn't include PUT routes that can update a user or a thought",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application includes PUT routes that can update 1 of the 2 following: a user or a thought',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application includes PUT routes that can update a user or a thought',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    "Application includes DELETE routes that can delete a user, a thought, a reaction to a thought, and remove a friend from a user's friend list",
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Application doesn't include DELETE routes that can delete a user, a thought, a reaction to a thought, or a friend from a user's friend list",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        "Application includes DELETE routes that can do 1 of the 4 following: delete a user, delete a thought, delete a reaction to a thought, or remove a friend from a user's friend list",
+                      score: 2,
+                    },
+                    {
+                      message:
+                        "Application includes DELETE routes that can do 2 of the 4 following: delete a user, delete a thought, delete a reaction to a thought, or remove a friend from a user's friend list",
+                      score: 3,
+                    },
+                    {
+                      message:
+                        "Application includes DELETE routes that can do 3 of the 4 following: delete a user, delete a thought, delete a reaction to a thought, or remove a friend from a user's friend list",
+                      score: 4,
+                    },
+                    {
+                      message:
+                        "Application includes DELETE routes that can delete a user, a thought, a reaction to a thought, and remove a friend from a user's friend list",
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Repository Quality',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: 'Repository has a unique name',
+                  score: 2,
+                  type: 'true false',
+                  message: "Repo doesn't have a unique name",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository follows best practices for file structure and naming conventions',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository follows best practices for variable and function naming conventions, indentation, quality comments, etc.',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    "Code doesn't follow good practices for variable names, indentation, lacks quality comments",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository contains multiple descriptive commit messages',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository contains quality README with description and a link to walkthrough video',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Repo doesn't contain a quality README with description and a link to walkthrough video",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Repository README is missing either the description or the link to the walkthrough video',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Repository contains quality README with description and a link to walkthrough video',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Bonus',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title:
+                    "Application removes a user's associated thoughts when the user is deleted",
+                  score: 10,
+                  type: 'true false',
+                  message:
+                    "Application removes a user's associated thoughts when the user is deleted",
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Academic Integrity',
+              subtitle:
+                'If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.',
+              score: 0,
+              questions: [
+                {
+                  title: 'Application contains a low number of commits',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+                {
+                  title:
+                    'Application codebase appears advanced for the student, at this point in the course, or in general',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+                {
+                  title:
+                    'Application package.json file references a different author and/or repository',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+                {
+                  title:
+                    'Application package.json file references much older versions of npm packages',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+              ],
+            },
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem('isDarkMode')) || false;
+
+          this.categories.forEach((category) => {
+            category.score = 0;
+            category.questions.forEach((question) => {
+              question.category = category.title;
+              question.status = false;
+              question.id = this.questions.length;
+              question.flagged = category.title === 'Academic Integrity';
+              question.bonus = category.title === 'Bonus';
+              if (question.type === 'likert') {
+                question.message = question.options[0].message
+                  .split(' - ')
+                  .pop();
+              }
+
+              let messageObj = {
+                message: question.message,
+                category: question.category,
+              };
+
+              question.message &&
+                !question.bonus &&
+                this.messages.add(messageObj);
+              this.questions.push(question);
+              category.score += question.score;
+            });
+          });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem('isDarkMode', this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute('name'));
+          const question = this.questions[id];
+          const questionScore = document.querySelector(
+            "span[data-id='" + id + "']"
+          );
+          const category = question.category;
+          const categoryScore = document.querySelector(
+            "span[data-cat='" + category + "']"
+          );
+          const resultsCategoryScore = document.querySelector(
+            "span[data-res-cat='" + category + "']"
+          );
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+            if (!question.bonus) {
+              this.messages.forEach((message) => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              });
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category,
+              };
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category,
+                };
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach((message) => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              });
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== 'Academic Integrity') {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(
+              (cat) => cat.title === category
+            )[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach((question) => {
+              if (question.type === 'true false') {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector(
+                  "select[name='" + question.id + "']"
+                ).value;
+                categoryTotal += parseInt(score);
+              }
+            });
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute('name'));
+          const question = this.questions[id];
+          const questionScore = document.querySelector(
+            "span[data-id='" + id + "']"
+          );
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector(
+            "span[data-cat='" + category + "']"
+          );
+          const resultsCategoryScore = document.querySelector(
+            "span[data-res-cat='" + category + "']"
+          );
+          let newMessage = select.selectedOptions[0].text.split(' - ').pop();
+
+          if (newMessage === 'Open this menu') {
+            newMessage = select[1].text.split(' - ').pop();
+          }
+
+          if (question.type === 'likert') {
+            this.messages.forEach((item) => {
+              if (item.message === question.message) {
+                this.messages.delete(item);
+              }
+            });
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category,
+            };
+
+            this.messages.add(messageObj);
+          }
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(
+            (cat) => cat.title === category
+          )[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach((question) => {
+            if (question.type === 'true false') {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector(
+                "select[name='" + question.id + "']"
+              ).value;
+              categoryTotal += parseInt(score);
+            }
+          });
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = '';
+
+          if (roundedScore >= 97) {
+            letterGrade = 'A+';
+          } else if (roundedScore >= 93 && roundedScore <= 96) {
+            letterGrade = 'A';
+          } else if (roundedScore >= 90 && roundedScore <= 92) {
+            letterGrade = 'A-';
+          } else if (roundedScore >= 87 && roundedScore <= 89) {
+            letterGrade = 'B+';
+          } else if (roundedScore >= 83 && roundedScore <= 86) {
+            letterGrade = 'B';
+          } else if (roundedScore >= 80 && roundedScore <= 82) {
+            letterGrade = 'B-';
+          } else if (roundedScore >= 77 && roundedScore <= 79) {
+            letterGrade = 'C+';
+          } else if (roundedScore >= 73 && roundedScore <= 76) {
+            letterGrade = 'C';
+          } else if (roundedScore >= 70 && roundedScore <= 72) {
+            letterGrade = 'C-';
+          } else if (roundedScore >= 67 && roundedScore <= 69) {
+            letterGrade = 'D+';
+          } else if (roundedScore >= 63 && roundedScore <= 66) {
+            letterGrade = 'D';
+          } else if (roundedScore >= 60 && roundedScore <= 62) {
+            letterGrade = 'D-';
+          } else {
+            letterGrade = 'F';
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector('#dark-mode');
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(document.querySelectorAll('select'));
+
+          this.totals = document.querySelector('.totals');
+          this.grade = document.querySelector('.grade');
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector('.summary-heading');
+          this.summary = document.querySelector('.summary');
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .map((category) => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + '-summary';
+              return (this[summaryName] = document.querySelector(
+                '.' + summary
+              ));
+            });
+
+          this.resetButton = document.querySelector('.reset');
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .map((category) => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = 'copyButton-' + button;
+              return (this[buttonName] = document.querySelector(
+                '.copy-' + button
+              ));
+            });
+        };
+
+        View.prototype.createFieldset = function ({
+          title,
+          id,
+          score,
+          status,
+          category,
+          type,
+          options,
+        }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${
+            category === 'Academic Integrity'
+              ? ''
+              : "<span class='score'><span data-id='" +
+                id +
+                "' class='current-score'>0</span> / " +
+                score +
+                '</span>'
+          }
+          </legend>
+          ${
+            type === 'true false'
+              ? `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>`
+              : `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(
+                (option) =>
+                  `<option value="${option.score}">${option.score} - ${option.message}</option>`
+              )}
+            </select>
+          </div>`
+          }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({
+          title,
+          subtitle,
+          questions,
+          score,
+        }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${
+            title !== 'Academic Integrity'
+              ? "<span class='cat-score'><span data-cat='" +
+                title +
+                "'>0</span> / " +
+                score +
+                '</span>'
+              : ''
+          }</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ''}
+          ${questions.map((child) => this.createFieldset(child)).join('')}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+          .filter((category) => category.title !== 'Academic Integrity')
+          .map((category) => {
+            let score = category.score;
+            let title = category.title;
+            let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+            return (
+              '<h2>' +
+              title +
+              "<span class='cat-score'><span data-res-cat='" +
+              title +
+              "'>0</span> / " +
+              score +
+              "</span></h2><pre class='" +
+              catClass +
+              "'>" +
+              [...this.model.messages]
+                .filter((messageObj) => messageObj.category === category.title)
+                .map((messageObj) => `* ${messageObj.message}\n`)
+                .join('') +
+              "</pre><button class='copy copy-" +
+              catClass +
+              "'>Copy To Clipboard</button>"
+            );
+          })
+          .join('')}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+                isDarkMode ? 'checked' : ''
+              }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector('#app');
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add('dark');
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, '');
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove('d-none');
+          } else {
+            this.alert.classList.add('d-none');
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add('dark');
+          } else {
+            this.wrapper.classList.remove('dark');
+          }
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .forEach((category) => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + '-summary'].innerHTML = [...this.model.messages]
+                .filter((messageObj) => messageObj.category === category.title)
+                .map((messageObj) => `* ${messageObj.message}\n`)
+                .join('');
+            });
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener('change', (event) => {
+            const { target } = event;
+
+            if (target.name !== 'dark-mode') {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener('click', () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              (radio) => (radio.checked = radio.hasAttribute('checked'))
+            );
+
+            this.view.selects.forEach(
+              (select) => (select.selectedIndex = null)
+            );
+
+            this.model.questions
+              .filter((question) => question.category !== 'Academic Integrity')
+              .forEach((question) => {
+                const id = question.id;
+                const questionScore = document.querySelector(
+                  "span[data-id='" + id + "']"
+                );
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter((category) => category.title !== 'Academic Integrity')
+              .forEach((category) => {
+                const title = category.title;
+                const categoryScore = document.querySelector(
+                  "span[data-cat='" + title + "']"
+                );
+                const resultsCategoryScore = document.querySelector(
+                  "span[data-res-cat='" + title + "']"
+                );
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .forEach((category) => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = 'copyButton-' + title;
+
+              let summaryName = title + '-summary';
+
+              this.view[buttonName].addEventListener('click', () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand('copy');
+              });
+            });
+
+          this.view.darkModeToggle.addEventListener('change', () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+    </script>
+  </body>
+</html>

+ 1346 - 0
rubrics/mod19.html

@@ -0,0 +1,1346 @@
+
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+    <title>Module 19 PWA Challenge Rubric</title>
+    <style>
+      *,
+      *::before,
+      *::after {
+        box-sizing: border-box;
+      }
+
+      html,
+      body {
+        margin: 0;
+        padding: 0;
+        line-height: 1.15;
+        font-size: 100%;
+        color: #fefefe;
+        transition: all 0.4s;
+      }
+
+      body {
+        background-color: #fbfcfd;
+        font-family: "Inter", sans-serif;
+        color: #4a4a4a;
+      }
+
+      #app.dark {
+        background-color: #121212;
+        color: #e2e2e2;
+      }
+
+      h1 {
+        font-size: 1.9rem;
+      }
+
+      h2 {
+        font-size: 1.6rem;
+      }
+
+      h3 {
+        font-size: 1.2rem;
+      }
+
+      h4 {
+        font-size: 0.9rem;
+      }
+
+      p,
+      legend {
+        font-size: 1.2rem;
+      }
+
+      h3.solution {
+        margin-top: 20px!important;
+      }
+
+      a {
+        color: gray;
+        text-decoration: none;
+      }
+
+      a:hover {
+        transition: all .2s ease-in-out;
+        color: white;
+        text-decoration: underline;
+      }
+
+      label {
+        font-size: 0.9rem;
+        line-height: 2;
+      }
+
+      fieldset {
+        border: none;
+        padding: 0;
+        margin-top: 0.9rem;
+      }
+
+      input[type="radio"],
+      input[type="checkbox"] {
+        position: absolute !important;
+        height: 1px;
+        width: 1px;
+        overflow: hidden;
+        clip: rect(1px 1px 1px 1px);
+        clip: rect(1px, 1px, 1px, 1px);
+        white-space: nowrap;
+      }
+
+      .select-lg,
+      .select-lg option {
+        font-family: "Inter", sans-serif;
+        padding: 10px;
+      }
+
+      .narrow {
+        width: 800px;
+      }
+
+      #app {
+        padding: 3%;
+        transition: all 0.4s;
+      }
+
+      #app h1 {
+        font-size: 1.9rem;
+        font-weight: 700;
+        line-height: 1.3;
+      }
+
+      header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+      }
+
+      .switch-group {
+        display: flex;
+        align-items: center;
+        height: 2.25rem;
+        width: 4.5rem;
+      }
+
+      input[type="checkbox"] + .toggle {
+        height: 100%;
+        width: 100%;
+        background-color: #77a1db;
+        border-radius: 1.9rem;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        padding: 0.35rem;
+        cursor: pointer;
+        overflow: hidden;
+        background-position: 25% 125%;
+        transition: all 0.5s;
+      }
+
+      input[type="checkbox"]:focus + .toggle {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      input[type="checkbox"]:checked + .toggle {
+        background-color: rgb(85, 89, 204);
+        background-size: 1rem;
+        background-repeat: no-repeat;
+        background-position: 25%;
+        background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+        transition: all 0.5s;
+      }
+
+      input[type="checkbox"] + .toggle::after {
+        content: "";
+        display: block;
+        height: 1.7rem;
+        width: 1.7rem;
+        border-radius: 50%;
+        cursor: pointer;
+        border: 4px solid #e1be64;
+        background-color: #fddf74;
+        transform: translateX(0rem);
+        transition: all 0.5s;
+        background-position: center bottom;
+      }
+
+      input[type="checkbox"]:hover + .toggle::after {
+        transform: translateX(0rem) scale(1.1);
+      }
+
+      input[type="checkbox"]:checked + .toggle::after {
+        border: 4px solid #ffffff;
+        background-color: rgba(255, 255, 255, 0.4);
+        transform: translateX(2.1rem);
+        transition: all 0.5s;
+        background-position: center;
+        background-size: 60%;
+        background-repeat: no-repeat;
+        background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+      }
+
+      input[type="checkbox"]:checked:hover + .toggle::after {
+        transform: translateX(2.1rem) scale(1.075);
+      }
+
+      .card {
+        background-color: #fff;
+        border-radius: 6px;
+        border-width: 0;
+        box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+        font-size: 1.1rem;
+        height: 100%;
+        line-height: 1.8rem;
+        padding: 1.5rem 2rem;
+        width: 100%;
+        margin-bottom: 1.9rem;
+      }
+
+      .dark .card {
+        background-color: #333333;
+        box-shadow: none;
+      }
+
+      .card h1,
+      .card h2,
+      .card h3,
+      .card h4,
+      .card h5,
+      .card h6 {
+        color: #4a4a4a;
+      }
+
+      .dark .card h1,
+      .dark .card h2,
+      .dark .card h3,
+      .dark .card h4,
+      .dark .card h5,
+      .dark .card h6 {
+        color: #e2e2e2;
+      }
+
+      .card-header h2 {
+        display: inline-block;
+      }
+
+      .card .cat-score {
+        margin-left: 20px;
+        font-size: smaller;
+      }
+
+      .card h3 {
+        font-size: 1.2rem;
+        font-weight: 700;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card p,
+      .card label,
+      .card legend {
+        color: #7a869c;
+        border-width: 0;
+        display: block;
+        font-size: 1.1rem;
+        font-weight: 300;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card legend {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+      }
+
+      .card legend .score {
+        font-size: smaller;
+        margin-left: 20px;
+        font-weight: 100;
+        min-width: 5%;
+      }
+
+      .dark .card p,
+      .dark .card label,
+      .dark .card legend {
+        color: #e2e2e2;
+      }
+
+      .card label {
+        display: inline-block;
+      }
+
+      .card input[type="radio"] + label::before {
+        content: "";
+        height: 0.9rem;
+        width: 0.9rem;
+        margin-right: 0.7rem;
+        border-radius: 50%;
+        border: 1px solid rgb(74, 74, 74);
+        background-color: #ffffff;
+        display: inline-block;
+        cursor: pointer;
+        position: relative;
+        top: 0.09rem;
+      }
+
+      .dark .card input[type="radio"] + label::before {
+        border: 1px solid #e2e2e2;
+        background-color: inherit;
+      }
+
+      .card input[type="radio"]:checked + label::before {
+        background-color: rgb(47, 106, 189);
+        border-color: rgb(47, 106, 189);
+      }
+
+      .dark .card input[type="radio"]:checked + label::before {
+        background-color: #d9d9d9;
+        border-color: #d9d9d9;
+      }
+
+      .card input[type="radio"]:checked + label {
+        font-weight: bold;
+        color: #4a4a4a;
+      }
+
+      .dark input[type="radio"]:checked + label {
+        color: #e2e2e2;
+      }
+
+      .card input[type="radio"]:focus + label::before {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      .card legend {
+        margin-bottom: 0.4rem;
+      }
+
+      .totals {
+        font-size: 1.6rem;
+      }
+
+      .totals + h2 {
+        font-size: 1.9rem;
+        margin-top: 0;
+      }
+
+      .grade {
+        font-size: 1.9rem;
+      }
+
+      button {
+        background-color: #fff;
+        border: 1px solid #4a4a4a;
+        border-radius: 6px;
+        box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+        color: #4a4a4a;
+        cursor: pointer;
+        display: inline-block;
+        font-size: 1rem;
+        font-weight: 500;
+        letter-spacing: 0.04em;
+        line-height: 1.8em;
+        margin: 0.75rem 1.1rem 1.1rem 0;
+        outline: none;
+        padding: 0.5rem 1.4rem;
+        text-transform: uppercase;
+        transition-delay: 0s;
+        transition-duration: 0.25s;
+        transition-property: all;
+        transition-timing-function: ease-out;
+        width: fit-content;
+      }
+
+      .dark button {
+        background: #d9d9d9;
+        color: #121212;
+        box-shadow: none;
+      }
+
+      button:hover {
+        border-color: #dbdbdb;
+        color: #dbdbdb;
+      }
+
+      .dark button:hover {
+        color: #121212;
+      }
+
+      button.copy {
+        margin-left: 0.4rem;
+        font-size: .8rem;
+      }
+
+      button:focus {
+        border-color: rgb(56, 56, 56);
+        box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+        color: rgb(56, 56, 56);
+      }
+
+      div[role="alert"] {
+        color: #4a4a4a;
+        font-size: 1.2rem;
+        margin-bottom: 1.2rem;
+      }
+
+      .dark div[role="alert"] {
+        color: #e2e2e2;
+      }
+
+      .summary {
+        font-size: 1.2rem;
+        border: none;
+        font-family: monospace;
+        background-color: #d5d9de;
+        padding: 0.9rem;
+        border-radius: 6px;
+        line-height: 1.5;
+        width: 100%;
+      }
+
+      .summary h2,
+      .dark .summary h2 {
+        font-size: 1.2rem;
+        font-family: "Inter", sans-serif;
+        margin-top: 35px;
+        margin-bottom: 5px;
+      }
+
+      .summary h2:first-of-type,
+      .dark .summary h2:first-of-type {
+        margin-top: 15px;
+      }
+
+      .dark .summary {
+        background: #6b6a6a;
+        color: #e2e2e2;
+      }
+
+      .summary pre,
+      .dark .summary pre {
+        white-space: pre-wrap;
+        border-radius: 5px;
+        padding: 15px;
+        border: solid 2px rgba(0,0,0,.4);
+      }
+
+      .summary pre:empty {
+        display: none;
+      }
+
+      .summary pre:empty + button {
+        display: none;
+      }
+
+      .d-none {
+        display: none;
+      }
+
+      @media (max-width: 450px) {
+        .btn-container {
+          text-align: center;
+        }
+
+        button {
+          width: 100%;
+          margin: 0.5rem;
+        }
+
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.3rem);
+          background-size: 50%;
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+          background-size: 50%;
+        }
+      }
+
+      @media (max-width: 515px) {
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.7rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.7rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 460px) {
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.4rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.4rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 400px) {
+        #app h1 {
+          font-size: 1.6rem;
+        }
+
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(1.3rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 340px) {
+        input[type="checkbox"]:checked + .toggle::after {
+          transform: translateX(0.9rem);
+        }
+
+        input[type="checkbox"]:checked:hover + .toggle::after {
+          transform: translateX(0.9rem) scale(1.075);
+        }
+      }
+    </style>
+    <script>
+      $(function () {
+        let $select2 = $('.select2').select2({
+          containerCssClass: "wrap"
+        })
+
+        function triggerEvent(name, val) {
+          let selected = $("select[name='" + name + "']")
+          let change = new Event('change', { bubbles: true })
+          selected[0].dispatchEvent(change)
+        }
+
+        $('.select2').on('select2:select', function (e) {
+          triggerEvent(e.target.name, e.target.value)
+        });
+      })
+  </script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script>
+      (function(window) {
+        "use strict";
+
+        function Model() {
+          this.title = `Module 19 PWA Challenge Rubric (Coding)`;
+          this.categories = [
+  {
+    title: "Technical Acceptance Criteria",
+    subtitle: null,
+    solution: null,
+    previewText: null,
+    score: 0,
+    questions: [
+      {
+        title: "Application uses IndexedDB to create an object store and includes both GET and PUT methods",
+        score: 5,
+        type: "likert",
+        options: [
+          {
+            message: "The application does not use IndexedDB",
+            score: 0
+          },
+          {
+            message: "Application uses IndexedDB to create an object store, but doesn't include GET or PUT methods",
+            score: 1
+          },
+          {
+            message: "Application uses IndexedDB to create an object store, but only includes one of the 2 following: a GET or PUT method",
+            score: 3
+          },
+          {
+            message: "Application uses IndexedDB to create an object store and includes both GET and PUT methods",
+            score: 5
+          }
+        ],
+        message: "",
+        status: false
+      },
+      {
+        title: "The application works without an internet connection",
+        score: 5,
+        type: "true false",
+        message: "The application does not work without an internet connection",
+        status: false
+      },
+      {
+        title: "Application automatically saves the content in the text editor when the DOM window is unfocused",
+        score: 5,
+        type: "true false",
+        message: "Content in the text editor content is not saved when the DOM window is unfocused",
+        status: false
+      },
+      {
+        title: "The application is bundled with webpack",
+        score: 5,
+        type: "true false",
+        message: "The application is not bundled using webpack",
+        status: false
+      },
+      {
+        title: "The application creates a service worker with workbox that caches static assets",
+        score: 5,
+        type: "likert",
+        options: [
+          {
+            message: "The application does not register a service worker with workbox that caches static assets",
+            score: 0
+          },
+          {
+            message: "The application creates a service worker with workbox, but it does not cache static assets",
+            score: 2
+          },
+          {
+            message: "The application creates a service worker with workbox that caches static assets",
+            score: 5
+          }
+        ],
+        message: "",
+        status: false
+      },
+      {
+        title: "The application uses Babel to use async/await",
+        score: 5,
+        type: "true false",
+        message: "The application does not use Babel to use async/await",
+        status: false
+      },
+      {
+        title: "Application has a manifest.json generated using the WebpackPwaManifest plug-in",
+        score: 5,
+        type: "true false",
+        message: "The application does not have a manifest.json",
+        status: false
+      },
+      {
+        title: "Application can be installed as a Progressive Web Application",
+        score: 5,
+        type: "true false",
+        message: "Application cannot be installed as a Progressive Web Application",
+        status: false
+      }
+    ]
+  },
+  {
+    title: "Deployment",
+    subtitle: null,
+    solution: null,
+    previewText: null,
+    score: 0,
+    questions: [
+      {
+        title: "Application deployed to Heroku at live URL with build scripts",
+        score: 8,
+        type: "true false",
+        message: "Application isn't deployed and doesn't have build scripts",
+        status: false
+      },
+      {
+        title: "Application loads with no errors",
+        score: 8,
+        type: "true false",
+        message: "Application isn't deployed",
+        status: false
+      },
+      {
+        title: "Application GitHub URL submitted",
+        score: 8,
+        type: "true false",
+        message: "Missing GitHub submission URL",
+        status: false
+      },
+      {
+        title: "GitHub repo contains application code",
+        score: 8,
+        type: "true false",
+        message: "GitHub repo is empty",
+        status: false
+      }
+    ]
+  },
+  {
+    title: "Application Quality",
+    subtitle: null,
+    score: 0,
+    questions: [
+        {
+            title: "Application user experience is intuitive and easy to navigate",
+            score: 5,
+            type: "likert",
+            options: [
+                {
+                    message: "Application user experience is not intuitive and easy to navigate",
+                    score: 0
+                },
+                {
+                    message: "Application user experience could be much more intuitive and easy to navigate",
+                    score: 2
+                },
+                {
+                    message: "Application user experience could be a lot more intuitive and easy to navigate",
+                    score: 4
+                },
+                {
+                    message: "Application user experience is intuitive and easy to navigate",
+                    score: 5
+                }
+            ],
+            message: "",
+            status: false
+        },
+        {
+            title: "Application user interface style is clean and polished",
+            score: 5,
+            type: "likert",
+            options: [
+                {
+                    message: "Application user interface is not clean and polished",
+                    score: 0
+                },
+                {
+                    message: "Application user interface could be much better",
+                    score: 2
+                },
+                {
+                    message: "Application user interface could be a bit better",
+                    score: 4
+                },
+                {
+                    message: "Application user interface style is clean and polished",
+                    score: 5
+                }
+            ],
+            message: "",
+            status: false
+        },
+        {
+            title: "Application resembles the mock-up functionality provided in Challenge instructions",
+            score: 5,
+            type: "likert",
+            options: [
+                {
+                    message: "Application doesn't resemble the mock-up functionality provided in Challenge instructions",
+                    score: 0
+                },
+                {
+                    message: "Application could resemble mock-up functionality provided with Challenge instructions much more closely",
+                    score: 2
+                },
+                {
+                    message: "Application could resemble mock-up functionality provided with Challenge instructions a bit more closely",
+                    score: 4
+                },
+                {
+                    message: "Application resembles the mock-up functionality provided in Challenge instructions",
+                    score: 5
+                }
+            ],
+            message: "",
+            status: false
+        }
+      ]
+    },
+{
+  title: "Repository Quality",
+  subtitle: null,
+  score: 0,
+  questions: [
+      {
+          title: "Repository has a unique name",
+          score: 2,
+          type: "true false",
+          message: "Repo doesn't have a unique name",
+          status: false
+      },
+      {
+          title:
+              "Repository follows best practices for file structure and naming conventions",
+          score: 2,
+          type: "true false",
+          message:
+              "Code doesn't follow recommended file/folder structure",
+          status: false
+      },
+      {
+          title:
+              "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+          score: 2,
+          type: "true false",
+          message:
+              "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+          status: false
+      },
+      {
+          title:
+              "Repository contains multiple descriptive commit messages",
+          score: 2,
+          type: "true false",
+          message:
+              "Repo doesn't contain multiple descriptive commit messages",
+          status: false
+      },
+      {
+          title: "Repository contains quality README with description, screenshot, link to deployed application",
+          score: 5,
+          type: "likert",
+          options: [
+              {
+                  message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                  score: 0
+              },
+              {
+                  message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                  score: 2
+              },
+              {
+                  message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                  score: 4
+              },
+              {
+                  message: "Repository contains quality README with description, screenshot, link to deployed application",
+                  score: 5
+              }
+          ],
+          message: "",
+          status: false
+      }
+  ]
+},
+  {
+    title: "Academic Integrity",
+    subtitle: "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+    solution: null,
+    previewText: null,
+    score: null,
+    questions: [
+      {
+        title: "Application contains a low number of commits",
+        score: 0,
+        type: "true false",
+        message: null,
+        status: false
+      },
+      {
+        title: "Application codebase appears advanced for the student, at this point in the course, or in general",
+        score: 0,
+        type: "true false",
+        message: null,
+        status: false
+      },
+      {
+        title: "Application package.json file references a different author and/or repository",
+        score: 0,
+        type: "true false",
+        message: null,
+        status: false
+      },
+      {
+        title: "Application package.json file references much older versions of npm packages",
+        score: 0,
+        type: "true false",
+        message: null,
+        status: false
+      }
+    ]
+  }
+];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function() {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+          this.categories
+          .forEach(category => {
+            category.score = 0;
+            category.questions.forEach(question => {
+              question.category = category.title;
+              question.status = false;
+              question.id = this.questions.length;
+              question.flagged = category.title === "Academic Integrity";
+              question.bonus = category.title === "Bonus";
+              if (question.type === "likert") {
+                question.message = question.options[0].message.split(" - ").pop();
+              };
+
+              let messageObj = {
+                message : question.message,
+                category : question.category
+              };
+
+              question.message && !question.bonus && this.messages.add(messageObj);
+              this.questions.push(question);
+              category.score += question.score;
+            });
+          });
+        };
+
+        Model.prototype.toggleDarkMode = function() {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem("isDarkMode", this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function(radio) {
+          const id = Number(radio.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+            
+            if (!question.bonus) {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            } else {
+              let messageObj = {
+                message : question.message,
+                category : category
+              }
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category
+                }
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach(message => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              })
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== "Academic Integrity") {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute("name"));
+          const question = this.questions[id];
+          const questionScore = document.querySelector("span[data-id='" + id + "']");
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector("span[data-cat='" + category.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+          const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+          let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+          
+          if (newMessage === "Open this menu") {
+            newMessage = select[1].text.split(" - ").pop();
+          };
+
+          if (question.type === "likert") {
+
+            this.messages.forEach(item => {
+              if (item.message === question.message) {
+                this.messages.delete(item)
+              }
+            })
+           
+           question.message = newMessage;
+
+           let messageObj = {
+              message: newMessage,
+              category: category
+            }
+
+            this.messages.add(messageObj);
+
+          };
+
+          
+          
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach(question => {
+              if (question.type === "true false") {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector("select[name='" + question.id + "']").value;
+                categoryTotal += parseInt(score);
+              }
+            })
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+
+        };
+
+        Model.prototype.calcGrade = function() {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = "";
+
+          if (roundedScore >= 99.5) {
+            letterGrade = "A+";
+          } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+            letterGrade = "A";
+          } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+            letterGrade = "A-";
+          } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+            letterGrade = "B+";
+          } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+            letterGrade = "B";
+          } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+            letterGrade = "B-";
+          } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+            letterGrade = "C+";
+          } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+            letterGrade = "C";
+          } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+            letterGrade = "C-";
+          } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+            letterGrade = "D+";
+          } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+            letterGrade = "D";
+          } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+            letterGrade = "D-";
+          } else {
+            letterGrade = "F";
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function() {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function() {
+          this.darkModeToggle = document.querySelector("#dark-mode");
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(
+            document.querySelectorAll("select")
+          );
+
+          this.totals = document.querySelector(".totals");
+          this.grade = document.querySelector(".grade");
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector(".summary-heading");
+          this.summary = document.querySelector(".summary");
+          
+          this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .map(category => {
+            let summary = category.title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+            let summaryName = summary + "-summary";
+            return this[summaryName] = document.querySelector("." + summary);
+            })
+
+          this.resetButton = document.querySelector(".reset");
+          
+          this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .map(category => {
+            let button = category.title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+            let buttonName = "copyButton-" + button;
+            return this[buttonName] = document.querySelector(".copy-" + button);
+            })
+        };
+
+        View.prototype.createFieldset = function({ title, id, score, status, category, type, options }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+          `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+          `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+        }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function({ title, subtitle, solution, previewText, questions, score }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${solution ? `<h3 class="solution">Solution: <a href=${solution} target="_blank" rel="noopener noreferrer">${previewText !== null ? previewText : solution}</a></h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+        };
+
+        View.prototype.createResults = function() {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .map(category => {
+            let score = category.score;
+            let title = category.title;
+            let catClass = title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+
+            return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+            .filter(messageObj => messageObj.category === category.title)
+            .map(messageObj => `* ${messageObj.message}\n`)
+            .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+            })
+          .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function(title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+                isDarkMode ? "checked" : ""
+              }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+        };
+
+        View.prototype.generateHTML = function() {
+          this.wrapper = document.querySelector("#app");
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, "");
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function() {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove("d-none");
+          } else {
+            this.alert.classList.add("d-none");
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add("dark");
+          } else {
+            this.wrapper.classList.remove("dark");
+          }
+
+          this.model.categories
+          .filter(category => category.title !== "Academic Integrity")
+          .forEach(category => {
+            let catClass = category.title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+
+            this[catClass + "-summary"].innerHTML = [...this.model.messages]
+            .filter(messageObj => messageObj.category === category.title)
+            .map(messageObj => `* ${messageObj.message}\n`)
+            .join("")
+            })
+
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function() {
+          this.view.wrapper.addEventListener("change", event => {
+            const { target } = event;
+            
+            if (target.name !== "dark-mode") {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener("click", () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              radio => (radio.checked = radio.hasAttribute("checked"))
+            );
+
+            this.view.selects.forEach(
+              select => select.selectedIndex = null
+            )
+            
+            this.model.questions
+            .filter(question => question.category !== "Academic Integrity")
+            .forEach(question => {
+              const id = question.id;
+              const questionScore = document.querySelector("span[data-id='" + id + "']");
+              questionScore.textContent = 0;
+            });
+            
+            this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              const title = category.title
+              const categoryScore = document.querySelector("span[data-cat='" + title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+              const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase() + "']");
+              categoryScore.textContent = 0;
+              resultsCategoryScore.textContent = 0;
+            });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter(category => category.title !== "Academic Integrity")
+            .forEach(category => {
+              let title = category.title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-').toLowerCase();
+              let buttonName = "copyButton-" + title;
+              
+              let summaryName = title + "-summary";
+
+              this.view[buttonName].addEventListener("click", () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand("copy");
+              });
+            });
+
+
+          this.view.darkModeToggle.addEventListener("change", () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+    </script>
+  </body>
+  </html>
+    

+ 1367 - 0
rubrics/mod20.html

@@ -0,0 +1,1367 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+    <title>Module 20 React Challenge Rubric</title>
+    <style>
+        *,
+        *::before,
+        *::after {
+            box-sizing: border-box;
+        }
+
+        html,
+        body {
+            margin: 0;
+            padding: 0;
+            line-height: 1.15;
+            font-size: 100%;
+            color: #fefefe;
+            transition: all 0.4s;
+        }
+
+        body {
+            background-color: #fbfcfd;
+            font-family: "Inter", sans-serif;
+            color: #4a4a4a;
+        }
+
+        #app.dark {
+            background-color: #121212;
+            color: #e2e2e2;
+        }
+
+        h1 {
+            font-size: 1.9rem;
+        }
+
+        h2 {
+            font-size: 1.6rem;
+        }
+
+        h3 {
+            font-size: 1.2rem;
+        }
+
+        h4 {
+            font-size: 0.9rem;
+        }
+
+        p,
+        legend {
+            font-size: 1.2rem;
+        }
+
+        label {
+            font-size: 0.9rem;
+            line-height: 2;
+        }
+
+        fieldset {
+            border: none;
+            padding: 0;
+            margin-top: 0.9rem;
+        }
+
+        input[type="radio"],
+        input[type="checkbox"] {
+            position: absolute !important;
+            height: 1px;
+            width: 1px;
+            overflow: hidden;
+            clip: rect(1px 1px 1px 1px);
+            clip: rect(1px, 1px, 1px, 1px);
+            white-space: nowrap;
+        }
+
+        .select-lg,
+        .select-lg option {
+            font-family: "Inter", sans-serif;
+            padding: 10px;
+        }
+
+        .narrow {
+            width: 800px;
+        }
+
+        #app {
+            padding: 3%;
+            transition: all 0.4s;
+        }
+
+        #app h1 {
+            font-size: 1.9rem;
+            font-weight: 700;
+            line-height: 1.3;
+        }
+
+        header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .switch-group {
+            display: flex;
+            align-items: center;
+            height: 2.25rem;
+            width: 4.5rem;
+        }
+
+        input[type="checkbox"]+.toggle {
+            height: 100%;
+            width: 100%;
+            background-color: #77a1db;
+            border-radius: 1.9rem;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            padding: 0.35rem;
+            cursor: pointer;
+            overflow: hidden;
+            background-position: 25% 125%;
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]:focus+.toggle {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        input[type="checkbox"]:checked+.toggle {
+            background-color: rgb(85, 89, 204);
+            background-size: 1rem;
+            background-repeat: no-repeat;
+            background-position: 25%;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]+.toggle::after {
+            content: "";
+            display: block;
+            height: 1.7rem;
+            width: 1.7rem;
+            border-radius: 50%;
+            cursor: pointer;
+            border: 4px solid #e1be64;
+            background-color: #fddf74;
+            transform: translateX(0rem);
+            transition: all 0.5s;
+            background-position: center bottom;
+        }
+
+        input[type="checkbox"]:hover+.toggle::after {
+            transform: translateX(0rem) scale(1.1);
+        }
+
+        input[type="checkbox"]:checked+.toggle::after {
+            border: 4px solid #ffffff;
+            background-color: rgba(255, 255, 255, 0.4);
+            transform: translateX(2.1rem);
+            transition: all 0.5s;
+            background-position: center;
+            background-size: 60%;
+            background-repeat: no-repeat;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+        }
+
+        input[type="checkbox"]:checked:hover+.toggle::after {
+            transform: translateX(2.1rem) scale(1.075);
+        }
+
+        .card {
+            background-color: #fff;
+            border-radius: 6px;
+            border-width: 0;
+            box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+            font-size: 1.1rem;
+            height: 100%;
+            line-height: 1.8rem;
+            padding: 1.5rem 2rem;
+            width: 100%;
+            margin-bottom: 1.9rem;
+        }
+
+        .dark .card {
+            background-color: #333333;
+            box-shadow: none;
+        }
+
+        .card h1,
+        .card h2,
+        .card h3,
+        .card h4,
+        .card h5,
+        .card h6 {
+            color: #4a4a4a;
+        }
+
+        .dark .card h1,
+        .dark .card h2,
+        .dark .card h3,
+        .dark .card h4,
+        .dark .card h5,
+        .dark .card h6 {
+            color: #e2e2e2;
+        }
+
+        .card-header h2 {
+            display: inline-block;
+        }
+
+        .card .cat-score {
+            margin-left: 20px;
+            font-size: smaller;
+        }
+
+        .card h3 {
+            font-size: 1.2rem;
+            font-weight: 700;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card p,
+        .card label,
+        .card legend {
+            color: #7a869c;
+            border-width: 0;
+            display: block;
+            font-size: 1.1rem;
+            font-weight: 300;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card legend {
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+        }
+
+        .card legend .score {
+            font-size: smaller;
+            margin-left: 20px;
+            font-weight: 100;
+            min-width: 5%;
+        }
+
+        .dark .card p,
+        .dark .card label,
+        .dark .card legend {
+            color: #e2e2e2;
+        }
+
+        .card label {
+            display: inline-block;
+        }
+
+        .card input[type="radio"]+label::before {
+            content: "";
+            height: 0.9rem;
+            width: 0.9rem;
+            margin-right: 0.7rem;
+            border-radius: 50%;
+            border: 1px solid rgb(74, 74, 74);
+            background-color: #ffffff;
+            display: inline-block;
+            cursor: pointer;
+            position: relative;
+            top: 0.09rem;
+        }
+
+        .dark .card input[type="radio"]+label::before {
+            border: 1px solid #e2e2e2;
+            background-color: inherit;
+        }
+
+        .card input[type="radio"]:checked+label::before {
+            background-color: rgb(47, 106, 189);
+            border-color: rgb(47, 106, 189);
+        }
+
+        .dark .card input[type="radio"]:checked+label::before {
+            background-color: #d9d9d9;
+            border-color: #d9d9d9;
+        }
+
+        .card input[type="radio"]:checked+label {
+            font-weight: bold;
+            color: #4a4a4a;
+        }
+
+        .dark input[type="radio"]:checked+label {
+            color: #e2e2e2;
+        }
+
+        .card input[type="radio"]:focus+label::before {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        .card legend {
+            margin-bottom: 0.4rem;
+        }
+
+        .totals {
+            font-size: 1.6rem;
+        }
+
+        .totals+h2 {
+            font-size: 1.9rem;
+            margin-top: 0;
+        }
+
+        .grade {
+            font-size: 1.9rem;
+        }
+
+        button {
+            background-color: #fff;
+            border: 1px solid #4a4a4a;
+            border-radius: 6px;
+            box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+            color: #4a4a4a;
+            cursor: pointer;
+            display: inline-block;
+            font-size: 1rem;
+            font-weight: 500;
+            letter-spacing: 0.04em;
+            line-height: 1.8em;
+            margin: 0.75rem 1.1rem 1.1rem 0;
+            outline: none;
+            padding: 0.5rem 1.4rem;
+            text-transform: uppercase;
+            transition-delay: 0s;
+            transition-duration: 0.25s;
+            transition-property: all;
+            transition-timing-function: ease-out;
+            width: fit-content;
+        }
+
+        .dark button {
+            background: #d9d9d9;
+            color: #121212;
+            box-shadow: none;
+        }
+
+        button:hover {
+            border-color: #dbdbdb;
+            color: #dbdbdb;
+        }
+
+        .dark button:hover {
+            color: #121212;
+        }
+
+        button.copy {
+            margin-left: 0.4rem;
+            font-size: .8rem;
+        }
+
+        button:focus {
+            border-color: rgb(56, 56, 56);
+            box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+            color: rgb(56, 56, 56);
+        }
+
+        div[role="alert"] {
+            color: #4a4a4a;
+            font-size: 1.2rem;
+            margin-bottom: 1.2rem;
+        }
+
+        .dark div[role="alert"] {
+            color: #e2e2e2;
+        }
+
+        .summary {
+            font-size: 1.2rem;
+            border: none;
+            font-family: monospace;
+            background-color: #d5d9de;
+            padding: 0.9rem;
+            border-radius: 6px;
+            line-height: 1.5;
+            width: 100%;
+        }
+
+        .summary h2,
+        .dark .summary h2 {
+            font-size: 1.2rem;
+            font-family: "Inter", sans-serif;
+            margin-top: 35px;
+            margin-bottom: 5px;
+        }
+
+        .summary h2:first-of-type,
+        .dark .summary h2:first-of-type {
+            margin-top: 15px;
+        }
+
+        .dark .summary {
+            background: #6b6a6a;
+            color: #e2e2e2;
+        }
+
+        .summary pre,
+        .dark .summary pre {
+            white-space: pre-wrap;
+            border-radius: 5px;
+            padding: 15px;
+            border: solid 2px rgba(0, 0, 0, .4);
+        }
+
+        .summary pre:empty {
+            display: none;
+        }
+
+        .summary pre:empty+button {
+            display: none;
+        }
+
+        .d-none {
+            display: none;
+        }
+
+        @media (max-width: 450px) {
+            .btn-container {
+                text-align: center;
+            }
+
+            button {
+                width: 100%;
+                margin: 0.5rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+                background-size: 50%;
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+                background-size: 50%;
+            }
+        }
+
+        @media (max-width: 515px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.7rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.7rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 460px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.4rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.4rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 400px) {
+            #app h1 {
+                font-size: 1.6rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 340px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(0.9rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(0.9rem) scale(1.075);
+            }
+        }
+    </style>
+    <script>
+        $(function () {
+            let $select2 = $('.select2').select2({
+                containerCssClass: "wrap"
+            })
+
+            function triggerEvent(name, val) {
+                let selected = $("select[name='" + name + "']")
+                let change = new Event('change', { bubbles: true })
+                selected[0].dispatchEvent(change)
+            }
+
+            $('.select2').on('select2:select', function (e) {
+                triggerEvent(e.target.name, e.target.value)
+            });
+        })
+    </script>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script>
+            (function (window) {
+                "use strict";
+
+                function Model() {
+                    this.title = `Module 20 React Challenge Rubric (Coding)`;
+                    this.categories = [
+                        {
+                            title: "Deployment",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Student submitted deployed live application URL",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "Application isn't deployed",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application loads at deployed URL (no 404, or Heroku/Netlify error)",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "Application isn't deployed",
+                                    status: false
+                                },
+                                {
+                                    title: "Student submitted application GitHub URL",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "Missing GitHub submission URL",
+                                    status: false
+                                },
+                                {
+                                    title: "GitHub Repo contains application code",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "GitHub repo is empty",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Technical Acceptance Criteria",
+                            subtitle:
+                                "Keep your browser console open and watch out for errors from this point forward.",
+                            questions: [
+                                {
+                                    title: "Application uses React to render content",
+                                    score: 5,
+                                    type: "true false",
+                                    message: "Application doesn't use React to render content",
+                                    status: false
+                                },
+                                {
+                                    title: "Application has a single Header component that appears on multiple pages with a Navigation component within it that is used to conditionally render About Me, Portfolio, Contact, and Resume sections",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't have a single Header component that appears on multiple pages with a Navigation component within it that is used to conditionally render About Me, Portfolio, Contact, and Resume sections",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application has a single Header component that appears on multiple pages but it does not include a Navigation component",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application has a single Header component that appears on multiple pages with a Navigation component within it but the navigation does not work",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application has a single Header component that appears on multiple pages with a Navigation component within it that is used to conditionally render About Me, Portfolio, Contact, and Resume sections",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application has a single Project component that is used multiple times in the Portfolio section",
+                                    score: 5,
+                                    type: "true false",
+                                    message: "Application doesn't have a single Project component that is used multiple times in the Portfolio section",
+                                    status: false
+                                },
+                                {
+                                    title: "Application has a single Footer component that appears on multiple pages",
+                                    score: 5,
+                                    type: "true false",
+                                    message: "Application doesn't have a single Footer component that appears on multiple pages",
+                                    status: false
+                                },
+                                {
+                                    title: "Application's About Me section includes a photo or avatar of the student and a bio",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application's About Me section doesn't include a photo or avatar of the student and a bio",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application's About Me section is missing one of the following requirements: a photo or avatar of the student and a bio",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application's About Me section includes a photo or avatar of the student and a bio",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application's Portfolio section includes 6 deployed projects with an image of the project and links to the deployed application and the corresponding repository",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application's Portfolio section doesn't include 6 deployed projects with an image of the project and links to the deployed application and the corresponding repository",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application's Portfolio section includes 1 to 6 deployed projects with an image of the project, but both of the following are missing from any of the projects: a link to the deployed application and a link to the corresponding repository",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application's Portfolio section includes 1 to 6 deployed projects with an image of the project, but one of the following is missing from any of the projects: a link to the deployed application or a link to the corresponding repository",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application's Portfolio section includes 1 to 3 deployed projects with an image of the project and links to the deployed application and the corresponding repository",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application's Portfolio section includes 4 to 5 deployed projects with an image of the project and links to the deployed application and the corresponding repository",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application's Portfolio section includes 6 deployed projects with an image of the project and links to the deployed application and the corresponding repository",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application's Contact section includes a contact form with form field validation",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application's Contact section doesn't include a contact form with form field validation",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application's Contact section includes a contact form but is missing form field validation",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application's Contact section includes a contact form with form field validation",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application's Resume section includes a link to download a resume and a list of the student's proficiencies",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application's Resume section doesn't include a link to download a resume and a list of the student's proficiencies",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application's Resume section is missing one of the following requirements: a link to download a resume and a list of the student's proficiencies",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application's Resume section includes a link to download a resume and a list of the student's proficiencies",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                            ]
+                        },
+                        {
+                            title: "Repository Quality",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Repository has a unique name",
+                                    score: 2,
+                                    type: "true false",
+                                    message: "Repo doesn't have a unique name",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository follows best practices for file structure and naming conventions",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Code doesn't follow recommended file/folder structure",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository contains multiple descriptive commit messages",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Repo doesn't contain multiple descriptive commit messages",
+                                    status: false
+                                },
+                                {
+                                    title: "Repository contains quality README with description, screenshot, link to deployed application",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Repository contains quality README with description, screenshot, link to deployed application",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Application Quality",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Application user experience is intuitive and easy to navigate",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application user experience is not intuitive and easy to navigate",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application user experience could be much more intuitive and easy to navigate",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application user experience could be a lot more intuitive and easy to navigate",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application user experience is intuitive and easy to navigate",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application user interface style is clean and polished",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application user interface is not clean and polished",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application user interface could be much better",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application user interface could be a bit better",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application user interface style is clean and polished",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application uses a color scheme other than the default Boostrap color palette",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application has no color scheme or uses the default Bootstrap color palette",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Academic Integrity",
+                            subtitle:
+                                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Application contains a low number of commits",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application codebase appears advanced for the student, at this point in the course, or in general",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application package.json file references a different author and/or repository",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application package.json file references much older versions of npm packages",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                }
+                            ]
+                        }
+                    ];
+
+                    this.initStorage();
+                }
+
+                Model.prototype.initStorage = function () {
+                    this.score = 0;
+                    this.questions = [];
+                    this.messages = new Set();
+                    this.flagged = new Set();
+                    this.isDarkMode =
+                        JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+                    this.categories
+                        .forEach(category => {
+                            category.score = 0;
+                            category.questions.forEach(question => {
+                                question.category = category.title;
+                                question.status = false;
+                                question.id = this.questions.length;
+                                question.flagged = category.title === "Academic Integrity";
+                                question.bonus = category.title === "Bonus";
+                                if (question.type === "likert") {
+                                    question.message = question.options[0].message.split(" - ").pop();
+                                };
+
+                                let messageObj = {
+                                    message: question.message,
+                                    category: question.category
+                                };
+
+                                question.message && !question.bonus && this.messages.add(messageObj);
+                                this.questions.push(question);
+                                category.score += question.score;
+                            });
+                        });
+                };
+
+                Model.prototype.toggleDarkMode = function () {
+                    this.isDarkMode = !this.isDarkMode;
+                    localStorage.setItem("isDarkMode", this.isDarkMode);
+                };
+
+                Model.prototype.toggleRadio = function (radio) {
+                    const id = Number(radio.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+                    question.status = !question.status;
+
+                    if (question.status === true) {
+
+                        if (!question.bonus) {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        } else {
+                            let messageObj = {
+                                message: question.message,
+                                category: category
+                            }
+                            this.messages.add(messageObj);
+                        }
+
+                        this.score += question.score;
+
+                        if (question.flagged) {
+                            this.flagged.add(question.id);
+                        }
+                    } else {
+                        if (question.flagged) {
+                            this.flagged.delete(question.id);
+                        }
+
+                        if (!question.bonus) {
+                            if (question.message) {
+                                let messageObj = {
+                                    message: question.message,
+                                    category: category
+                                }
+                                this.messages.add(messageObj);
+                            }
+                        } else {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        }
+
+                        this.score -= question.score;
+                    }
+
+                    if (category !== "Academic Integrity") {
+                        if (question.status) {
+                            questionScore.textContent = question.score;
+                        } else {
+                            questionScore.textContent = 0;
+                        }
+
+                        const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                        let categoryTotal = 0;
+
+                        catQuestions.forEach(question => {
+                            if (question.type === "true false") {
+                                if (question.status) {
+                                    categoryTotal += question.score;
+                                }
+                            } else {
+                                let score = document.querySelector("select[name='" + question.id + "']").value;
+                                categoryTotal += parseInt(score);
+                            }
+                        })
+
+                        categoryScore.textContent = categoryTotal;
+                        resultsCategoryScore.textContent = categoryTotal;
+                    }
+
+                };
+
+                Model.prototype.handleSelect = function (select) {
+                    const id = Number(select.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const oldScore = questionScore.textContent;
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+                    let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+                    if (newMessage === "Open this menu") {
+                        newMessage = select[1].text.split(" - ").pop();
+                    };
+
+                    if (question.type === "likert") {
+
+                        this.messages.forEach(item => {
+                            if (item.message === question.message) {
+                                this.messages.delete(item)
+                            }
+                        })
+
+                        question.message = newMessage;
+
+                        let messageObj = {
+                            message: newMessage,
+                            category: category
+                        }
+
+                        this.messages.add(messageObj);
+
+                    };
+
+
+
+                    this.score -= parseInt(oldScore);
+                    this.score += parseInt(select.value);
+                    questionScore.textContent = select.value;
+
+                    const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                    let categoryTotal = 0;
+
+                    catQuestions.forEach(question => {
+                        if (question.type === "true false") {
+                            if (question.status) {
+                                categoryTotal += question.score;
+                            }
+                        } else {
+                            let score = document.querySelector("select[name='" + question.id + "']").value;
+                            categoryTotal += parseInt(score);
+                        }
+                    })
+
+                    categoryScore.textContent = categoryTotal;
+                    resultsCategoryScore.textContent = categoryTotal;
+
+                };
+
+                Model.prototype.calcGrade = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.score));
+
+                    let letterGrade = "";
+
+                    if (roundedScore >= 99.5) {
+                        letterGrade = "A+";
+                    } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+                        letterGrade = "A";
+                    } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+                        letterGrade = "A-";
+                    } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+                        letterGrade = "B+";
+                    } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+                        letterGrade = "B";
+                    } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+                        letterGrade = "B-";
+                    } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+                        letterGrade = "C+";
+                    } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+                        letterGrade = "C";
+                    } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+                        letterGrade = "C-";
+                    } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+                        letterGrade = "D+";
+                    } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+                        letterGrade = "D";
+                    } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+                        letterGrade = "D-";
+                    } else {
+                        letterGrade = "F";
+                    }
+
+                    return letterGrade;
+                };
+
+                Model.prototype.hasFlags = function () {
+                    return this.flagged.size > 0;
+                };
+
+                function View(model) {
+                    this.model = model;
+                    this.generateHTML();
+                    this.cacheDom();
+                }
+
+                View.prototype.cacheDom = function () {
+                    this.darkModeToggle = document.querySelector("#dark-mode");
+
+                    this.radios = Array.from(
+                        document.querySelectorAll("input[type='radio']")
+                    );
+
+                    this.selects = Array.from(
+                        document.querySelectorAll("select")
+                    );
+
+                    this.totals = document.querySelector(".totals");
+                    this.grade = document.querySelector(".grade");
+                    this.alert = document.querySelector("div[role='alert']");
+                    this.summaryHeading = document.querySelector(".summary-heading");
+                    this.summary = document.querySelector(".summary");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let summaryName = summary + "-summary";
+                            return this[summaryName] = document.querySelector("." + summary);
+                        })
+
+                    this.resetButton = document.querySelector(".reset");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let button = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + button;
+                            return this[buttonName] = document.querySelector(".copy-" + button);
+                        })
+                };
+
+                View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+                    return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+                            `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+                            `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+                        }
+        </fieldset>`;
+                };
+
+                View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+                    return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+                };
+
+                View.prototype.createResults = function () {
+                    return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .map(category => {
+                                let score = category.score;
+                                let title = category.title;
+                                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                                    .filter(messageObj => messageObj.category === category.title)
+                                    .map(messageObj => `* ${messageObj.message}\n`)
+                                    .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+                            })
+                            .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+                };
+
+                View.prototype.createHeader = function (title, isDarkMode = false) {
+                    return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
+                        }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+                };
+
+                View.prototype.generateHTML = function () {
+                    this.wrapper = document.querySelector("#app");
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    }
+
+                    const survey = this.model.categories.reduce((final, current) => {
+                        final += this.createCard(current);
+                        return final;
+                    }, "");
+
+                    const results = this.createResults();
+
+                    const header = this.createHeader(
+                        this.model.title,
+                        this.model.isDarkMode
+                    );
+
+                    this.wrapper.innerHTML = header + survey + results;
+                };
+
+                View.prototype.update = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.model.score));
+                    this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+                    const letterGrade = this.model.calcGrade();
+                    this.grade.textContent = letterGrade;
+
+                    if (this.model.hasFlags()) {
+                        this.alert.classList.remove("d-none");
+                    } else {
+                        this.alert.classList.add("d-none");
+                    }
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    } else {
+                        this.wrapper.classList.remove("dark");
+                    }
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+                            this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                                .filter(messageObj => messageObj.category === category.title)
+                                .map(messageObj => `* ${messageObj.message}\n`)
+                                .join("")
+                        })
+
+                };
+
+                function Controller(model, view) {
+                    this.model = model;
+                    this.view = view;
+                    this.addEventListeners();
+                }
+
+                Controller.prototype.addEventListeners = function () {
+                    this.view.wrapper.addEventListener("change", event => {
+                        const { target } = event;
+
+                        if (target.name !== "dark-mode") {
+                            if (!target.matches("input[type='radio']")) {
+                                this.model.handleSelect(target);
+                            } else {
+                                this.model.toggleRadio(target);
+                            }
+                        }
+
+                        this.view.update();
+                    });
+
+                    this.view.resetButton.addEventListener("click", () => {
+                        this.model.initStorage();
+
+                        this.view.radios.forEach(
+                            radio => (radio.checked = radio.hasAttribute("checked"))
+                        );
+
+                        this.view.selects.forEach(
+                            select => select.selectedIndex = null
+                        )
+
+                        this.model.questions
+                            .filter(question => question.category !== "Academic Integrity")
+                            .forEach(question => {
+                                const id = question.id;
+                                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                                questionScore.textContent = 0;
+                            });
+
+                        this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .forEach(category => {
+                                const title = category.title
+                                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                                categoryScore.textContent = 0;
+                                resultsCategoryScore.textContent = 0;
+                            });
+
+                        this.view.update();
+                        window.scrollTo(0, 0);
+                    });
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let title = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + title;
+
+                            let summaryName = title + "-summary";
+
+                            this.view[buttonName].addEventListener("click", () => {
+                                const selection = window.getSelection();
+                                const range = document.createRange();
+                                range.selectNodeContents(this.view[summaryName]);
+                                selection.removeAllRanges();
+                                selection.addRange(range);
+                                document.execCommand("copy");
+                            });
+                        });
+
+
+                    this.view.darkModeToggle.addEventListener("change", () => {
+                        this.model.toggleDarkMode();
+                        this.view.update();
+                    });
+                };
+
+                function App(Model, View, Controller) {
+                    this.model = new Model();
+                    this.view = new View(this.model);
+                    this.controller = new Controller(this.model, this.view);
+                }
+
+                return new App(Model, View, Controller);
+            })(window);
+    </script>
+</body>
+
+</html>

+ 1353 - 0
rubrics/mod21.html

@@ -0,0 +1,1353 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
+    <title>Module 21 MERN Challenge Rubric</title>
+    <style>
+        *,
+        *::before,
+        *::after {
+            box-sizing: border-box;
+        }
+
+        html,
+        body {
+            margin: 0;
+            padding: 0;
+            line-height: 1.15;
+            font-size: 100%;
+            color: #fefefe;
+            transition: all 0.4s;
+        }
+
+        body {
+            background-color: #fbfcfd;
+            font-family: "Inter", sans-serif;
+            color: #4a4a4a;
+        }
+
+        #app.dark {
+            background-color: #121212;
+            color: #e2e2e2;
+        }
+
+        h1 {
+            font-size: 1.9rem;
+        }
+
+        h2 {
+            font-size: 1.6rem;
+        }
+
+        h3 {
+            font-size: 1.2rem;
+        }
+
+        h4 {
+            font-size: 0.9rem;
+        }
+
+        p,
+        legend {
+            font-size: 1.2rem;
+        }
+
+        label {
+            font-size: 0.9rem;
+            line-height: 2;
+        }
+
+        fieldset {
+            border: none;
+            padding: 0;
+            margin-top: 0.9rem;
+        }
+
+        input[type="radio"],
+        input[type="checkbox"] {
+            position: absolute !important;
+            height: 1px;
+            width: 1px;
+            overflow: hidden;
+            clip: rect(1px 1px 1px 1px);
+            clip: rect(1px, 1px, 1px, 1px);
+            white-space: nowrap;
+        }
+
+        .select-lg,
+        .select-lg option {
+            font-family: "Inter", sans-serif;
+            padding: 10px;
+        }
+
+        .narrow {
+            width: 800px;
+        }
+
+        #app {
+            padding: 3%;
+            transition: all 0.4s;
+        }
+
+        #app h1 {
+            font-size: 1.9rem;
+            font-weight: 700;
+            line-height: 1.3;
+        }
+
+        header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .switch-group {
+            display: flex;
+            align-items: center;
+            height: 2.25rem;
+            width: 4.5rem;
+        }
+
+        input[type="checkbox"]+.toggle {
+            height: 100%;
+            width: 100%;
+            background-color: #77a1db;
+            border-radius: 1.9rem;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            padding: 0.35rem;
+            cursor: pointer;
+            overflow: hidden;
+            background-position: 25% 125%;
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]:focus+.toggle {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        input[type="checkbox"]:checked+.toggle {
+            background-color: rgb(85, 89, 204);
+            background-size: 1rem;
+            background-repeat: no-repeat;
+            background-position: 25%;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
+            transition: all 0.5s;
+        }
+
+        input[type="checkbox"]+.toggle::after {
+            content: "";
+            display: block;
+            height: 1.7rem;
+            width: 1.7rem;
+            border-radius: 50%;
+            cursor: pointer;
+            border: 4px solid #e1be64;
+            background-color: #fddf74;
+            transform: translateX(0rem);
+            transition: all 0.5s;
+            background-position: center bottom;
+        }
+
+        input[type="checkbox"]:hover+.toggle::after {
+            transform: translateX(0rem) scale(1.1);
+        }
+
+        input[type="checkbox"]:checked+.toggle::after {
+            border: 4px solid #ffffff;
+            background-color: rgba(255, 255, 255, 0.4);
+            transform: translateX(2.1rem);
+            transition: all 0.5s;
+            background-position: center;
+            background-size: 60%;
+            background-repeat: no-repeat;
+            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
+        }
+
+        input[type="checkbox"]:checked:hover+.toggle::after {
+            transform: translateX(2.1rem) scale(1.075);
+        }
+
+        .card {
+            background-color: #fff;
+            border-radius: 6px;
+            border-width: 0;
+            box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+            font-size: 1.1rem;
+            height: 100%;
+            line-height: 1.8rem;
+            padding: 1.5rem 2rem;
+            width: 100%;
+            margin-bottom: 1.9rem;
+        }
+
+        .dark .card {
+            background-color: #333333;
+            box-shadow: none;
+        }
+
+        .card h1,
+        .card h2,
+        .card h3,
+        .card h4,
+        .card h5,
+        .card h6 {
+            color: #4a4a4a;
+        }
+
+        .dark .card h1,
+        .dark .card h2,
+        .dark .card h3,
+        .dark .card h4,
+        .dark .card h5,
+        .dark .card h6 {
+            color: #e2e2e2;
+        }
+
+        .card-header h2 {
+            display: inline-block;
+        }
+
+        .card .cat-score {
+            margin-left: 20px;
+            font-size: smaller;
+        }
+
+        .card h3 {
+            font-size: 1.2rem;
+            font-weight: 700;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card p,
+        .card label,
+        .card legend {
+            color: #7a869c;
+            border-width: 0;
+            display: block;
+            font-size: 1.1rem;
+            font-weight: 300;
+            line-height: 1.8em;
+            margin: 0;
+            padding: 0;
+        }
+
+        .card legend {
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+        }
+
+        .card legend .score {
+            font-size: smaller;
+            margin-left: 20px;
+            font-weight: 100;
+            min-width: 5%;
+        }
+
+        .dark .card p,
+        .dark .card label,
+        .dark .card legend {
+            color: #e2e2e2;
+        }
+
+        .card label {
+            display: inline-block;
+        }
+
+        .card input[type="radio"]+label::before {
+            content: "";
+            height: 0.9rem;
+            width: 0.9rem;
+            margin-right: 0.7rem;
+            border-radius: 50%;
+            border: 1px solid rgb(74, 74, 74);
+            background-color: #ffffff;
+            display: inline-block;
+            cursor: pointer;
+            position: relative;
+            top: 0.09rem;
+        }
+
+        .dark .card input[type="radio"]+label::before {
+            border: 1px solid #e2e2e2;
+            background-color: inherit;
+        }
+
+        .card input[type="radio"]:checked+label::before {
+            background-color: rgb(47, 106, 189);
+            border-color: rgb(47, 106, 189);
+        }
+
+        .dark .card input[type="radio"]:checked+label::before {
+            background-color: #d9d9d9;
+            border-color: #d9d9d9;
+        }
+
+        .card input[type="radio"]:checked+label {
+            font-weight: bold;
+            color: #4a4a4a;
+        }
+
+        .dark input[type="radio"]:checked+label {
+            color: #e2e2e2;
+        }
+
+        .card input[type="radio"]:focus+label::before {
+            outline: rgb(59, 153, 252) auto 5px;
+        }
+
+        .card legend {
+            margin-bottom: 0.4rem;
+        }
+
+        .totals {
+            font-size: 1.6rem;
+        }
+
+        .totals+h2 {
+            font-size: 1.9rem;
+            margin-top: 0;
+        }
+
+        .grade {
+            font-size: 1.9rem;
+        }
+
+        button {
+            background-color: #fff;
+            border: 1px solid #4a4a4a;
+            border-radius: 6px;
+            box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+            color: #4a4a4a;
+            cursor: pointer;
+            display: inline-block;
+            font-size: 1rem;
+            font-weight: 500;
+            letter-spacing: 0.04em;
+            line-height: 1.8em;
+            margin: 0.75rem 1.1rem 1.1rem 0;
+            outline: none;
+            padding: 0.5rem 1.4rem;
+            text-transform: uppercase;
+            transition-delay: 0s;
+            transition-duration: 0.25s;
+            transition-property: all;
+            transition-timing-function: ease-out;
+            width: fit-content;
+        }
+
+        .dark button {
+            background: #d9d9d9;
+            color: #121212;
+            box-shadow: none;
+        }
+
+        button:hover {
+            border-color: #dbdbdb;
+            color: #dbdbdb;
+        }
+
+        .dark button:hover {
+            color: #121212;
+        }
+
+        button.copy {
+            margin-left: 0.4rem;
+            font-size: .8rem;
+        }
+
+        button:focus {
+            border-color: rgb(56, 56, 56);
+            box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+            color: rgb(56, 56, 56);
+        }
+
+        div[role="alert"] {
+            color: #4a4a4a;
+            font-size: 1.2rem;
+            margin-bottom: 1.2rem;
+        }
+
+        .dark div[role="alert"] {
+            color: #e2e2e2;
+        }
+
+        .summary {
+            font-size: 1.2rem;
+            border: none;
+            font-family: monospace;
+            background-color: #d5d9de;
+            padding: 0.9rem;
+            border-radius: 6px;
+            line-height: 1.5;
+            width: 100%;
+        }
+
+        .summary h2,
+        .dark .summary h2 {
+            font-size: 1.2rem;
+            font-family: "Inter", sans-serif;
+            margin-top: 35px;
+            margin-bottom: 5px;
+        }
+
+        .summary h2:first-of-type,
+        .dark .summary h2:first-of-type {
+            margin-top: 15px;
+        }
+
+        .dark .summary {
+            background: #6b6a6a;
+            color: #e2e2e2;
+        }
+
+        .summary pre,
+        .dark .summary pre {
+            white-space: pre-wrap;
+            border-radius: 5px;
+            padding: 15px;
+            border: solid 2px rgba(0, 0, 0, .4);
+        }
+
+        .summary pre:empty {
+            display: none;
+        }
+
+        .summary pre:empty+button {
+            display: none;
+        }
+
+        .d-none {
+            display: none;
+        }
+
+        @media (max-width: 450px) {
+            .btn-container {
+                text-align: center;
+            }
+
+            button {
+                width: 100%;
+                margin: 0.5rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+                background-size: 50%;
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+                background-size: 50%;
+            }
+        }
+
+        @media (max-width: 515px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.7rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.7rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 460px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.4rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.4rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 400px) {
+            #app h1 {
+                font-size: 1.6rem;
+            }
+
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(1.3rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(1.3rem) scale(1.1);
+            }
+        }
+
+        @media (max-width: 340px) {
+            input[type="checkbox"]:checked+.toggle::after {
+                transform: translateX(0.9rem);
+            }
+
+            input[type="checkbox"]:checked:hover+.toggle::after {
+                transform: translateX(0.9rem) scale(1.075);
+            }
+        }
+    </style>
+    <script>
+        $(function () {
+            let $select2 = $('.select2').select2({
+                containerCssClass: "wrap"
+            })
+
+            function triggerEvent(name, val) {
+                let selected = $("select[name='" + name + "']")
+                let change = new Event('change', { bubbles: true })
+                selected[0].dispatchEvent(change)
+            }
+
+            $('.select2').on('select2:select', function (e) {
+                triggerEvent(e.target.name, e.target.value)
+            });
+        })
+    </script>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script>
+            (function (window) {
+                "use strict";
+
+                function Model() {
+                    this.title = `Module 21 MERN Challenge Rubric (Coding)`;
+                    this.categories = [
+                        {
+                            title: "Deployment",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Student submitted deployed live application URL",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "Application isn't deployed",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application loads at deployed URL (no 404, or Heroku/Netlify error)",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "Application isn't deployed",
+                                    status: false
+                                },
+                                {
+                                    title: "Student submitted application GitHub URL",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "Missing GitHub submission URL",
+                                    status: false
+                                },
+                                {
+                                    title: "GitHub Repo contains application code",
+                                    score: 8,
+                                    type: "true false",
+                                    message: "GitHub repo is empty",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Technical Acceptance Criteria",
+                            subtitle:
+                                "Watch out for errors in the console from this point forward.",
+                            questions: [
+                                {
+                                    title:
+                                        "Application has replaced the RESTful API with a GraphQL API",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application has not replaced the RESTful API with a GraphQL API",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application has an Apollo Server that uses GraphQL queries and mutations to fetch and modify data",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application doesn't have an Apollo Server that uses GraphQL queries and mutations to fetch and modify data",
+                                    status: false
+                                },
+                                {
+                                    title: "Application includes the schema settings for resolvers and typeDefs outlined in the Challenge instructions",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't include any of the schema settings for resolvers and typeDefs outlined in the Challenge instructions",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application is missing most of the schema settings for resolvers and typeDefs outlined in the Challenge instructions",
+                                            score: 1
+                                        },
+                                        {
+                                            message: "Application is missing 1 of the schema settings for resolvers and typeDefs outlined in the Challenge instructions",
+                                            score: 3
+                                        },
+                                        {
+                                            message: "Application includes the schema settings for resolvers and typeDefs outlined in the Challenge instructions",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application's authentication middleware has been modified to work in the context of a GraphQL API",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application's authentication middleware has not been modified to work in the context of a GraphQL API",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application uses an Apollo Provider to communicate with the Apollo Server",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application doesn't use an Apollo Provider to communicate with the Apollo Server",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application allows users to sign up for an account and log in",
+                                    score: 5,
+                                    type: "true false",
+                                    message:
+                                        "Application doesn't allow users to sign up for an account and log in",
+                                    status: false
+                                },
+                                {
+                                    title: "Application allows users to search for books and save them to their account",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't allow users to search for books and save them to their account",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application allows users to search for books but doesn't allow them to save them to their account",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application allows users to search for books and save them to their account",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application allows users to view the books they've saved to their account and delete them from the Saved Books page",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't allow users to view the books they've saved to their account and delete them from the Saved Books page",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application allows users to view the books they've saved to their account from the Saved Books page but doesn't allow them to delete them",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application allows users to view the books they've saved to their account and delete them from the Saved Books page",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Repository Quality",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Repository has a unique name",
+                                    score: 2,
+                                    type: "true false",
+                                    message: "Repo doesn't have a unique name",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository follows best practices for file structure and naming conventions",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Code doesn't follow recommended file/folder structure",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Repository contains multiple descriptive commit messages",
+                                    score: 2,
+                                    type: "true false",
+                                    message:
+                                        "Repo doesn't contain multiple descriptive commit messages",
+                                    status: false
+                                },
+                                {
+                                    title: "Repository contains quality README with description, screenshot, link to deployed application",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Repository contains quality README with description, screenshot, link to deployed application",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Application Quality",
+                            subtitle: null,
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Application user experience is intuitive and easy to navigate",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application user experience is not intuitive and easy to navigate",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application user experience could be much more intuitive and easy to navigate",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application user experience could be a lot more intuitive and easy to navigate",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application user experience is intuitive and easy to navigate",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application user interface style is clean and polished",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application user interface is not clean and polished",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application user interface could be much better",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application user interface could be a bit better",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application user interface style is clean and polished",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                },
+                                {
+                                    title: "Application resembles the mock-up functionality provided in Challenge instructions",
+                                    score: 5,
+                                    type: "likert",
+                                    options: [
+                                        {
+                                            message: "Application doesn't resemble the mock-up functionality provided in Challenge instructions",
+                                            score: 0
+                                        },
+                                        {
+                                            message: "Application could resemble mock-up functionality provided with Challenge instructions much more closely",
+                                            score: 2
+                                        },
+                                        {
+                                            message: "Application could resemble mock-up functionality provided with Challenge instructions a bit more closely",
+                                            score: 4
+                                        },
+                                        {
+                                            message: "Application resembles the mock-up functionality provided in Challenge instructions",
+                                            score: 5
+                                        }
+                                    ],
+                                    message: "",
+                                    status: false
+                                }
+                            ]
+                        },
+                        {
+                            title: "Academic Integrity",
+                            subtitle:
+                                "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
+                            score: 0,
+                            questions: [
+                                {
+                                    title: "Application contains a low number of commits",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application codebase appears advanced for the student, at this point in the course, or in general",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application package.json file references a different author and/or repository",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                },
+                                {
+                                    title:
+                                        "Application package.json file references much older versions of npm packages",
+                                    score: 0,
+                                    type: "true false",
+                                    message: null,
+                                    status: false
+                                }
+                            ]
+                        }
+                    ];
+
+                    this.initStorage();
+                }
+
+                Model.prototype.initStorage = function () {
+                    this.score = 0;
+                    this.questions = [];
+                    this.messages = new Set();
+                    this.flagged = new Set();
+                    this.isDarkMode =
+                        JSON.parse(localStorage.getItem("isDarkMode")) || false;
+
+                    this.categories
+                        .forEach(category => {
+                            category.score = 0;
+                            category.questions.forEach(question => {
+                                question.category = category.title;
+                                question.status = false;
+                                question.id = this.questions.length;
+                                question.flagged = category.title === "Academic Integrity";
+                                question.bonus = category.title === "Bonus";
+                                if (question.type === "likert") {
+                                    question.message = question.options[0].message.split(" - ").pop();
+                                };
+
+                                let messageObj = {
+                                    message: question.message,
+                                    category: question.category
+                                };
+
+                                question.message && !question.bonus && this.messages.add(messageObj);
+                                this.questions.push(question);
+                                category.score += question.score;
+                            });
+                        });
+                };
+
+                Model.prototype.toggleDarkMode = function () {
+                    this.isDarkMode = !this.isDarkMode;
+                    localStorage.setItem("isDarkMode", this.isDarkMode);
+                };
+
+                Model.prototype.toggleRadio = function (radio) {
+                    const id = Number(radio.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+
+                    question.status = !question.status;
+
+                    if (question.status === true) {
+
+                        if (!question.bonus) {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        } else {
+                            let messageObj = {
+                                message: question.message,
+                                category: category
+                            }
+                            this.messages.add(messageObj);
+                        }
+
+                        this.score += question.score;
+
+                        if (question.flagged) {
+                            this.flagged.add(question.id);
+                        }
+                    } else {
+                        if (question.flagged) {
+                            this.flagged.delete(question.id);
+                        }
+
+                        if (!question.bonus) {
+                            if (question.message) {
+                                let messageObj = {
+                                    message: question.message,
+                                    category: category
+                                }
+                                this.messages.add(messageObj);
+                            }
+                        } else {
+                            this.messages.forEach(message => {
+                                if (message.message === question.message) {
+                                    this.messages.delete(message);
+                                }
+                            })
+                        }
+
+                        this.score -= question.score;
+                    }
+
+                    if (category !== "Academic Integrity") {
+                        if (question.status) {
+                            questionScore.textContent = question.score;
+                        } else {
+                            questionScore.textContent = 0;
+                        }
+
+                        const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                        let categoryTotal = 0;
+
+                        catQuestions.forEach(question => {
+                            if (question.type === "true false") {
+                                if (question.status) {
+                                    categoryTotal += question.score;
+                                }
+                            } else {
+                                let score = document.querySelector("select[name='" + question.id + "']").value;
+                                categoryTotal += parseInt(score);
+                            }
+                        })
+
+                        categoryScore.textContent = categoryTotal;
+                        resultsCategoryScore.textContent = categoryTotal;
+                    }
+
+                };
+
+                Model.prototype.handleSelect = function (select) {
+                    const id = Number(select.getAttribute("name"));
+                    const question = this.questions[id];
+                    const questionScore = document.querySelector("span[data-id='" + id + "']");
+                    const oldScore = questionScore.textContent;
+                    const category = question.category;
+                    const categoryScore = document.querySelector("span[data-cat='" + category + "']");
+                    const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
+                    let newMessage = select.selectedOptions[0].text.split(" - ").pop();
+
+                    if (newMessage === "Open this menu") {
+                        newMessage = select[1].text.split(" - ").pop();
+                    };
+
+                    if (question.type === "likert") {
+
+                        this.messages.forEach(item => {
+                            if (item.message === question.message) {
+                                this.messages.delete(item)
+                            }
+                        })
+
+                        question.message = newMessage;
+
+                        let messageObj = {
+                            message: newMessage,
+                            category: category
+                        }
+
+                        this.messages.add(messageObj);
+
+                    };
+
+
+
+                    this.score -= parseInt(oldScore);
+                    this.score += parseInt(select.value);
+                    questionScore.textContent = select.value;
+
+                    const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
+                    let categoryTotal = 0;
+
+                    catQuestions.forEach(question => {
+                        if (question.type === "true false") {
+                            if (question.status) {
+                                categoryTotal += question.score;
+                            }
+                        } else {
+                            let score = document.querySelector("select[name='" + question.id + "']").value;
+                            categoryTotal += parseInt(score);
+                        }
+                    })
+
+                    categoryScore.textContent = categoryTotal;
+                    resultsCategoryScore.textContent = categoryTotal;
+
+                };
+
+                Model.prototype.calcGrade = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.score));
+
+                    let letterGrade = "";
+
+                    if (roundedScore >= 99.5) {
+                        letterGrade = "A+";
+                    } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
+                        letterGrade = "A";
+                    } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
+                        letterGrade = "A-";
+                    } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
+                        letterGrade = "B+";
+                    } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
+                        letterGrade = "B";
+                    } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
+                        letterGrade = "B-";
+                    } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
+                        letterGrade = "C+";
+                    } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
+                        letterGrade = "C";
+                    } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
+                        letterGrade = "C-";
+                    } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
+                        letterGrade = "D+";
+                    } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
+                        letterGrade = "D";
+                    } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
+                        letterGrade = "D-";
+                    } else {
+                        letterGrade = "F";
+                    }
+
+                    return letterGrade;
+                };
+
+                Model.prototype.hasFlags = function () {
+                    return this.flagged.size > 0;
+                };
+
+                function View(model) {
+                    this.model = model;
+                    this.generateHTML();
+                    this.cacheDom();
+                }
+
+                View.prototype.cacheDom = function () {
+                    this.darkModeToggle = document.querySelector("#dark-mode");
+
+                    this.radios = Array.from(
+                        document.querySelectorAll("input[type='radio']")
+                    );
+
+                    this.selects = Array.from(
+                        document.querySelectorAll("select")
+                    );
+
+                    this.totals = document.querySelector(".totals");
+                    this.grade = document.querySelector(".grade");
+                    this.alert = document.querySelector("div[role='alert']");
+                    this.summaryHeading = document.querySelector(".summary-heading");
+                    this.summary = document.querySelector(".summary");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let summaryName = summary + "-summary";
+                            return this[summaryName] = document.querySelector("." + summary);
+                        })
+
+                    this.resetButton = document.querySelector(".reset");
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .map(category => {
+                            let button = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + button;
+                            return this[buttonName] = document.querySelector(".copy-" + button);
+                        })
+                };
+
+                View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
+                    return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
+          </legend>
+          ${type === "true false" ?
+                            `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>` :
+                            `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
+            </select>
+          </div>`
+                        }
+        </fieldset>`;
+                };
+
+                View.prototype.createCard = function ({ title, subtitle, questions, score }) {
+                    return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ""}
+          ${questions.map(child => this.createFieldset(child)).join("")}
+          </div>`;
+                };
+
+                View.prototype.createResults = function () {
+                    return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .map(category => {
+                                let score = category.score;
+                                let title = category.title;
+                                let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+                                return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
+                                    .filter(messageObj => messageObj.category === category.title)
+                                    .map(messageObj => `* ${messageObj.message}\n`)
+                                    .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
+                            })
+                            .join("")}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+                };
+
+                View.prototype.createHeader = function (title, isDarkMode = false) {
+                    return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
+                        }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>`;
+                };
+
+                View.prototype.generateHTML = function () {
+                    this.wrapper = document.querySelector("#app");
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    }
+
+                    const survey = this.model.categories.reduce((final, current) => {
+                        final += this.createCard(current);
+                        return final;
+                    }, "");
+
+                    const results = this.createResults();
+
+                    const header = this.createHeader(
+                        this.model.title,
+                        this.model.isDarkMode
+                    );
+
+                    this.wrapper.innerHTML = header + survey + results;
+                };
+
+                View.prototype.update = function () {
+                    const roundedScore = Math.min(100, Math.ceil(this.model.score));
+                    this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+                    const letterGrade = this.model.calcGrade();
+                    this.grade.textContent = letterGrade;
+
+                    if (this.model.hasFlags()) {
+                        this.alert.classList.remove("d-none");
+                    } else {
+                        this.alert.classList.add("d-none");
+                    }
+
+                    if (this.model.isDarkMode) {
+                        this.wrapper.classList.add("dark");
+                    } else {
+                        this.wrapper.classList.remove("dark");
+                    }
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+                            this[catClass + "-summary"].innerHTML = [...this.model.messages]
+                                .filter(messageObj => messageObj.category === category.title)
+                                .map(messageObj => `* ${messageObj.message}\n`)
+                                .join("")
+                        })
+
+                };
+
+                function Controller(model, view) {
+                    this.model = model;
+                    this.view = view;
+                    this.addEventListeners();
+                }
+
+                Controller.prototype.addEventListeners = function () {
+                    this.view.wrapper.addEventListener("change", event => {
+                        const { target } = event;
+
+                        if (target.name !== "dark-mode") {
+                            if (!target.matches("input[type='radio']")) {
+                                this.model.handleSelect(target);
+                            } else {
+                                this.model.toggleRadio(target);
+                            }
+                        }
+
+                        this.view.update();
+                    });
+
+                    this.view.resetButton.addEventListener("click", () => {
+                        this.model.initStorage();
+
+                        this.view.radios.forEach(
+                            radio => (radio.checked = radio.hasAttribute("checked"))
+                        );
+
+                        this.view.selects.forEach(
+                            select => select.selectedIndex = null
+                        )
+
+                        this.model.questions
+                            .filter(question => question.category !== "Academic Integrity")
+                            .forEach(question => {
+                                const id = question.id;
+                                const questionScore = document.querySelector("span[data-id='" + id + "']");
+                                questionScore.textContent = 0;
+                            });
+
+                        this.model.categories
+                            .filter(category => category.title !== "Academic Integrity")
+                            .forEach(category => {
+                                const title = category.title
+                                const categoryScore = document.querySelector("span[data-cat='" + title + "']");
+                                const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
+                                categoryScore.textContent = 0;
+                                resultsCategoryScore.textContent = 0;
+                            });
+
+                        this.view.update();
+                        window.scrollTo(0, 0);
+                    });
+
+                    this.model.categories
+                        .filter(category => category.title !== "Academic Integrity")
+                        .forEach(category => {
+                            let title = category.title.replace(/\s+/g, '-').toLowerCase();
+                            let buttonName = "copyButton-" + title;
+
+                            let summaryName = title + "-summary";
+
+                            this.view[buttonName].addEventListener("click", () => {
+                                const selection = window.getSelection();
+                                const range = document.createRange();
+                                range.selectNodeContents(this.view[summaryName]);
+                                selection.removeAllRanges();
+                                selection.addRange(range);
+                                document.execCommand("copy");
+                            });
+                        });
+
+
+                    this.view.darkModeToggle.addEventListener("change", () => {
+                        this.model.toggleDarkMode();
+                        this.view.update();
+                    });
+                };
+
+                function App(Model, View, Controller) {
+                    this.model = new Model();
+                    this.view = new View(this.model);
+                    this.controller = new Controller(this.model, this.view);
+                }
+
+                return new App(Model, View, Controller);
+            })(window);
+    </script>
+</body>
+
+</html>

+ 1403 - 0
rubrics/mod22.html

@@ -0,0 +1,1403 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
+    <link
+      href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css"
+      rel="stylesheet"
+    />
+    <title>Module 22 State Challenge Rubric</title>
+    <style>
+      *,
+      *::before,
+      *::after {
+        box-sizing: border-box;
+      }
+
+      html,
+      body {
+        margin: 0;
+        padding: 0;
+        line-height: 1.15;
+        font-size: 100%;
+        color: #fefefe;
+        transition: all 0.4s;
+      }
+
+      body {
+        background-color: #fbfcfd;
+        font-family: 'Inter', sans-serif;
+        color: #4a4a4a;
+      }
+
+      #app.dark {
+        background-color: #121212;
+        color: #e2e2e2;
+      }
+
+      h1 {
+        font-size: 1.9rem;
+      }
+
+      h2 {
+        font-size: 1.6rem;
+      }
+
+      h3 {
+        font-size: 1.2rem;
+      }
+
+      h4 {
+        font-size: 0.9rem;
+      }
+
+      p,
+      legend {
+        font-size: 1.2rem;
+      }
+
+      label {
+        font-size: 0.9rem;
+        line-height: 2;
+      }
+
+      fieldset {
+        border: none;
+        padding: 0;
+        margin-top: 0.9rem;
+      }
+
+      input[type='radio'],
+      input[type='checkbox'] {
+        position: absolute !important;
+        height: 1px;
+        width: 1px;
+        overflow: hidden;
+        clip: rect(1px 1px 1px 1px);
+        clip: rect(1px, 1px, 1px, 1px);
+        white-space: nowrap;
+      }
+
+      .select-lg,
+      .select-lg option {
+        font-family: 'Inter', sans-serif;
+        padding: 10px;
+      }
+
+      .narrow {
+        width: 800px;
+      }
+
+      #app {
+        padding: 3%;
+        transition: all 0.4s;
+      }
+
+      #app h1 {
+        font-size: 1.9rem;
+        font-weight: 700;
+        line-height: 1.3;
+      }
+
+      header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+      }
+
+      .switch-group {
+        display: flex;
+        align-items: center;
+        height: 2.25rem;
+        width: 4.5rem;
+      }
+
+      input[type='checkbox'] + .toggle {
+        height: 100%;
+        width: 100%;
+        background-color: #77a1db;
+        border-radius: 1.9rem;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        padding: 0.35rem;
+        cursor: pointer;
+        overflow: hidden;
+        background-position: 25% 125%;
+        transition: all 0.5s;
+      }
+
+      input[type='checkbox']:focus + .toggle {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      input[type='checkbox']:checked + .toggle {
+        background-color: rgb(85, 89, 204);
+        background-size: 1rem;
+        background-repeat: no-repeat;
+        background-position: 25%;
+        background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==');
+        transition: all 0.5s;
+      }
+
+      input[type='checkbox'] + .toggle::after {
+        content: '';
+        display: block;
+        height: 1.7rem;
+        width: 1.7rem;
+        border-radius: 50%;
+        cursor: pointer;
+        border: 4px solid #e1be64;
+        background-color: #fddf74;
+        transform: translateX(0rem);
+        transition: all 0.5s;
+        background-position: center bottom;
+      }
+
+      input[type='checkbox']:hover + .toggle::after {
+        transform: translateX(0rem) scale(1.1);
+      }
+
+      input[type='checkbox']:checked + .toggle::after {
+        border: 4px solid #ffffff;
+        background-color: rgba(255, 255, 255, 0.4);
+        transform: translateX(2.1rem);
+        transition: all 0.5s;
+        background-position: center;
+        background-size: 60%;
+        background-repeat: no-repeat;
+        background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
+      }
+
+      input[type='checkbox']:checked:hover + .toggle::after {
+        transform: translateX(2.1rem) scale(1.075);
+      }
+
+      .card {
+        background-color: #fff;
+        border-radius: 6px;
+        border-width: 0;
+        box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
+        font-size: 1.1rem;
+        height: 100%;
+        line-height: 1.8rem;
+        padding: 1.5rem 2rem;
+        width: 100%;
+        margin-bottom: 1.9rem;
+      }
+
+      .dark .card {
+        background-color: #333333;
+        box-shadow: none;
+      }
+
+      .card h1,
+      .card h2,
+      .card h3,
+      .card h4,
+      .card h5,
+      .card h6 {
+        color: #4a4a4a;
+      }
+
+      .dark .card h1,
+      .dark .card h2,
+      .dark .card h3,
+      .dark .card h4,
+      .dark .card h5,
+      .dark .card h6 {
+        color: #e2e2e2;
+      }
+
+      .card-header h2 {
+        display: inline-block;
+      }
+
+      .card .cat-score {
+        margin-left: 20px;
+        font-size: smaller;
+      }
+
+      .card h3 {
+        font-size: 1.2rem;
+        font-weight: 700;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card p,
+      .card label,
+      .card legend {
+        color: #7a869c;
+        border-width: 0;
+        display: block;
+        font-size: 1.1rem;
+        font-weight: 300;
+        line-height: 1.8em;
+        margin: 0;
+        padding: 0;
+      }
+
+      .card legend {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+      }
+
+      .card legend .score {
+        font-size: smaller;
+        margin-left: 20px;
+        font-weight: 100;
+        min-width: 5%;
+      }
+
+      .dark .card p,
+      .dark .card label,
+      .dark .card legend {
+        color: #e2e2e2;
+      }
+
+      .card label {
+        display: inline-block;
+      }
+
+      .card input[type='radio'] + label::before {
+        content: '';
+        height: 0.9rem;
+        width: 0.9rem;
+        margin-right: 0.7rem;
+        border-radius: 50%;
+        border: 1px solid rgb(74, 74, 74);
+        background-color: #ffffff;
+        display: inline-block;
+        cursor: pointer;
+        position: relative;
+        top: 0.09rem;
+      }
+
+      .dark .card input[type='radio'] + label::before {
+        border: 1px solid #e2e2e2;
+        background-color: inherit;
+      }
+
+      .card input[type='radio']:checked + label::before {
+        background-color: rgb(47, 106, 189);
+        border-color: rgb(47, 106, 189);
+      }
+
+      .dark .card input[type='radio']:checked + label::before {
+        background-color: #d9d9d9;
+        border-color: #d9d9d9;
+      }
+
+      .card input[type='radio']:checked + label {
+        font-weight: bold;
+        color: #4a4a4a;
+      }
+
+      .dark input[type='radio']:checked + label {
+        color: #e2e2e2;
+      }
+
+      .card input[type='radio']:focus + label::before {
+        outline: rgb(59, 153, 252) auto 5px;
+      }
+
+      .card legend {
+        margin-bottom: 0.4rem;
+      }
+
+      .totals {
+        font-size: 1.6rem;
+      }
+
+      .totals + h2 {
+        font-size: 1.9rem;
+        margin-top: 0;
+      }
+
+      .grade {
+        font-size: 1.9rem;
+      }
+
+      button {
+        background-color: #fff;
+        border: 1px solid #4a4a4a;
+        border-radius: 6px;
+        box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
+        color: #4a4a4a;
+        cursor: pointer;
+        display: inline-block;
+        font-size: 1rem;
+        font-weight: 500;
+        letter-spacing: 0.04em;
+        line-height: 1.8em;
+        margin: 0.75rem 1.1rem 1.1rem 0;
+        outline: none;
+        padding: 0.5rem 1.4rem;
+        text-transform: uppercase;
+        transition-delay: 0s;
+        transition-duration: 0.25s;
+        transition-property: all;
+        transition-timing-function: ease-out;
+        width: fit-content;
+      }
+
+      .dark button {
+        background: #d9d9d9;
+        color: #121212;
+        box-shadow: none;
+      }
+
+      button:hover {
+        border-color: #dbdbdb;
+        color: #dbdbdb;
+      }
+
+      .dark button:hover {
+        color: #121212;
+      }
+
+      button.copy {
+        margin-left: 0.4rem;
+        font-size: 0.8rem;
+      }
+
+      button:focus {
+        border-color: rgb(56, 56, 56);
+        box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
+        color: rgb(56, 56, 56);
+      }
+
+      div[role='alert'] {
+        color: #4a4a4a;
+        font-size: 1.2rem;
+        margin-bottom: 1.2rem;
+      }
+
+      .dark div[role='alert'] {
+        color: #e2e2e2;
+      }
+
+      .summary {
+        font-size: 1.2rem;
+        border: none;
+        font-family: monospace;
+        background-color: #d5d9de;
+        padding: 0.9rem;
+        border-radius: 6px;
+        line-height: 1.5;
+        width: 100%;
+      }
+
+      .summary h2,
+      .dark .summary h2 {
+        font-size: 1.2rem;
+        font-family: 'Inter', sans-serif;
+        margin-top: 35px;
+        margin-bottom: 5px;
+      }
+
+      .summary h2:first-of-type,
+      .dark .summary h2:first-of-type {
+        margin-top: 15px;
+      }
+
+      .dark .summary {
+        background: #6b6a6a;
+        color: #e2e2e2;
+      }
+
+      .summary pre,
+      .dark .summary pre {
+        white-space: pre-wrap;
+        border-radius: 5px;
+        padding: 15px;
+        border: solid 2px rgba(0, 0, 0, 0.4);
+      }
+
+      .summary pre:empty {
+        display: none;
+      }
+
+      .summary pre:empty + button {
+        display: none;
+      }
+
+      .d-none {
+        display: none;
+      }
+
+      @media (max-width: 450px) {
+        .btn-container {
+          text-align: center;
+        }
+
+        button {
+          width: 100%;
+          margin: 0.5rem;
+        }
+
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.3rem);
+          background-size: 50%;
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+          background-size: 50%;
+        }
+      }
+
+      @media (max-width: 515px) {
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.7rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.7rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 460px) {
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.4rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.4rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 400px) {
+        #app h1 {
+          font-size: 1.6rem;
+        }
+
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(1.3rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(1.3rem) scale(1.1);
+        }
+      }
+
+      @media (max-width: 340px) {
+        input[type='checkbox']:checked + .toggle::after {
+          transform: translateX(0.9rem);
+        }
+
+        input[type='checkbox']:checked:hover + .toggle::after {
+          transform: translateX(0.9rem) scale(1.075);
+        }
+      }
+    </style>
+    <script>
+      $(function () {
+        let $select2 = $('.select2').select2({
+          containerCssClass: 'wrap',
+        });
+
+        function triggerEvent(name, val) {
+          let selected = $("select[name='" + name + "']");
+          let change = new Event('change', { bubbles: true });
+          selected[0].dispatchEvent(change);
+        }
+
+        $('.select2').on('select2:select', function (e) {
+          triggerEvent(e.target.name, e.target.value);
+        });
+      });
+    </script>
+  </head>
+
+  <body>
+    <div id="app"></div>
+    <script>
+      (function (window) {
+        'use strict';
+
+        function Model() {
+          this.title = `Module 22 State Challenge Rubric (Coding)`;
+          this.categories = [
+            {
+              title: 'Deployment',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: 'Student submitted deployed live application URL',
+                  score: 8,
+                  type: 'true false',
+                  message: "Application isn't deployed",
+                  status: false,
+                },
+                {
+                  title:
+                    'Application loads at deployed URL (no 404, or Heroku/Netlify error)',
+                  score: 8,
+                  type: 'true false',
+                  message: "Application isn't deployed",
+                  status: false,
+                },
+                {
+                  title: 'Student submitted application GitHub URL',
+                  score: 8,
+                  type: 'true false',
+                  message: 'Missing GitHub submission URL',
+                  status: false,
+                },
+                {
+                  title: 'GitHub Repo contains application code',
+                  score: 8,
+                  type: 'true false',
+                  message: 'GitHub repo is empty',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Technical Acceptance Criteria',
+              subtitle:
+                'Watch out for errors in the console from this point forward.',
+              questions: [
+                {
+                  title:
+                    'Application uses the redux and react-redux npm packages',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    "Application doesn't use the redux and react-redux npm packages",
+                  status: false,
+                },
+                {
+                  title:
+                    'Application uses a Redux store and imports createStore from the redux package',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    "Application doesn't use a Redux store or uses the Context API for its store",
+                  status: false,
+                },
+                {
+                  title:
+                    'Application uses a Redux Provider imported from react-redux',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    "Application doesn't use a Redux Provider or uses a Context API Provider",
+                  status: false,
+                },
+                {
+                  title: 'Application passes reducers to a Redux store',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    "Application doesn't pass reducers to a Redux store or uses the Context API",
+                  status: false,
+                },
+                {
+                  title:
+                    'Application uses the useSelector Hook to extract state data from the store',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    "Application doesn't use the useSelector Hook to extract state data from the store or uses the Context API",
+                  status: false,
+                },
+                {
+                  title:
+                    'Application uses the useDispatch Hook to dispatch actions',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    "Application doesn't use the useDispatch Hook to dispatch actions or uses the Context API",
+                  status: false,
+                },
+                {
+                  title:
+                    'Application retains its original signup and login functionality',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    'Users are no longer able to sign up or log in to the application',
+                  status: false,
+                },
+                {
+                  title:
+                    'Application retains its original shopping cart functionality',
+                  score: 5,
+                  type: 'true false',
+                  message:
+                    "Users are no longer able to use the application's shopping cart",
+                },
+              ],
+            },
+            {
+              title: 'Repository Quality',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title: 'Repository has a unique name',
+                  score: 2,
+                  type: 'true false',
+                  message: "Repo doesn't have a unique name",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository follows best practices for file structure and naming conventions',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    "Code doesn't follow recommended file/folder structure",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository contains multiple descriptive commit messages',
+                  score: 2,
+                  type: 'true false',
+                  message:
+                    "Repo doesn't contain multiple descriptive commit messages",
+                  status: false,
+                },
+                {
+                  title:
+                    'Repository contains quality README with description, screenshot, link to deployed application',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Repository README is missing 2 of the following: description, screenshot, or link to deployed application',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Repository README is missing 1 of the following: description, screenshot, or link to deployed application',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'Repository contains quality README with description, screenshot, link to deployed application',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Application Quality',
+              subtitle: null,
+              score: 0,
+              questions: [
+                {
+                  title:
+                    'Application user experience is intuitive and easy to navigate',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'Application user experience is not intuitive and easy to navigate',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application user experience could be much more intuitive and easy to navigate',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application user experience could be a lot more intuitive and easy to navigate',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'Application user experience is intuitive and easy to navigate',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Application user interface style is clean and polished',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        'Application user interface is not clean and polished',
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application user interface could be much better',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application user interface could be a bit better',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'Application user interface style is clean and polished',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+                {
+                  title:
+                    'Application resembles the mock-up functionality provided in Challenge instructions',
+                  score: 5,
+                  type: 'likert',
+                  options: [
+                    {
+                      message:
+                        "Application doesn't resemble the mock-up functionality provided in Challenge instructions",
+                      score: 0,
+                    },
+                    {
+                      message:
+                        'Application could resemble mock-up functionality provided with Challenge instructions much more closely',
+                      score: 2,
+                    },
+                    {
+                      message:
+                        'Application could resemble mock-up functionality provided with Challenge instructions a bit more closely',
+                      score: 4,
+                    },
+                    {
+                      message:
+                        'Application resembles the mock-up functionality provided in Challenge instructions',
+                      score: 5,
+                    },
+                  ],
+                  message: '',
+                  status: false,
+                },
+              ],
+            },
+            {
+              title: 'Academic Integrity',
+              subtitle:
+                'If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.',
+              score: 0,
+              questions: [
+                {
+                  title: 'Application contains a low number of commits',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+                {
+                  title:
+                    'Application codebase appears advanced for the student, at this point in the course, or in general',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+                {
+                  title:
+                    'Application package.json file references a different author and/or repository',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+                {
+                  title:
+                    'Application package.json file references much older versions of npm packages',
+                  score: 0,
+                  type: 'true false',
+                  message: null,
+                  status: false,
+                },
+              ],
+            },
+          ];
+
+          this.initStorage();
+        }
+
+        Model.prototype.initStorage = function () {
+          this.score = 0;
+          this.questions = [];
+          this.messages = new Set();
+          this.flagged = new Set();
+          this.isDarkMode =
+            JSON.parse(localStorage.getItem('isDarkMode')) || false;
+
+          this.categories.forEach((category) => {
+            category.score = 0;
+            category.questions.forEach((question) => {
+              question.category = category.title;
+              question.status = false;
+              question.id = this.questions.length;
+              question.flagged = category.title === 'Academic Integrity';
+              question.bonus = category.title === 'Bonus';
+              if (question.type === 'likert') {
+                question.message = question.options[0].message
+                  .split(' - ')
+                  .pop();
+              }
+
+              let messageObj = {
+                message: question.message,
+                category: question.category,
+              };
+
+              question.message &&
+                !question.bonus &&
+                this.messages.add(messageObj);
+              this.questions.push(question);
+              category.score += question.score;
+            });
+          });
+        };
+
+        Model.prototype.toggleDarkMode = function () {
+          this.isDarkMode = !this.isDarkMode;
+          localStorage.setItem('isDarkMode', this.isDarkMode);
+        };
+
+        Model.prototype.toggleRadio = function (radio) {
+          const id = Number(radio.getAttribute('name'));
+          const question = this.questions[id];
+          const questionScore = document.querySelector(
+            "span[data-id='" + id + "']"
+          );
+          const category = question.category;
+          const categoryScore = document.querySelector(
+            "span[data-cat='" + category + "']"
+          );
+          const resultsCategoryScore = document.querySelector(
+            "span[data-res-cat='" + category + "']"
+          );
+
+          question.status = !question.status;
+
+          if (question.status === true) {
+            if (!question.bonus) {
+              this.messages.forEach((message) => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              });
+            } else {
+              let messageObj = {
+                message: question.message,
+                category: category,
+              };
+              this.messages.add(messageObj);
+            }
+
+            this.score += question.score;
+
+            if (question.flagged) {
+              this.flagged.add(question.id);
+            }
+          } else {
+            if (question.flagged) {
+              this.flagged.delete(question.id);
+            }
+
+            if (!question.bonus) {
+              if (question.message) {
+                let messageObj = {
+                  message: question.message,
+                  category: category,
+                };
+                this.messages.add(messageObj);
+              }
+            } else {
+              this.messages.forEach((message) => {
+                if (message.message === question.message) {
+                  this.messages.delete(message);
+                }
+              });
+            }
+
+            this.score -= question.score;
+          }
+
+          if (category !== 'Academic Integrity') {
+            if (question.status) {
+              questionScore.textContent = question.score;
+            } else {
+              questionScore.textContent = 0;
+            }
+
+            const catQuestions = this.categories.filter(
+              (cat) => cat.title === category
+            )[0].questions;
+            let categoryTotal = 0;
+
+            catQuestions.forEach((question) => {
+              if (question.type === 'true false') {
+                if (question.status) {
+                  categoryTotal += question.score;
+                }
+              } else {
+                let score = document.querySelector(
+                  "select[name='" + question.id + "']"
+                ).value;
+                categoryTotal += parseInt(score);
+              }
+            });
+
+            categoryScore.textContent = categoryTotal;
+            resultsCategoryScore.textContent = categoryTotal;
+          }
+        };
+
+        Model.prototype.handleSelect = function (select) {
+          const id = Number(select.getAttribute('name'));
+          const question = this.questions[id];
+          const questionScore = document.querySelector(
+            "span[data-id='" + id + "']"
+          );
+          const oldScore = questionScore.textContent;
+          const category = question.category;
+          const categoryScore = document.querySelector(
+            "span[data-cat='" + category + "']"
+          );
+          const resultsCategoryScore = document.querySelector(
+            "span[data-res-cat='" + category + "']"
+          );
+          let newMessage = select.selectedOptions[0].text.split(' - ').pop();
+
+          if (newMessage === 'Open this menu') {
+            newMessage = select[1].text.split(' - ').pop();
+          }
+
+          if (question.type === 'likert') {
+            this.messages.forEach((item) => {
+              if (item.message === question.message) {
+                this.messages.delete(item);
+              }
+            });
+
+            question.message = newMessage;
+
+            let messageObj = {
+              message: newMessage,
+              category: category,
+            };
+
+            this.messages.add(messageObj);
+          }
+
+          this.score -= parseInt(oldScore);
+          this.score += parseInt(select.value);
+          questionScore.textContent = select.value;
+
+          const catQuestions = this.categories.filter(
+            (cat) => cat.title === category
+          )[0].questions;
+          let categoryTotal = 0;
+
+          catQuestions.forEach((question) => {
+            if (question.type === 'true false') {
+              if (question.status) {
+                categoryTotal += question.score;
+              }
+            } else {
+              let score = document.querySelector(
+                "select[name='" + question.id + "']"
+              ).value;
+              categoryTotal += parseInt(score);
+            }
+          });
+
+          categoryScore.textContent = categoryTotal;
+          resultsCategoryScore.textContent = categoryTotal;
+        };
+
+        Model.prototype.calcGrade = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.score));
+
+          let letterGrade = '';
+
+          if (roundedScore >= 97) {
+            letterGrade = 'A+';
+          } else if (roundedScore >= 93 && roundedScore <= 96) {
+            letterGrade = 'A';
+          } else if (roundedScore >= 90 && roundedScore <= 92) {
+            letterGrade = 'A-';
+          } else if (roundedScore >= 87 && roundedScore <= 89) {
+            letterGrade = 'B+';
+          } else if (roundedScore >= 83 && roundedScore <= 86) {
+            letterGrade = 'B';
+          } else if (roundedScore >= 80 && roundedScore <= 82) {
+            letterGrade = 'B-';
+          } else if (roundedScore >= 77 && roundedScore <= 79) {
+            letterGrade = 'C+';
+          } else if (roundedScore >= 73 && roundedScore <= 76) {
+            letterGrade = 'C';
+          } else if (roundedScore >= 70 && roundedScore <= 72) {
+            letterGrade = 'C-';
+          } else if (roundedScore >= 67 && roundedScore <= 69) {
+            letterGrade = 'D+';
+          } else if (roundedScore >= 63 && roundedScore <= 66) {
+            letterGrade = 'D';
+          } else if (roundedScore >= 60 && roundedScore <= 62) {
+            letterGrade = 'D-';
+          } else {
+            letterGrade = 'F';
+          }
+
+          return letterGrade;
+        };
+
+        Model.prototype.hasFlags = function () {
+          return this.flagged.size > 0;
+        };
+
+        function View(model) {
+          this.model = model;
+          this.generateHTML();
+          this.cacheDom();
+        }
+
+        View.prototype.cacheDom = function () {
+          this.darkModeToggle = document.querySelector('#dark-mode');
+
+          this.radios = Array.from(
+            document.querySelectorAll("input[type='radio']")
+          );
+
+          this.selects = Array.from(document.querySelectorAll('select'));
+
+          this.totals = document.querySelector('.totals');
+          this.grade = document.querySelector('.grade');
+          this.alert = document.querySelector("div[role='alert']");
+          this.summaryHeading = document.querySelector('.summary-heading');
+          this.summary = document.querySelector('.summary');
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .map((category) => {
+              let summary = category.title.replace(/\s+/g, '-').toLowerCase();
+              let summaryName = summary + '-summary';
+              return (this[summaryName] = document.querySelector(
+                '.' + summary
+              ));
+            });
+
+          this.resetButton = document.querySelector('.reset');
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .map((category) => {
+              let button = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = 'copyButton-' + button;
+              return (this[buttonName] = document.querySelector(
+                '.copy-' + button
+              ));
+            });
+        };
+
+        View.prototype.createFieldset = function ({
+          title,
+          id,
+          score,
+          status,
+          category,
+          type,
+          options,
+        }) {
+          return `<fieldset class="input-group">
+          <legend>
+            <span class="title">${title}</span> ${
+            category === 'Academic Integrity'
+              ? ''
+              : "<span class='score'><span data-id='" +
+                id +
+                "' class='current-score'>0</span> / " +
+                score +
+                '</span>'
+          }
+          </legend>
+          ${
+            type === 'true false'
+              ? `<div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-true" value="true">
+            <label for="${id}-true">True</label>
+          </div>
+          <div class="radio-group">
+            <input type="radio" name="${id}" id="${id}-false" value="false" checked>
+            <label for="${id}-false">False</label>
+          </div>`
+              : `<div class="select-group">
+            <select name="${id}" class="select-lg select2 narrow wrap">
+              <option value="0" selected>Open this menu</option>
+              ${options.map(
+                (option) =>
+                  `<option value="${option.score}">${option.score} - ${option.message}</option>`
+              )}
+            </select>
+          </div>`
+          }
+        </fieldset>`;
+        };
+
+        View.prototype.createCard = function ({
+          title,
+          subtitle,
+          questions,
+          score,
+        }) {
+          return `<div class="card">
+          <div class="card-header"><h2>${title}</h2> ${
+            title !== 'Academic Integrity'
+              ? "<span class='cat-score'><span data-cat='" +
+                title +
+                "'>0</span> / " +
+                score +
+                '</span>'
+              : ''
+          }</div>
+          ${subtitle ? `<h3>${subtitle}</h3>` : ''}
+          ${questions.map((child) => this.createFieldset(child)).join('')}
+          </div>`;
+        };
+
+        View.prototype.createResults = function () {
+          return `<div class="card">
+        <h2>Recommended Grade: <span class="grade">F</span></h2>
+        <h2 class="totals">Total: (0/100)</h2>
+        <div class="d-none" role="alert">
+          You've selected <strong>True</strong> for one of more Academic
+          Integrity prompts and are advised to investigate plagiarism before
+          issuing a grade.
+        </div>
+        <div class="summary" aria-label="summary">
+        ${this.model.categories
+          .filter((category) => category.title !== 'Academic Integrity')
+          .map((category) => {
+            let score = category.score;
+            let title = category.title;
+            let catClass = title.replace(/\s+/g, '-').toLowerCase();
+
+            return (
+              '<h2>' +
+              title +
+              "<span class='cat-score'><span data-res-cat='" +
+              title +
+              "'>0</span> / " +
+              score +
+              "</span></h2><pre class='" +
+              catClass +
+              "'>" +
+              [...this.model.messages]
+                .filter((messageObj) => messageObj.category === category.title)
+                .map((messageObj) => `* ${messageObj.message}\n`)
+                .join('') +
+              "</pre><button class='copy copy-" +
+              catClass +
+              "'>Copy To Clipboard</button>"
+            );
+          })
+          .join('')}
+        </div>
+        <div class="btn-container">
+          <button class="reset">Reset Form</button>
+        </div>
+      </div>`;
+        };
+
+        View.prototype.createHeader = function (title, isDarkMode = false) {
+          return `<header>
+            <h1>${title}</h1>
+            <div class="switch-group">
+              <input type="checkbox" name="dark-mode" id="dark-mode" ${
+                isDarkMode ? 'checked' : ''
+              }>
+              <label aria-label="dark mode" for="dark-mode" class="toggle">
+              </label>
+            </div>
+          </header>
+          <h2>Note: This is an optional assignment for students, and they are not required to complete it.</h2>`;
+        };
+
+        View.prototype.generateHTML = function () {
+          this.wrapper = document.querySelector('#app');
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add('dark');
+          }
+
+          const survey = this.model.categories.reduce((final, current) => {
+            final += this.createCard(current);
+            return final;
+          }, '');
+
+          const results = this.createResults();
+
+          const header = this.createHeader(
+            this.model.title,
+            this.model.isDarkMode
+          );
+
+          this.wrapper.innerHTML = header + survey + results;
+        };
+
+        View.prototype.update = function () {
+          const roundedScore = Math.min(100, Math.ceil(this.model.score));
+          this.totals.textContent = `Total: (${roundedScore}/ 100)`;
+
+          const letterGrade = this.model.calcGrade();
+          this.grade.textContent = letterGrade;
+
+          if (this.model.hasFlags()) {
+            this.alert.classList.remove('d-none');
+          } else {
+            this.alert.classList.add('d-none');
+          }
+
+          if (this.model.isDarkMode) {
+            this.wrapper.classList.add('dark');
+          } else {
+            this.wrapper.classList.remove('dark');
+          }
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .forEach((category) => {
+              let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
+
+              this[catClass + '-summary'].innerHTML = [...this.model.messages]
+                .filter((messageObj) => messageObj.category === category.title)
+                .map((messageObj) => `* ${messageObj.message}\n`)
+                .join('');
+            });
+        };
+
+        function Controller(model, view) {
+          this.model = model;
+          this.view = view;
+          this.addEventListeners();
+        }
+
+        Controller.prototype.addEventListeners = function () {
+          this.view.wrapper.addEventListener('change', (event) => {
+            const { target } = event;
+
+            if (target.name !== 'dark-mode') {
+              if (!target.matches("input[type='radio']")) {
+                this.model.handleSelect(target);
+              } else {
+                this.model.toggleRadio(target);
+              }
+            }
+
+            this.view.update();
+          });
+
+          this.view.resetButton.addEventListener('click', () => {
+            this.model.initStorage();
+
+            this.view.radios.forEach(
+              (radio) => (radio.checked = radio.hasAttribute('checked'))
+            );
+
+            this.view.selects.forEach(
+              (select) => (select.selectedIndex = null)
+            );
+
+            this.model.questions
+              .filter((question) => question.category !== 'Academic Integrity')
+              .forEach((question) => {
+                const id = question.id;
+                const questionScore = document.querySelector(
+                  "span[data-id='" + id + "']"
+                );
+                questionScore.textContent = 0;
+              });
+
+            this.model.categories
+              .filter((category) => category.title !== 'Academic Integrity')
+              .forEach((category) => {
+                const title = category.title;
+                const categoryScore = document.querySelector(
+                  "span[data-cat='" + title + "']"
+                );
+                const resultsCategoryScore = document.querySelector(
+                  "span[data-res-cat='" + title + "']"
+                );
+                categoryScore.textContent = 0;
+                resultsCategoryScore.textContent = 0;
+              });
+
+            this.view.update();
+            window.scrollTo(0, 0);
+          });
+
+          this.model.categories
+            .filter((category) => category.title !== 'Academic Integrity')
+            .forEach((category) => {
+              let title = category.title.replace(/\s+/g, '-').toLowerCase();
+              let buttonName = 'copyButton-' + title;
+
+              let summaryName = title + '-summary';
+
+              this.view[buttonName].addEventListener('click', () => {
+                const selection = window.getSelection();
+                const range = document.createRange();
+                range.selectNodeContents(this.view[summaryName]);
+                selection.removeAllRanges();
+                selection.addRange(range);
+                document.execCommand('copy');
+              });
+            });
+
+          this.view.darkModeToggle.addEventListener('change', () => {
+            this.model.toggleDarkMode();
+            this.view.update();
+          });
+        };
+
+        function App(Model, View, Controller) {
+          this.model = new Model();
+          this.view = new View(this.model);
+          this.controller = new Controller(this.model, this.view);
+        }
+
+        return new App(Model, View, Controller);
+      })(window);
+    </script>
+  </body>
+</html>