Last active
November 16, 2017 21:13
-
-
Save Meshiest/820dfe96d1abe9458ff58db76c05876f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>HW2 Grader</title> | |
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script> | |
<script src="https://cwdoh.com/cssparser.js/demo/cssparser.min.js?cachebust=20170317"></script> | |
<style type="text/css"> | |
* { | |
margin: 0; | |
padding: 0; | |
} | |
.student-name { | |
background-image: linear-gradient(#f5f5f5 70%, transparent); | |
display: block; | |
padding: 8px; | |
position: sticky; | |
top: 0; | |
} | |
.criteria-header .student-name { | |
background: none; | |
color: #999; | |
display: inline; | |
font-size: 12px; | |
margin-left: 8px; | |
} | |
body { | |
background-color: #f5f5f5; | |
font-family: sans-serif; | |
} | |
.container { | |
margin: 20px auto; | |
max-width: 800px; | |
} | |
.missing { | |
background-color: #fcc; | |
} | |
.criteria-header { | |
margin: 4px 0; | |
} | |
.iframe-row { | |
display: flex; | |
overflow-x: auto; | |
} | |
.iframe-container { | |
display: flex; | |
flex-direction: column; | |
margin: 8px; | |
} | |
.iframe-container::before { | |
content: attr(title); | |
color: #555; | |
font-size: 20px; | |
} | |
iframe { | |
background-color: #fff; | |
border: none; | |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); | |
height: 300px; | |
} | |
.desktop { | |
width: 802px; | |
} | |
.tablet { | |
width: 768px; | |
} | |
.mobile { | |
width: 399px; | |
} | |
.code-input { | |
border: none; | |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); | |
margin-bottom: 16px; | |
min-height: 150px; | |
padding: 4px; | |
width: auto; | |
} | |
form, .criteria-container { | |
display: flex; | |
flex-direction: column; | |
margin: 8px; | |
width: auto; | |
} | |
.button { | |
background: #1E88E5; | |
border: none; | |
border-radius: 4px; | |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); | |
color: #fff; | |
cursor: pointer; | |
display: inline-block; | |
font-family: 'Roboto Condensed', sans-serif; | |
font-size: 1em; | |
margin: auto; | |
min-width: 100px; | |
padding: 4px; | |
text-align: center; | |
transition: all 0.5s; | |
} | |
.button:hover { | |
background: #2196F3; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); | |
} | |
.button-row { | |
margin: auto; | |
} | |
.upload { | |
cursor: pointer; | |
position: relative; | |
} | |
.upload > input { | |
cursor: pointer; | |
height: 100%; | |
left: 0; | |
opacity: 0; | |
position: absolute; | |
top: 0; | |
width: 100%; | |
} | |
.note { | |
color: #999; | |
} | |
h2.criteria-header { | |
position: sticky; | |
bottom: 0; | |
padding: 8px; | |
background-image: linear-gradient(transparent, #f5f5f5 30%); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Simple HW2 Grader (<a href="https://gist.github.com/Meshiest/f70d58c8d9298eeb3e39d4464f19391a">Rubric</a>)</h1> | |
<i id="header" class="student-name"></i> | |
<form id="form" action="javascript:void(0);"> | |
<textarea placeholder="Assignment CSS" | |
class="code-input" | |
id="css-input" | |
name="css"></textarea> | |
<div class="button-row"> | |
<button class="button" type="submit">Grade</button> | |
<div class="button upload"> | |
Upload | |
<input type="file" id="upload" multiple> | |
</div> | |
</div> | |
</form> | |
<div class="iframe-row"> | |
<div class="iframe-container" title="Desktop View"> | |
<iframe class="desktop"></iframe> | |
</div> | |
<div class="iframe-container" title="Tablet View"> | |
<iframe class="tablet"></iframe> | |
</div> | |
<div class="iframe-container" title="Mobile View"> | |
<iframe class="mobile"></iframe> | |
</div> | |
</div> | |
<div id="criteria" class="criteria-container"></div> | |
<i class="note">Note: this score does not account for penalties</i> | |
</div> | |
<template id="responsive-profile"> | |
<!DOCTYPE html> | |
<html lang="en-US"> | |
<head> | |
<title>Profile Page</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
</head> | |
<style id="style"></style> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
font-family: sans-serif; | |
} | |
code { | |
font-family: monospace; | |
} | |
/* A E S T H E T I C */ | |
.aesthetic { | |
font-family: serif; | |
letter-spacing: 4px; | |
text-transform: uppercase; | |
transition: all 0.5s; | |
} | |
.aesthetic:hover { | |
animation: rainbow 1s infinite; | |
color: #faa; | |
} | |
@keyframes rainbow { | |
from { | |
filter: hue-rotate(0deg); | |
} | |
to { | |
filter: hue-rotate(360deg); | |
} | |
} | |
/* World domination layout and animation */ | |
body { | |
/* | |
The world domination gets so big | |
that it adds a scrollbar to the page... | |
So let's hide that :) | |
*/ | |
overflow-x: hidden; | |
background-color: #f5f5f5; | |
} | |
.world-domination-shake, .world-domination-grow { | |
color: black; | |
position: absolute; /* This is needed to use transform */ | |
white-space: nowrap; /* Don't want the words to split up */ | |
} | |
.world-domination-shake:hover { | |
animation: shake 0.5s infinite; /* Animate the shake foreverrrrr */ | |
} | |
.world-domination-grow { | |
transition: all 0s; /* if we're not hovering, make the text small immediately */ | |
} | |
.world-domination-grow:hover { | |
color: red; | |
text-shadow: 0 0 5px red; | |
transform: scale(10, 10) rotate(30deg); | |
transition: all 100s; /* Animate the all element properties over 100 seconds */ | |
} | |
.invisible { | |
color: transparent; | |
} | |
/* Animation for shaking the world domination */ | |
@keyframes shake { | |
0% { | |
transform: translate(0, 0); | |
} | |
10% { | |
transform: translate(-2px, 2px); | |
} | |
20% { | |
transform: translate(2px, -2px); | |
} | |
30% { | |
transform: translate(-2px, -2px); | |
} | |
40% { | |
transform: translate(2px, 2px); | |
} | |
50% { | |
transform: translate(2px, -2px); | |
} | |
60% { | |
transform: translate(2px, 2px); | |
} | |
70% { | |
transform: translate(-2px, 2px); | |
} | |
80% { | |
transform: translate(-2px, -2px); | |
} | |
90% { | |
transform: translate(2px, 2px); | |
} | |
100% { | |
transform: translate(0, 0); | |
} | |
} | |
</style> | |
<body> | |
<div class="profile"> | |
<img class="profile-picture large" src="https://img0.etsystatic.com/007/0/5491726/il_fullxfull.373486284_7urh.jpg"> | |
<!-- You might need to put max-width: 100% on this for mobile views --> | |
<div class="profile-content"> | |
<header> | |
<h1 class="profile-name">Mr Cat</h1> | |
<h2 class="subheader">Business Cat, Professional Web Developer</h2> | |
<p> | |
Hi, my name is Mr Cat. I secretly plan for <span class="world-domination-container"> | |
<!-- | |
This will be position absolute so it can shake an grow when hovered | |
--> | |
<span class="world-domination-shake"> | |
<span class="world-domination-grow"> | |
w͖̝̭͖͈̦ͧ̓̎o͎͖̜̝͈͈̓ŕ͎̲̬̟͙̠̻ͩ͟ḷ̍ͫͥ͆͝d͜ ̬̤̹̗͉̋̉̔d̛̫̔͐͆̆͋̆ͅǒ̗͇̞̣͙̖̥ͥ͋ͥͭͭ͡m̢̤̣̰̱̠̝͗̌̒́̈́ͪi̤͉n͊̀̎͒̿̈́̚͏͈ả̶̬̺̩̰̘͐̌͆ͯt̨ͫ̈̉̔ͧ̌ͅi̹̺͔͕̣͒͟o̙͔͚̙̳ṉ̭ͮ̾̑̉ | |
</span> | |
</span> | |
<!-- | |
I made this invisible so I can use the width for when the zalgo (above) | |
position: absolute | |
--> | |
<span class="invisible">world domination</span> | |
</span> and free reign for the feline empire! I'll accept all friend requests if your name without honorifics starts on the first half of the alphabet, otherwise, please add me on SnapCat first! | |
</p> | |
</header> | |
<section> | |
<h3>Interests</h3> | |
<p> | |
I like tearing holes in furniture. I develop clean websites with <span class="aesthetic">aesthetic</span> the likes of which has never been seen before in CS 146. I lead sales in multiple departments in middle management. I only eat fish and birds originating from the equator. | |
</p> | |
</section> | |
<section> | |
<h3>Hints</h3> | |
<p> | |
I really like using <code>box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);</code> for shadows! | |
If an image doesn't fit scale well with its size, I typically use <code>object-fit: cover;</code> to make the image cover its element! When text is too cluttered, I simply put <code>line-height: 1.4;</code> for more breathing room! | |
</p> | |
</section> | |
<section> | |
<h3>Friends</h3> | |
<!-- Playing flexboxfroggy.com will make centering and wrapping these a lot easier! --> | |
<div class="friends-list"> | |
<img class="profile-picture small online" src="http://www.petmd.com/sites/all/modules/breedopedia/images/thumbnails/cat/tn-sokoke-forest-cat.jpg"> | |
<img class="profile-picture small online" src="https://yt3.ggpht.com/-Uqwr0hWfOic/AAAAAAAAAAI/AAAAAAAAAAA/Rip1vis4UrM/s100-c-k-no-mo-rj-c0xffffff/photo.jpg"> | |
<img class="profile-picture small offline" src="https://img.thrfun.com/img/083/421/cat_with_fleas_ts1.jpg"> | |
<img class="profile-picture small offline" src="https://yt3.ggpht.com/-eOoGMjwrbH0/AAAAAAAAAAI/AAAAAAAAAAA/OK3BG-h02r8/s100-c-k-no-mo-rj-c0xffffff/photo.jpg"> | |
<img class="profile-picture small offline" src="http://is2.mzstatic.com/image/thumb/Purple118/v4/30/3d/d6/303dd663-6e2c-7a99-e5d2-5ec0f5578997/source/100x100bb.jpg"> | |
<img class="profile-picture small offline" src="https://cdn.skim.gs/image/upload/c_fill,f_auto,fl_lossy,q_auto,h_100,w_100,dpr_1.0/bstgpv87cx0zkqavsq9c"> | |
<img class="profile-picture small offline" src="https://untappedcities-wpengine.netdna-ssl.com/wp-content/uploads/2017/10/Matilda-Cat-Algonquin-Hotel-NYC-Untapped-Cities-100x100.jpg"> | |
<img class="profile-picture small offline" src="https://yt3.ggpht.com/-PFDc7zNOArk/AAAAAAAAAAI/AAAAAAAAAAA/cyOu5r-ugt0/s100-c-k-no-mo-rj-c0xffffff/photo.jpg"> | |
<img class="profile-picture small offline" src="http://www.pethealthnetwork.com/sites/default/files/8-common-myths-about-surgery-and-cats-178110810.jpg"> | |
</div> | |
</section> | |
</div> | |
</div> | |
</body> | |
</html> | |
</template> | |
<script> | |
let total = 0; | |
// Adds a section header to the criteria list | |
function addCriteriaGroup(name) { | |
$('#criteria').append($('<h3 class="criteria-header"/>').text(name)); | |
} | |
// Adds a header to the criteria list | |
function addCriteriaHeader(name) { | |
$('#criteria').append( | |
$('<h2 class="criteria-header"/>') | |
.text(name) | |
.append($('<i id="header" class="student-name"></i>') | |
.text($('#header').text()))); | |
} | |
// Adds a note to the criteria list | |
function addCriteriaNote(name) { | |
$('#criteria').append($('<i/>').text(name)); | |
} | |
// Adds an error to the criteria list | |
function addCriteriaError(error) { | |
$('#criteria').append( | |
$('<pre/>') | |
.text(error) | |
.css('background-color', '#fcc') | |
.css('overflow-x', 'auto') | |
.css('color', '#800')); | |
} | |
// Appends a check/cross followed by point value and the name of the criteria | |
function addCriteria(isDone, name, points, maxPoints, noAdd) { | |
if(!noAdd) | |
total += isDone ? points : 0; | |
$('#criteria').append($('<div class="criteria-entry"/>') | |
.addClass(!isDone ? 'missing' : '') | |
.html((isDone ? '✓ ' : '✗ ') + | |
(isDone ? points + (maxPoints && maxPoints != points ? '/' + maxPoints : '') + 'pt' : '0/' + (points || maxPoints) + 'pt' ) + ' ' + | |
name)); | |
} | |
// Determines if any selected elements in doc with have a computed value on the specified prop | |
function assertStyle(doc, selector, prop, expectedValue) { | |
let hasValue = false; | |
let isRegex = typeof expectedValue.test === "function"; | |
_.each(doc.querySelectorAll(selector), e => { | |
let computedStyle = getComputedStyle(e)[prop]; | |
if(isRegex) { | |
if(expectedValue.test(computedStyle)) | |
hasValue = true | |
} else { | |
if(computedStyle == expectedValue) | |
hasValue = true; | |
} | |
}); | |
return hasValue; | |
} | |
function shouldBeRound(doc, selector) { | |
let hasValue = false; | |
_.each(doc.querySelectorAll(selector), e => { | |
let style = getComputedStyle(e); | |
let radius = style.borderRadius; | |
let width = style.width; | |
if(width == 0) | |
return; | |
if(radius == "50%") | |
hasValue = true; | |
else if(parseInt(radius)/parseInt(width) >= 0.5) | |
hasValue = true; | |
}); | |
return hasValue; | |
} | |
// Called when the upload file button is pressed | |
async function uploadCSS(event) { | |
event.preventDefault(); | |
let files = $('#upload')[0].files; | |
// Open first file locally | |
let file = files[0]; | |
if(file && file.type === 'text/css') | |
setContent(file.name, await readFile(file)); | |
// Open rest of the files in new tabs | |
for(let i = 1; i < files.length; i++) { | |
file = files[i]; | |
if(file && file.type === 'text/css') | |
newTab(i, file, readFile(file)); | |
} | |
} | |
$('#upload').on('change', uploadCSS); | |
// Reads a file, returns a promise | |
function readFile(file) { | |
return new Promise(resolve => { | |
let reader = new FileReader(); | |
reader.onloadend = () => resolve(reader.result); | |
reader.readAsText(file); | |
}) | |
} | |
window.loadedCSS = {}; | |
async function newTab(i, file, css) { | |
loadedCSS[file.name] = await css; | |
window.open(location.href + '#' + file.name, '_blank'); | |
} | |
function setContent(name, css) { | |
console.log('set', name); | |
$('#header').text(name); | |
$('#css-input').val(css); | |
total = 0; | |
$('#criteria').empty(); | |
$('button[type=submit]').click(); | |
} | |
// We have no internet somehow | |
if(typeof jQuery === 'undefined') { | |
document.body.innerHTML = "Missing jQuery. reloading in 5 seconds..."; | |
setTimeout(() => location.reload(), 5000); | |
} | |
// Add our form submit callback | |
$(document).ready(() => { | |
// Setup our form submission | |
$('#form').submit(event => { | |
let form = event.target; | |
event.preventDefault(); | |
let html = $('#responsive-profile').html(); | |
let css = form.css.value; | |
let docs = {}; | |
$('iframe').each((i, e) => { | |
let doc = e.contentWindow.document; | |
docs[e.className] = doc; | |
doc.open(); | |
doc.write(html); | |
doc.close(); | |
doc.getElementById('style').innerHTML = css; | |
}); | |
let rules = docs.desktop.styleSheets[0].rules; | |
// Clear previous test data | |
total = 0; | |
$('#criteria').empty(); | |
addCriteriaGroup('Base'); | |
// Base 5 points | |
addCriteria(css.length, 'Submitting Something', 5); | |
// Check if CSS is valid | |
try { | |
cssparser.parse(css); | |
addCriteria(css.length, 'Valid CSS (-5 if original CSS was invalid)', 5); | |
} catch (e) { | |
addCriteria(false, 'Invalid CSS', 5); | |
addCriteriaError(e); | |
} | |
// Rubric assertions and scoring | |
addCriteriaGroup('Mandatory Requirements'); | |
// Fetch media queries | |
let mediaQueries = _.filter(rules, {type: 4}); | |
// Determine if max-width/min-width is being used | |
let usingWidth = false; | |
_.each(mediaQueries, m => { | |
if(m.conditionText.match(/(min|max)-width/)) | |
usingWidth = true; | |
}); | |
addCriteria(usingWidth, 'Media Queries', Math.floor(Math.min(mediaQueries.length, 2)) * 10, 20); | |
// Determine if a background is set | |
addCriteria(!assertStyle(docs.desktop, '.profile', 'background-color', 'rgba(0, 0, 0, 0)'), 'Background Color', 6); | |
// All headers are not bold | |
addCriteria( | |
!assertStyle(docs.desktop, 'h1', 'font-weight', 'bold') && | |
!assertStyle(docs.desktop, 'h2', 'font-weight', 'bold') && | |
!assertStyle(docs.desktop, 'h3', 'font-weight', 'bold'), | |
'Non-bold Headers', | |
6); | |
addCriteria( | |
!assertStyle(docs.desktop, 'h3', 'border-bottom-style', 'none') || | |
assertStyle(docs.desktop, 'h3', 'text-decoration', /underline/), | |
'Underlined/Border Bottom Header', | |
8); | |
let largeDesktop = assertStyle(docs.desktop, 'img.large', 'height', '300px'); | |
let largeTablet = assertStyle(docs.tablet, 'img.large', 'height', '150px'); | |
addCriteria( | |
largeDesktop || largeTablet, | |
'Profile Image Size on Desktop and Tablet', | |
(largeDesktop ? 5 : 0) + (largeTablet ? 5 : 0), | |
10); | |
let alignDesktop = assertStyle(docs.desktop, 'h1', 'text-align', 'start') && assertStyle(docs.desktop, 'h1', 'text-align', 'left'); | |
let alignTablet = assertStyle(docs.tablet, 'h1', 'text-align', 'center') || | |
(assertStyle(docs.tablet, 'h1', 'justify-content', 'center') || | |
assertStyle(docs.tablet, 'h1', 'align-items', 'center') | |
) && | |
assertStyle(docs.tablet, 'h1', 'display', 'flex'); | |
addCriteria( | |
alignDesktop || alignTablet, | |
'Proper Header Align on Desktop and Tablet', 10); | |
addCriteriaGroup('Loose Requirements (>= 3 complete)'); | |
let looseTotal = 0; | |
let lineHeight = !assertStyle(docs.mobile, 'p', 'line-height', 'normal'); | |
if(lineHeight) | |
looseTotal += 10; | |
addCriteria(lineHeight, 'Line Height Not Normal on Mobile', 10, 10, true); | |
let imageOpacity = !assertStyle(docs.desktop, 'img.offline', 'opacity', '1'); | |
if(imageOpacity) | |
looseTotal += 10; | |
addCriteria(imageOpacity, 'Image Opacity on Offline Photos', 10, 10, true); | |
let roundMobile = shouldBeRound(docs.mobile, 'img'); | |
let roundTablet = shouldBeRound(docs.tablet, 'img'); | |
if(roundMobile && roundTablet) | |
looseTotal += 10; | |
addCriteria( | |
roundMobile && roundTablet, | |
'Circular Images on Mobile and Tablet', | |
10, 10, true); | |
let useFlexbox = assertStyle(docs.mobile, '*', 'display', 'flex') && ( | |
assertStyle(docs.mobile, '*', 'justify-content', 'center') || | |
assertStyle(docs.mobile, '*', 'align-items', 'center') | |
); | |
if(useFlexbox) | |
looseTotal += 10; | |
addCriteria(useFlexbox, 'Use Flex to Center', 10, 10, true); | |
let useFlexwrap = assertStyle(docs.mobile, '*', 'display', 'flex') && | |
assertStyle(docs.mobile, '*', 'flex-wrap', 'wrap'); | |
if(useFlexwrap) | |
looseTotal += 10; | |
addCriteria(useFlexwrap, 'Use Flex to Wrap', 10, 10, true); | |
let justifyMobile = assertStyle(docs.mobile, 'p', 'text-align', 'justify'); | |
if(justifyMobile) | |
looseTotal += 10; | |
addCriteria(justifyMobile, 'Justified Paragraphs on Mobile', 10, 10, true); | |
total += Math.min(looseTotal, 30); | |
addCriteriaHeader('Total: ' + total + '/100pt'); | |
}); | |
// Load css if we were opened | |
if(window.opener && location.hash && window.opener.loadedCSS) { | |
let name = location.hash.slice(1); | |
let css = window.opener.loadedCSS[decodeURI(name)]; | |
if(name && css) | |
setContent(name, css); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment