fsf14.html 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  7. <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
  8. <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
  9. <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.full.js"></script>
  10. <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet" />
  11. <link href="/2u/style" rel="stylesheet"/>
  12. <title>Module 14 MVC Challenge Rubric</title>
  13. <style>
  14. *,
  15. *::before,
  16. *::after {
  17. box-sizing: border-box;
  18. }
  19. html,
  20. body {
  21. margin: 0;
  22. padding: 0;
  23. line-height: 1.15;
  24. font-size: 100%;
  25. color: #fefefe;
  26. transition: all 0.4s;
  27. }
  28. body {
  29. background-color: #fbfcfd;
  30. font-family: "Inter", sans-serif;
  31. color: #4a4a4a;
  32. }
  33. #app.dark {
  34. background-color: #121212;
  35. color: #e2e2e2;
  36. }
  37. h1 {
  38. font-size: 1.9rem;
  39. }
  40. h2 {
  41. font-size: 1.6rem;
  42. }
  43. h3 {
  44. font-size: 1.2rem;
  45. }
  46. h4 {
  47. font-size: 0.9rem;
  48. }
  49. p,
  50. legend {
  51. font-size: 1.2rem;
  52. }
  53. label {
  54. font-size: 0.9rem;
  55. line-height: 2;
  56. }
  57. fieldset {
  58. border: none;
  59. padding: 0;
  60. margin-top: 0.9rem;
  61. }
  62. input[type="radio"],
  63. input[type="checkbox"] {
  64. position: absolute !important;
  65. height: 1px;
  66. width: 1px;
  67. overflow: hidden;
  68. clip: rect(1px 1px 1px 1px);
  69. clip: rect(1px, 1px, 1px, 1px);
  70. white-space: nowrap;
  71. }
  72. .select-lg,
  73. .select-lg option {
  74. font-family: "Inter", sans-serif;
  75. padding: 10px;
  76. }
  77. .narrow {
  78. width: 800px;
  79. }
  80. #app {
  81. padding: 3%;
  82. transition: all 0.4s;
  83. }
  84. #app h1 {
  85. font-size: 1.9rem;
  86. font-weight: 700;
  87. line-height: 1.3;
  88. }
  89. header {
  90. display: flex;
  91. justify-content: space-between;
  92. align-items: center;
  93. }
  94. .switch-group {
  95. display: flex;
  96. align-items: center;
  97. height: 2.25rem;
  98. width: 4.5rem;
  99. }
  100. input[type="checkbox"]+.toggle {
  101. height: 100%;
  102. width: 100%;
  103. background-color: #77a1db;
  104. border-radius: 1.9rem;
  105. cursor: pointer;
  106. display: flex;
  107. align-items: center;
  108. padding: 0.35rem;
  109. cursor: pointer;
  110. overflow: hidden;
  111. background-position: 25% 125%;
  112. transition: all 0.5s;
  113. }
  114. input[type="checkbox"]:focus+.toggle {
  115. outline: rgb(59, 153, 252) auto 5px;
  116. }
  117. input[type="checkbox"]:checked+.toggle {
  118. background-color: rgb(85, 89, 204);
  119. background-size: 1rem;
  120. background-repeat: no-repeat;
  121. background-position: 25%;
  122. background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMSIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjgiIGN5PSI3IiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSI1IiBjeT0iMTMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjUiIGN5PSIyIiByPSIyIiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMyIgY3k9IjMiIHI9IjEiIGZpbGw9IndoaXRlIi8+CjxjaXJjbGUgY3g9IjEyLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==");
  123. transition: all 0.5s;
  124. }
  125. input[type="checkbox"]+.toggle::after {
  126. content: "";
  127. display: block;
  128. height: 1.7rem;
  129. width: 1.7rem;
  130. border-radius: 50%;
  131. cursor: pointer;
  132. border: 4px solid #e1be64;
  133. background-color: #fddf74;
  134. transform: translateX(0rem);
  135. transition: all 0.5s;
  136. background-position: center bottom;
  137. }
  138. input[type="checkbox"]:hover+.toggle::after {
  139. transform: translateX(0rem) scale(1.1);
  140. }
  141. input[type="checkbox"]:checked+.toggle::after {
  142. border: 4px solid #ffffff;
  143. background-color: rgba(255, 255, 255, 0.4);
  144. transform: translateX(2.1rem);
  145. transition: all 0.5s;
  146. background-position: center;
  147. background-size: 60%;
  148. background-repeat: no-repeat;
  149. background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxOSAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4LjUgMTAuMjlDMTguMzQyNyAxMS45OTIyIDE3LjcwMzkgMTMuNjE0NCAxNi42NTgyIDE0Ljk2NjhDMTUuNjEyNiAxNi4zMTkyIDE0LjIwMzUgMTcuMzQ1OCAxMi41OTU3IDE3LjkyNjVDMTAuOTg3OSAxOC41MDczIDkuMjQ3OTggMTguNjE4MSA3LjU3OTUgMTguMjQ2MUM1LjkxMTAyIDE3Ljg3NDEgNC4zODI5OSAxNy4wMzQ1IDMuMTc0MjMgMTUuODI1OEMxLjk2NTQ2IDE0LjYxNyAxLjEyNTk0IDEzLjA4OSAwLjc1MzkwNyAxMS40MjA1QzAuMzgxODc1IDkuNzUyMDIgMC40OTI3MTYgOC4wMTIwOCAxLjA3MzQ2IDYuNDA0M0MxLjY1NDIgNC43OTY1MSAyLjY4MDgzIDMuMzg3MzcgNC4wMzMyMSAyLjM0MTc1QzUuMzg1NTkgMS4yOTYxNCA3LjAwNzggMC42NTczMDUgOC43MSAwLjVDNy43MTM0MSAxLjg0ODI3IDcuMjMzODUgMy41MDk0NSA3LjM1ODUzIDUuMTgxNDFDNy40ODMyMSA2Ljg1MzM4IDguMjAzODYgOC40MjUwNiA5LjM4OTQgOS42MTA2QzEwLjU3NDkgMTAuNzk2MSAxMi4xNDY2IDExLjUxNjggMTMuODE4NiAxMS42NDE1QzE1LjQ5MDYgMTEuNzY2MiAxNy4xNTE3IDExLjI4NjYgMTguNSAxMC4yOVYxMC4yOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=");
  150. }
  151. input[type="checkbox"]:checked:hover+.toggle::after {
  152. transform: translateX(2.1rem) scale(1.075);
  153. }
  154. .card {
  155. background-color: #fff;
  156. border-radius: 6px;
  157. border-width: 0;
  158. box-shadow: rgba(155, 155, 155, 0.45) 0 0 30px -5px;
  159. font-size: 1.1rem;
  160. height: 100%;
  161. line-height: 1.8rem;
  162. padding: 1.5rem 2rem;
  163. width: 100%;
  164. margin-bottom: 1.9rem;
  165. }
  166. .dark .card {
  167. background-color: #333333;
  168. box-shadow: none;
  169. }
  170. .card h1,
  171. .card h2,
  172. .card h3,
  173. .card h4,
  174. .card h5,
  175. .card h6 {
  176. color: #4a4a4a;
  177. }
  178. .dark .card h1,
  179. .dark .card h2,
  180. .dark .card h3,
  181. .dark .card h4,
  182. .dark .card h5,
  183. .dark .card h6 {
  184. color: #e2e2e2;
  185. }
  186. .card-header h2 {
  187. display: inline-block;
  188. }
  189. .card .cat-score {
  190. margin-left: 20px;
  191. font-size: smaller;
  192. }
  193. .card h3 {
  194. font-size: 1.2rem;
  195. font-weight: 700;
  196. line-height: 1.8em;
  197. margin: 0;
  198. padding: 0;
  199. }
  200. .card p,
  201. .card label,
  202. .card legend {
  203. color: #7a869c;
  204. border-width: 0;
  205. display: block;
  206. font-size: 1.1rem;
  207. font-weight: 300;
  208. line-height: 1.8em;
  209. margin: 0;
  210. padding: 0;
  211. }
  212. .card legend {
  213. display: flex;
  214. justify-content: flex-start;
  215. align-items: center;
  216. }
  217. .card legend .score {
  218. font-size: smaller;
  219. margin-left: 20px;
  220. font-weight: 100;
  221. min-width: 5%;
  222. }
  223. .dark .card p,
  224. .dark .card label,
  225. .dark .card legend {
  226. color: #e2e2e2;
  227. }
  228. .card label {
  229. display: inline-block;
  230. }
  231. .card input[type="radio"]+label::before {
  232. content: "";
  233. height: 0.9rem;
  234. width: 0.9rem;
  235. margin-right: 0.7rem;
  236. border-radius: 50%;
  237. border: 1px solid rgb(74, 74, 74);
  238. background-color: #ffffff;
  239. display: inline-block;
  240. cursor: pointer;
  241. position: relative;
  242. top: 0.09rem;
  243. }
  244. .dark .card input[type="radio"]+label::before {
  245. border: 1px solid #e2e2e2;
  246. background-color: inherit;
  247. }
  248. .card input[type="radio"]:checked+label::before {
  249. background-color: rgb(47, 106, 189);
  250. border-color: rgb(47, 106, 189);
  251. }
  252. .dark .card input[type="radio"]:checked+label::before {
  253. background-color: #d9d9d9;
  254. border-color: #d9d9d9;
  255. }
  256. .card input[type="radio"]:checked+label {
  257. font-weight: bold;
  258. color: #4a4a4a;
  259. }
  260. .dark input[type="radio"]:checked+label {
  261. color: #e2e2e2;
  262. }
  263. .card input[type="radio"]:focus+label::before {
  264. outline: rgb(59, 153, 252) auto 5px;
  265. }
  266. .card legend {
  267. margin-bottom: 0.4rem;
  268. }
  269. .totals {
  270. font-size: 1.6rem;
  271. }
  272. .totals+h2 {
  273. font-size: 1.9rem;
  274. margin-top: 0;
  275. }
  276. .grade {
  277. font-size: 1.9rem;
  278. }
  279. button {
  280. background-color: #fff;
  281. border: 1px solid #4a4a4a;
  282. border-radius: 6px;
  283. box-shadow: rgba(155, 155, 155, 0.5) 1px 2px 3px 0;
  284. color: #4a4a4a;
  285. cursor: pointer;
  286. display: inline-block;
  287. font-size: 1rem;
  288. font-weight: 500;
  289. letter-spacing: 0.04em;
  290. line-height: 1.8em;
  291. margin: 0.75rem 1.1rem 1.1rem 0;
  292. outline: none;
  293. padding: 0.5rem 1.4rem;
  294. text-transform: uppercase;
  295. transition-delay: 0s;
  296. transition-duration: 0.25s;
  297. transition-property: all;
  298. transition-timing-function: ease-out;
  299. width: fit-content;
  300. }
  301. .dark button {
  302. background: #d9d9d9;
  303. color: #121212;
  304. box-shadow: none;
  305. }
  306. button:hover {
  307. border-color: #dbdbdb;
  308. color: #dbdbdb;
  309. }
  310. .dark button:hover {
  311. color: #121212;
  312. }
  313. button.copy {
  314. margin-left: 0.4rem;
  315. font-size: .8rem;
  316. }
  317. button:focus {
  318. border-color: rgb(56, 56, 56);
  319. box-shadow: rgb(219, 219, 219) 0px 0px 0px 3px;
  320. color: rgb(56, 56, 56);
  321. }
  322. div[role="alert"] {
  323. color: #4a4a4a;
  324. font-size: 1.2rem;
  325. margin-bottom: 1.2rem;
  326. }
  327. .dark div[role="alert"] {
  328. color: #e2e2e2;
  329. }
  330. .summary {
  331. font-size: 1.2rem;
  332. border: none;
  333. font-family: monospace;
  334. background-color: #d5d9de;
  335. padding: 0.9rem;
  336. border-radius: 6px;
  337. line-height: 1.5;
  338. width: 100%;
  339. }
  340. .summary h2,
  341. .dark .summary h2 {
  342. font-size: 1.2rem;
  343. font-family: "Inter", sans-serif;
  344. margin-top: 35px;
  345. margin-bottom: 5px;
  346. }
  347. .summary h2:first-of-type,
  348. .dark .summary h2:first-of-type {
  349. margin-top: 15px;
  350. }
  351. .dark .summary {
  352. background: #6b6a6a;
  353. color: #e2e2e2;
  354. }
  355. .summary pre,
  356. .dark .summary pre {
  357. white-space: pre-wrap;
  358. border-radius: 5px;
  359. padding: 15px;
  360. border: solid 2px rgba(0, 0, 0, .4);
  361. }
  362. .summary pre:empty {
  363. display: none;
  364. }
  365. .summary pre:empty+button {
  366. display: none;
  367. }
  368. .d-none {
  369. display: none;
  370. }
  371. @media (max-width: 450px) {
  372. .btn-container {
  373. text-align: center;
  374. }
  375. button {
  376. width: 100%;
  377. margin: 0.5rem;
  378. }
  379. input[type="checkbox"]:checked+.toggle::after {
  380. transform: translateX(1.3rem);
  381. background-size: 50%;
  382. }
  383. input[type="checkbox"]:checked:hover+.toggle::after {
  384. transform: translateX(1.3rem) scale(1.1);
  385. background-size: 50%;
  386. }
  387. }
  388. @media (max-width: 515px) {
  389. input[type="checkbox"]:checked+.toggle::after {
  390. transform: translateX(1.7rem);
  391. }
  392. input[type="checkbox"]:checked:hover+.toggle::after {
  393. transform: translateX(1.7rem) scale(1.1);
  394. }
  395. }
  396. @media (max-width: 460px) {
  397. input[type="checkbox"]:checked+.toggle::after {
  398. transform: translateX(1.4rem);
  399. }
  400. input[type="checkbox"]:checked:hover+.toggle::after {
  401. transform: translateX(1.4rem) scale(1.1);
  402. }
  403. }
  404. @media (max-width: 400px) {
  405. #app h1 {
  406. font-size: 1.6rem;
  407. }
  408. input[type="checkbox"]:checked+.toggle::after {
  409. transform: translateX(1.3rem);
  410. }
  411. input[type="checkbox"]:checked:hover+.toggle::after {
  412. transform: translateX(1.3rem) scale(1.1);
  413. }
  414. }
  415. @media (max-width: 340px) {
  416. input[type="checkbox"]:checked+.toggle::after {
  417. transform: translateX(0.9rem);
  418. }
  419. input[type="checkbox"]:checked:hover+.toggle::after {
  420. transform: translateX(0.9rem) scale(1.075);
  421. }
  422. }
  423. </style>
  424. <script>
  425. $(function () {
  426. let $select2 = $('.select2').select2({
  427. containerCssClass: "wrap"
  428. })
  429. function triggerEvent(name, val) {
  430. let selected = $("select[name='" + name + "']")
  431. let change = new Event('change', { bubbles: true })
  432. selected[0].dispatchEvent(change)
  433. }
  434. $('.select2').on('select2:select', function (e) {
  435. triggerEvent(e.target.name, e.target.value)
  436. });
  437. })
  438. </script>
  439. </head>
  440. <body>
  441. <div id="app"></div>
  442. <script>
  443. (function (window) {
  444. "use strict";
  445. function Model() {
  446. this.title = `Module 14 MVC Challenge Rubric (Coding)`;
  447. this.categories = [
  448. {
  449. title: "Deployment",
  450. subtitle: null,
  451. score: 0,
  452. questions: [
  453. {
  454. title: "Student submitted deployed live application URL",
  455. score: 8,
  456. type: "true false",
  457. message: "Application isn't deployed",
  458. status: false
  459. },
  460. {
  461. title:
  462. "Application loads at deployed URL (no 404, or Heroku/GH Pages error)",
  463. score: 8,
  464. type: "true false",
  465. message: "Application isn't deployed",
  466. status: false
  467. },
  468. {
  469. title: "Student submitted application GitHub URL",
  470. score: 8,
  471. type: "true false",
  472. message: "Missing GitHub submission URL",
  473. status: false
  474. },
  475. {
  476. title: "GitHub Repo contains application code",
  477. score: 8,
  478. type: "true false",
  479. message: "GitHub repo is empty",
  480. status: false
  481. }
  482. ]
  483. },
  484. {
  485. title: "Technical Acceptance Criteria",
  486. subtitle:
  487. "Keep your browser console open and watch out for errors from this point forward.",
  488. score: 0,
  489. questions: [
  490. {
  491. title:
  492. "Application allows users to sign up and log in",
  493. score: 5,
  494. type: "true false",
  495. message:
  496. "Application doesn't allow users to sign up and log in",
  497. status: false
  498. },
  499. {
  500. 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",
  501. score: 5,
  502. type: "likert",
  503. options: [
  504. {
  505. 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",
  506. score: 0
  507. },
  508. {
  509. 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",
  510. score: 2
  511. },
  512. {
  513. 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",
  514. score: 5
  515. }
  516. ],
  517. message: "",
  518. status: false
  519. },
  520. {
  521. 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",
  522. score: 5,
  523. type: "likert",
  524. options: [
  525. {
  526. 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",
  527. score: 0
  528. },
  529. {
  530. 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",
  531. score: 2
  532. },
  533. {
  534. 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",
  535. score: 5
  536. }
  537. ],
  538. message: "",
  539. status: false
  540. },
  541. {
  542. 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",
  543. score: 5,
  544. type: "likert",
  545. options: [
  546. {
  547. message: "Application dashboard doesn't include the option to create new blog posts",
  548. score: 0
  549. },
  550. {
  551. message: "Application dashboard includes the option to create new blog posts, but is missing either the option to include a title or the content",
  552. score: 1
  553. },
  554. {
  555. 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",
  556. score: 2
  557. },
  558. {
  559. 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",
  560. score: 3
  561. },
  562. {
  563. 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",
  564. score: 5
  565. }
  566. ],
  567. message: "",
  568. status: false
  569. },
  570. {
  571. title: "Application dashboard allows user to click into their blog posts and either update or delete them",
  572. score: 5,
  573. type: "likert",
  574. options: [
  575. {
  576. message: "Application dashboard doesn't allow user to click into their blog posts and either update or delete them",
  577. score: 0
  578. },
  579. {
  580. message: "Application dashboard allows user to click into their blog posts but is missing either the option to update them or delete them",
  581. score: 2
  582. },
  583. {
  584. message: "Application dashboard allows user to click into their blog posts and either update or delete them",
  585. score: 5
  586. }
  587. ],
  588. message: "",
  589. status: false
  590. },
  591. {
  592. 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)",
  593. score: 5,
  594. type: "likert",
  595. options: [
  596. {
  597. 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)",
  598. score: 0
  599. },
  600. {
  601. 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)",
  602. score: 1
  603. },
  604. {
  605. 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)",
  606. score: 2
  607. },
  608. {
  609. 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)",
  610. score: 3
  611. },
  612. {
  613. 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)",
  614. score: 4
  615. },
  616. {
  617. 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)",
  618. score: 5
  619. }
  620. ],
  621. message: "",
  622. status: false
  623. },
  624. {
  625. title:
  626. "Application allows logged in users to leave a comment on a post that they click on and updates post to include comment",
  627. score: 5,
  628. type: "true false",
  629. message:
  630. "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",
  631. status: false
  632. },
  633. {
  634. 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",
  635. score: 5,
  636. type: "likert",
  637. options: [
  638. {
  639. 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",
  640. score: 0
  641. },
  642. {
  643. 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",
  644. score: 3
  645. },
  646. {
  647. 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",
  648. score: 5
  649. }
  650. ],
  651. message: "",
  652. status: false
  653. }
  654. ]
  655. },
  656. {
  657. title: "Repository Quality",
  658. subtitle: null,
  659. score: 0,
  660. questions: [
  661. {
  662. title: "Repository has a unique name",
  663. score: 2,
  664. type: "true false",
  665. message: "Repo doesn't have a unique name",
  666. status: false
  667. },
  668. {
  669. title:
  670. "Repository follows best practices for file structure and naming conventions",
  671. score: 2,
  672. type: "true false",
  673. message:
  674. "Code doesn't follow recommended file/folder structure",
  675. status: false
  676. },
  677. {
  678. title:
  679. "Repository follows best practices for class/id naming conventions, indentation, quality comments, etc.",
  680. score: 2,
  681. type: "true false",
  682. message:
  683. "Code doesn't follow good practices for class/id names, indentation, lacks quality comments",
  684. status: false
  685. },
  686. {
  687. title:
  688. "Repository contains multiple descriptive commit messages",
  689. score: 2,
  690. type: "true false",
  691. message:
  692. "Repo doesn't contain multiple descriptive commit messages",
  693. status: false
  694. },
  695. {
  696. title: "Repository contains quality README with description, screenshot, link to deployed application",
  697. score: 5,
  698. type: "likert",
  699. options: [
  700. {
  701. message: "Repo doesn't include a high quality README with screenshots, a description, link to deployed application",
  702. score: 0
  703. },
  704. {
  705. message: "Repository README is missing 2 of the following: description, screenshot, or link to deployed application",
  706. score: 2
  707. },
  708. {
  709. message: "Repository README is missing 1 of the following: description, screenshot, or link to deployed application",
  710. score: 4
  711. },
  712. {
  713. message: "Repository contains quality README with description, screenshot, link to deployed application",
  714. score: 5
  715. }
  716. ],
  717. message: "",
  718. status: false
  719. }
  720. ]
  721. },
  722. {
  723. title: "Application Quality",
  724. subtitle: null,
  725. score: 0,
  726. questions: [
  727. {
  728. title: "Application user experience is intuitive and easy to navigate",
  729. score: 5,
  730. type: "likert",
  731. options: [
  732. {
  733. message: "Application user experience is not intuitive and easy to navigate",
  734. score: 0
  735. },
  736. {
  737. message: "Application user experience could be much more intuitive and easy to navigate",
  738. score: 2
  739. },
  740. {
  741. message: "Application user experience could be a lot more intuitive and easy to navigate",
  742. score: 4
  743. },
  744. {
  745. message: "Application user experience is intuitive and easy to navigate",
  746. score: 5
  747. }
  748. ],
  749. message: "",
  750. status: false
  751. },
  752. {
  753. title: "Application user interface style is clean and polished",
  754. score: 5,
  755. type: "likert",
  756. options: [
  757. {
  758. message: "Application user interface is not clean and polished",
  759. score: 0
  760. },
  761. {
  762. message: "Application user interface could be much better",
  763. score: 2
  764. },
  765. {
  766. message: "Application user interface could be a bit better",
  767. score: 4
  768. },
  769. {
  770. message: "Application user interface style is clean and polished",
  771. score: 5
  772. }
  773. ],
  774. message: "",
  775. status: false
  776. },
  777. {
  778. title: "Application resembles the mock-up functionality provided in Challenge instructions",
  779. score: 5,
  780. type: "likert",
  781. options: [
  782. {
  783. message: "Application doesn't resemble the mock-up functionality provided in Challenge instructions",
  784. score: 0
  785. },
  786. {
  787. message: "Application could resemble mock-up functionality provided with Challenge instructions much more closely",
  788. score: 2
  789. },
  790. {
  791. message: "Application could resemble mock-up functionality provided with Challenge instructions a bit more closely",
  792. score: 4
  793. },
  794. {
  795. message: "Application resembles the mock-up functionality provided in Challenge instructions",
  796. score: 5
  797. }
  798. ],
  799. message: "",
  800. status: false
  801. }
  802. ]
  803. },
  804. {
  805. title: "Academic Integrity",
  806. subtitle:
  807. "If any of of the following are true, partner with your instructor to investigate plagiarism. Contact your SSM if you believe plagiarism has occurred.",
  808. score: 0,
  809. questions: [
  810. {
  811. title: "Application contains a low number of commits",
  812. score: 0,
  813. type: "true false",
  814. message: null,
  815. status: false
  816. },
  817. {
  818. title:
  819. "Application codebase appears advanced for the student, at this point in the course, or in general",
  820. score: 0,
  821. type: "true false",
  822. message: null,
  823. status: false
  824. },
  825. {
  826. title:
  827. "Application package.json file references a different author and/or repository",
  828. score: 0,
  829. type: "true false",
  830. message: null,
  831. status: false
  832. },
  833. {
  834. title:
  835. "Application package.json file references much older versions of npm packages",
  836. score: 0,
  837. type: "true false",
  838. message: null,
  839. status: false
  840. }
  841. ]
  842. }
  843. ];
  844. this.initStorage();
  845. }
  846. Model.prototype.initStorage = function () {
  847. this.score = 0;
  848. this.questions = [];
  849. this.messages = new Set();
  850. this.flagged = new Set();
  851. this.isDarkMode =
  852. JSON.parse(localStorage.getItem("isDarkMode")) || false;
  853. this.categories
  854. .forEach(category => {
  855. category.score = 0;
  856. category.questions.forEach(question => {
  857. question.category = category.title;
  858. question.status = false;
  859. question.id = this.questions.length;
  860. question.flagged = category.title === "Academic Integrity";
  861. question.bonus = category.title === "Bonus";
  862. if (question.type === "likert") {
  863. question.message = question.options[0].message.split(" - ").pop();
  864. };
  865. let messageObj = {
  866. message: question.message,
  867. category: question.category
  868. };
  869. question.message && !question.bonus && this.messages.add(messageObj);
  870. this.questions.push(question);
  871. category.score += question.score;
  872. });
  873. });
  874. };
  875. Model.prototype.toggleDarkMode = function () {
  876. this.isDarkMode = !this.isDarkMode;
  877. localStorage.setItem("isDarkMode", this.isDarkMode);
  878. };
  879. Model.prototype.toggleRadio = function (radio) {
  880. const id = Number(radio.getAttribute("name"));
  881. const question = this.questions[id];
  882. const questionScore = document.querySelector("span[data-id='" + id + "']");
  883. const category = question.category;
  884. const categoryScore = document.querySelector("span[data-cat='" + category + "']");
  885. const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
  886. question.status = !question.status;
  887. if (question.status === true) {
  888. if (!question.bonus) {
  889. this.messages.forEach(message => {
  890. if (message.message === question.message) {
  891. this.messages.delete(message);
  892. }
  893. })
  894. } else {
  895. let messageObj = {
  896. message: question.message,
  897. category: category
  898. }
  899. this.messages.add(messageObj);
  900. }
  901. this.score += question.score;
  902. if (question.flagged) {
  903. this.flagged.add(question.id);
  904. }
  905. } else {
  906. if (question.flagged) {
  907. this.flagged.delete(question.id);
  908. }
  909. if (!question.bonus) {
  910. if (question.message) {
  911. let messageObj = {
  912. message: question.message,
  913. category: category
  914. }
  915. this.messages.add(messageObj);
  916. }
  917. } else {
  918. this.messages.forEach(message => {
  919. if (message.message === question.message) {
  920. this.messages.delete(message);
  921. }
  922. })
  923. }
  924. this.score -= question.score;
  925. }
  926. if (category !== "Academic Integrity") {
  927. if (question.status) {
  928. questionScore.textContent = question.score;
  929. } else {
  930. questionScore.textContent = 0;
  931. }
  932. const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
  933. let categoryTotal = 0;
  934. catQuestions.forEach(question => {
  935. if (question.type === "true false") {
  936. if (question.status) {
  937. categoryTotal += question.score;
  938. }
  939. } else {
  940. let score = document.querySelector("select[name='" + question.id + "']").value;
  941. categoryTotal += parseInt(score);
  942. }
  943. })
  944. categoryScore.textContent = categoryTotal;
  945. resultsCategoryScore.textContent = categoryTotal;
  946. }
  947. };
  948. Model.prototype.handleSelect = function (select) {
  949. const id = Number(select.getAttribute("name"));
  950. const question = this.questions[id];
  951. const questionScore = document.querySelector("span[data-id='" + id + "']");
  952. const oldScore = questionScore.textContent;
  953. const category = question.category;
  954. const categoryScore = document.querySelector("span[data-cat='" + category + "']");
  955. const resultsCategoryScore = document.querySelector("span[data-res-cat='" + category + "']");
  956. let newMessage = select.selectedOptions[0].text.split(" - ").pop();
  957. if (newMessage === "Open this menu") {
  958. newMessage = select[1].text.split(" - ").pop();
  959. };
  960. if (question.type === "likert") {
  961. this.messages.forEach(item => {
  962. if (item.message === question.message) {
  963. this.messages.delete(item)
  964. }
  965. })
  966. question.message = newMessage;
  967. let messageObj = {
  968. message: newMessage,
  969. category: category
  970. }
  971. this.messages.add(messageObj);
  972. };
  973. this.score -= parseInt(oldScore);
  974. this.score += parseInt(select.value);
  975. questionScore.textContent = select.value;
  976. const catQuestions = this.categories.filter(cat => cat.title === category)[0].questions;
  977. let categoryTotal = 0;
  978. catQuestions.forEach(question => {
  979. if (question.type === "true false") {
  980. if (question.status) {
  981. categoryTotal += question.score;
  982. }
  983. } else {
  984. let score = document.querySelector("select[name='" + question.id + "']").value;
  985. categoryTotal += parseInt(score);
  986. }
  987. })
  988. categoryScore.textContent = categoryTotal;
  989. resultsCategoryScore.textContent = categoryTotal;
  990. };
  991. Model.prototype.calcGrade = function () {
  992. const roundedScore = Math.min(100, Math.ceil(this.score));
  993. let letterGrade = "";
  994. if (roundedScore >= 99.5) {
  995. letterGrade = "A+";
  996. } else if (roundedScore >= 94.5 && roundedScore <= 99.4) {
  997. letterGrade = "A";
  998. } else if (roundedScore >= 91.5 && roundedScore <= 94.4) {
  999. letterGrade = "A-";
  1000. } else if (roundedScore >= 87.5 && roundedScore <= 91.4) {
  1001. letterGrade = "B+";
  1002. } else if (roundedScore >= 84.5 && roundedScore <= 87.4) {
  1003. letterGrade = "B";
  1004. } else if (roundedScore >= 81.5 && roundedScore <= 84.4) {
  1005. letterGrade = "B-";
  1006. } else if (roundedScore >= 77.5 && roundedScore <= 81.4) {
  1007. letterGrade = "C+";
  1008. } else if (roundedScore >= 74.5 && roundedScore <= 77.4) {
  1009. letterGrade = "C";
  1010. } else if (roundedScore >= 71.5 && roundedScore <= 74.4) {
  1011. letterGrade = "C-";
  1012. } else if (roundedScore >= 69.5 && roundedScore <= 71.4) {
  1013. letterGrade = "D+";
  1014. } else if (roundedScore >= 64.5 && roundedScore <= 69.4) {
  1015. letterGrade = "D";
  1016. } else if (roundedScore >= 61.5 && roundedScore <= 64.4) {
  1017. letterGrade = "D-";
  1018. } else {
  1019. letterGrade = "F";
  1020. }
  1021. return letterGrade;
  1022. };
  1023. Model.prototype.hasFlags = function () {
  1024. return this.flagged.size > 0;
  1025. };
  1026. function View(model) {
  1027. this.model = model;
  1028. this.generateHTML();
  1029. this.cacheDom();
  1030. }
  1031. View.prototype.cacheDom = function () {
  1032. this.darkModeToggle = document.querySelector("#dark-mode");
  1033. this.radios = Array.from(
  1034. document.querySelectorAll("input[type='radio']")
  1035. );
  1036. this.selects = Array.from(
  1037. document.querySelectorAll("select")
  1038. );
  1039. this.totals = document.querySelector(".totals");
  1040. this.grade = document.querySelector(".grade");
  1041. this.alert = document.querySelector("div[role='alert']");
  1042. this.summaryHeading = document.querySelector(".summary-heading");
  1043. this.summary = document.querySelector(".summary");
  1044. this.model.categories
  1045. .filter(category => category.title !== "Academic Integrity")
  1046. .map(category => {
  1047. let summary = category.title.replace(/\s+/g, '-').toLowerCase();
  1048. let summaryName = summary + "-summary";
  1049. return this[summaryName] = document.querySelector("." + summary);
  1050. })
  1051. this.resetButton = document.querySelector(".reset");
  1052. this.model.categories
  1053. .filter(category => category.title !== "Academic Integrity")
  1054. .map(category => {
  1055. let button = category.title.replace(/\s+/g, '-').toLowerCase();
  1056. let buttonName = "copyButton-" + button;
  1057. return this[buttonName] = document.querySelector(".copy-" + button);
  1058. })
  1059. };
  1060. View.prototype.createFieldset = function ({ title, id, score, status, category, type, options }) {
  1061. return `<fieldset class="input-group">
  1062. <legend>
  1063. <span class="title">${title}</span> ${category === "Academic Integrity" ? "" : "<span class='score'><span data-id='" + id + "' class='current-score'>0</span> / " + score + "</span>"}
  1064. </legend>
  1065. ${type === "true false" ?
  1066. `<div class="radio-group">
  1067. <input type="radio" name="${id}" id="${id}-true" value="true">
  1068. <label for="${id}-true">True</label>
  1069. </div>
  1070. <div class="radio-group">
  1071. <input type="radio" name="${id}" id="${id}-false" value="false" checked>
  1072. <label for="${id}-false">False</label>
  1073. </div>` :
  1074. `<div class="select-group">
  1075. <select name="${id}" class="select-lg select2 narrow wrap">
  1076. <option value="0" selected>Open this menu</option>
  1077. ${options.map(option => `<option value="${option.score}">${option.score} - ${option.message}</option>`)}
  1078. </select>
  1079. </div>`
  1080. }
  1081. </fieldset>`;
  1082. };
  1083. View.prototype.createCard = function ({ title, subtitle, questions, score }) {
  1084. return `<div class="card">
  1085. <div class="card-header"><h2>${title}</h2> ${title !== "Academic Integrity" ? "<span class='cat-score'><span data-cat='" + title + "'>0</span> / " + score + "</span>" : ""}</div>
  1086. ${subtitle ? `<h3>${subtitle}</h3>` : ""}
  1087. ${questions.map(child => this.createFieldset(child)).join("")}
  1088. </div>`;
  1089. };
  1090. View.prototype.createResults = function () {
  1091. return `<div class="card">
  1092. <h2>Recommended Grade: <span class="grade">F</span></h2>
  1093. <h2 class="totals">Total: (0/100)</h2>
  1094. <div class="d-none" role="alert">
  1095. You've selected <strong>True</strong> for one of more Academic
  1096. Integrity prompts and are advised to investigate plagiarism before
  1097. issuing a grade.
  1098. </div>
  1099. <div class="summary" aria-label="summary">
  1100. ${this.model.categories
  1101. .filter(category => category.title !== "Academic Integrity")
  1102. .map(category => {
  1103. let score = category.score;
  1104. let title = category.title;
  1105. let catClass = title.replace(/\s+/g, '-').toLowerCase();
  1106. return "<h2>" + title + "<span class='cat-score'><span data-res-cat='" + title + "'>0</span> / " + score + "</span></h2><pre class='" + catClass + "'>" + [...this.model.messages]
  1107. .filter(messageObj => messageObj.category === category.title)
  1108. .map(messageObj => `* ${messageObj.message}\n`)
  1109. .join("") + "</pre><button class='copy copy-" + catClass + "'>Copy To Clipboard</button>"
  1110. })
  1111. .join("")}
  1112. </div>
  1113. <div class="btn-container">
  1114. <button class="reset">Reset Form</button>
  1115. </div>
  1116. </div>`;
  1117. };
  1118. View.prototype.createHeader = function (title, isDarkMode = false) {
  1119. return `<header>
  1120. <h1>${title}</h1>
  1121. <div class="switch-group">
  1122. <input type="checkbox" name="dark-mode" id="dark-mode" ${isDarkMode ? "checked" : ""
  1123. }>
  1124. <label aria-label="dark mode" for="dark-mode" class="toggle">
  1125. </label>
  1126. </div>
  1127. </header>`;
  1128. };
  1129. View.prototype.generateHTML = function () {
  1130. this.wrapper = document.querySelector("#app");
  1131. if (this.model.isDarkMode) {
  1132. this.wrapper.classList.add("dark");
  1133. }
  1134. const survey = this.model.categories.reduce((final, current) => {
  1135. final += this.createCard(current);
  1136. return final;
  1137. }, "");
  1138. const results = this.createResults();
  1139. const header = this.createHeader(
  1140. this.model.title,
  1141. this.model.isDarkMode
  1142. );
  1143. this.wrapper.innerHTML = header + survey + results;
  1144. };
  1145. View.prototype.update = function () {
  1146. const roundedScore = Math.min(100, Math.ceil(this.model.score));
  1147. this.totals.textContent = `Total: (${roundedScore}/ 100)`;
  1148. const letterGrade = this.model.calcGrade();
  1149. this.grade.textContent = letterGrade;
  1150. if (this.model.hasFlags()) {
  1151. this.alert.classList.remove("d-none");
  1152. } else {
  1153. this.alert.classList.add("d-none");
  1154. }
  1155. if (this.model.isDarkMode) {
  1156. this.wrapper.classList.add("dark");
  1157. } else {
  1158. this.wrapper.classList.remove("dark");
  1159. }
  1160. this.model.categories
  1161. .filter(category => category.title !== "Academic Integrity")
  1162. .forEach(category => {
  1163. let catClass = category.title.replace(/\s+/g, '-').toLowerCase();
  1164. this[catClass + "-summary"].innerHTML = [...this.model.messages]
  1165. .filter(messageObj => messageObj.category === category.title)
  1166. .map(messageObj => `* ${messageObj.message}\n`)
  1167. .join("")
  1168. })
  1169. };
  1170. function Controller(model, view) {
  1171. this.model = model;
  1172. this.view = view;
  1173. this.addEventListeners();
  1174. }
  1175. Controller.prototype.addEventListeners = function () {
  1176. this.view.wrapper.addEventListener("change", event => {
  1177. const { target } = event;
  1178. if (target.name !== "dark-mode") {
  1179. if (!target.matches("input[type='radio']")) {
  1180. this.model.handleSelect(target);
  1181. } else {
  1182. this.model.toggleRadio(target);
  1183. }
  1184. }
  1185. this.view.update();
  1186. });
  1187. this.view.resetButton.addEventListener("click", () => {
  1188. this.model.initStorage();
  1189. this.view.radios.forEach(
  1190. radio => (radio.checked = radio.hasAttribute("checked"))
  1191. );
  1192. this.view.selects.forEach(
  1193. select => select.selectedIndex = null
  1194. )
  1195. this.model.questions
  1196. .filter(question => question.category !== "Academic Integrity")
  1197. .forEach(question => {
  1198. const id = question.id;
  1199. const questionScore = document.querySelector("span[data-id='" + id + "']");
  1200. questionScore.textContent = 0;
  1201. });
  1202. this.model.categories
  1203. .filter(category => category.title !== "Academic Integrity")
  1204. .forEach(category => {
  1205. const title = category.title
  1206. const categoryScore = document.querySelector("span[data-cat='" + title + "']");
  1207. const resultsCategoryScore = document.querySelector("span[data-res-cat='" + title + "']");
  1208. categoryScore.textContent = 0;
  1209. resultsCategoryScore.textContent = 0;
  1210. });
  1211. this.view.update();
  1212. window.scrollTo(0, 0);
  1213. });
  1214. this.model.categories
  1215. .filter(category => category.title !== "Academic Integrity")
  1216. .forEach(category => {
  1217. let title = category.title.replace(/\s+/g, '-').toLowerCase();
  1218. let buttonName = "copyButton-" + title;
  1219. let summaryName = title + "-summary";
  1220. this.view[buttonName].addEventListener("click", () => {
  1221. const selection = window.getSelection();
  1222. const range = document.createRange();
  1223. range.selectNodeContents(this.view[summaryName]);
  1224. selection.removeAllRanges();
  1225. selection.addRange(range);
  1226. document.execCommand("copy");
  1227. });
  1228. });
  1229. this.view.darkModeToggle.addEventListener("change", () => {
  1230. this.model.toggleDarkMode();
  1231. this.view.update();
  1232. });
  1233. };
  1234. function App(Model, View, Controller) {
  1235. this.model = new Model();
  1236. this.view = new View(this.model);
  1237. this.controller = new Controller(this.model, this.view);
  1238. }
  1239. return new App(Model, View, Controller);
  1240. })(window);
  1241. </script>
  1242. </body>
  1243. </html>