hw5: done 1,2,3,4,5,7, but not really tested
This commit is contained in:
parent
f6d54b18fd
commit
688ae08a65
33 changed files with 32444 additions and 0 deletions
1
hw5/Claudio_Maggioni/.gitignore
vendored
Normal file
1
hw5/Claudio_Maggioni/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
db.json
|
59
hw5/Claudio_Maggioni/app.js
Normal file
59
hw5/Claudio_Maggioni/app.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
// vim: set ts=2 sw=2 et tw=80:
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const logger = require('morgan');
|
||||
const bodyParser = require('body-parser');
|
||||
const kleiDust = require('klei-dust');
|
||||
const methodOverride = require('method-override');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
|
||||
//configure app
|
||||
app.use(logger('dev'));
|
||||
app.set('views', __dirname + '/views');
|
||||
app.engine('dust', kleiDust.dust);
|
||||
app.set('view engine', 'dust');
|
||||
app.set('view options', { layout: false });
|
||||
|
||||
app.use(methodOverride('_method'));
|
||||
|
||||
// parse application/x-www-form-urlencoded
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
|
||||
// parse application/json
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use(express.static('public'));
|
||||
|
||||
fs.readFile(__dirname + '/db.json', { encoding: 'utf8' }, (e, data) => {
|
||||
if (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
console.info("No database file. Initializing with empty data");
|
||||
} else console.error(e);
|
||||
app.locals.favourites = [];
|
||||
} else {
|
||||
try {
|
||||
app.locals.favourites = JSON.parse(data);
|
||||
console.log('DB initialized', app.locals.favourites);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
app.locals.favourites = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize routers here
|
||||
const routers = require('./routes/routers');
|
||||
app.use('/', routers.root);
|
||||
app.use('/favourites', routers.favourites);
|
||||
});
|
||||
|
||||
app.locals.writeFavs = () => {
|
||||
fs.writeFile(__dirname + '/db.json', JSON.stringify(app.locals.favourites),
|
||||
{ encoding: 'utf8' }, err => {
|
||||
if (err) console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = app;
|
9
hw5/Claudio_Maggioni/bin/www
Executable file
9
hw5/Claudio_Maggioni/bin/www
Executable file
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env node
|
||||
var debug = require('debug')('canvas-server');
|
||||
var app = require('../app');
|
||||
|
||||
app.set('port', process.env.PORT || 3000);
|
||||
|
||||
var server = app.listen(app.get('port'), function() {
|
||||
debug('Express server listening on port ' + server.address().port);
|
||||
});
|
11
hw5/Claudio_Maggioni/config.js
Normal file
11
hw5/Claudio_Maggioni/config.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
//Server URL
|
||||
url: "http://127.0.0.1:3000",
|
||||
//Form structure
|
||||
form: {
|
||||
_id: "_id",
|
||||
name: "name",
|
||||
dataURL: "dataURL",
|
||||
bookmarked: "bookmarked" // Optional
|
||||
}
|
||||
}
|
2993
hw5/Claudio_Maggioni/package-lock.json
generated
Normal file
2993
hw5/Claudio_Maggioni/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
hw5/Claudio_Maggioni/package.json
Normal file
35
hw5/Claudio_Maggioni/package.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "wa-exercise-7-2017-2018",
|
||||
"version": "0.0.0",
|
||||
"description": "Exercise 7 of Web Atelier. Express.js and Mongoose",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "DEBUG='canvas-server' ./bin/www",
|
||||
"test": "npm run test-mocha",
|
||||
"test-mocha": "./node_modules/mocha/bin/mocha -R spec ./test/routes ./test/hypermedia"
|
||||
},
|
||||
"keywords": [
|
||||
"Node.js",
|
||||
"Express.js",
|
||||
"MongoDB",
|
||||
"Mongoose",
|
||||
"REST"
|
||||
],
|
||||
"author": "Vincenzo Ferme, Andrea Gallidabino, Vasileios Triglianos, Ilya Yanok",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.19.0",
|
||||
"debug": "^3.1.0",
|
||||
"dustjs-linkedin": "^2.7.5",
|
||||
"express": "^4.16.2",
|
||||
"klei-dust": "^1.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"morgan": "^1.9.0",
|
||||
"request": "^2.88.0",
|
||||
"supertest": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^4.0.1",
|
||||
"should": "^13.1.3"
|
||||
}
|
||||
}
|
15
hw5/Claudio_Maggioni/public/main.js
Normal file
15
hw5/Claudio_Maggioni/public/main.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
// vim: set ts=2 sw=2 tw=80 et:
|
||||
// Enter your initialization code here
|
||||
|
||||
function init() {
|
||||
// Create canvas app
|
||||
const app = new App({
|
||||
canvas: 'canvas',
|
||||
buttons: {
|
||||
clear: 'clear-btn',
|
||||
camera: 'camera-btn',
|
||||
undo: 'undo-btn'
|
||||
},
|
||||
brushToolbar: 'brush-toolbar'
|
||||
});
|
||||
}
|
5
hw5/Claudio_Maggioni/public/qunit-compat.js
Normal file
5
hw5/Claudio_Maggioni/public/qunit-compat.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
let test = QUnit.test;
|
||||
let equal = QUnit.assert.equal.bind(QUnit.assert);
|
||||
let notEqual = QUnit.assert.notEqual.bind(QUnit.assert);
|
||||
let deepEqual = QUnit.assert.deepEqual.bind(QUnit.assert);
|
||||
let notDeepEqual = QUnit.assert.notDeepEqual.bind(QUnit.assert);
|
5588
hw5/Claudio_Maggioni/public/resources/jsverify.standalone.js
Normal file
5588
hw5/Claudio_Maggioni/public/resources/jsverify.standalone.js
Normal file
File diff suppressed because it is too large
Load diff
17084
hw5/Claudio_Maggioni/public/resources/lodash.js
Normal file
17084
hw5/Claudio_Maggioni/public/resources/lodash.js
Normal file
File diff suppressed because it is too large
Load diff
5048
hw5/Claudio_Maggioni/public/resources/qunit-2.4.0.js
Normal file
5048
hw5/Claudio_Maggioni/public/resources/qunit-2.4.0.js
Normal file
File diff suppressed because it is too large
Load diff
235
hw5/Claudio_Maggioni/public/resources/qunit.css
Normal file
235
hw5/Claudio_Maggioni/public/resources/qunit.css
Normal file
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* QUnit v1.10.0 - A JavaScript Unit Testing Framework
|
||||
*
|
||||
* http://qunitjs.com
|
||||
*
|
||||
* Copyright 2012 jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*/
|
||||
|
||||
/** Font Family and Sizes */
|
||||
|
||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
|
||||
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
|
||||
#qunit-tests { font-size: smaller; }
|
||||
|
||||
|
||||
/** Resets */
|
||||
|
||||
#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/** Header */
|
||||
|
||||
#qunit-header {
|
||||
padding: 0.5em 0 0.5em 1em;
|
||||
|
||||
color: #8699a4;
|
||||
background-color: #0d3349;
|
||||
|
||||
font-size: 1.5em;
|
||||
line-height: 1em;
|
||||
font-weight: normal;
|
||||
|
||||
border-radius: 5px 5px 0 0;
|
||||
-moz-border-radius: 5px 5px 0 0;
|
||||
-webkit-border-top-right-radius: 5px;
|
||||
-webkit-border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
#qunit-header a {
|
||||
text-decoration: none;
|
||||
color: #c2ccd1;
|
||||
}
|
||||
|
||||
#qunit-header a:hover,
|
||||
#qunit-header a:focus {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar label {
|
||||
display: inline-block;
|
||||
padding: 0 .5em 0 .1em;
|
||||
}
|
||||
|
||||
#qunit-banner {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar {
|
||||
padding: 0.5em 0 0.5em 2em;
|
||||
color: #5E740B;
|
||||
background-color: #eee;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#qunit-userAgent {
|
||||
padding: 0.5em 0 0.5em 2.5em;
|
||||
background-color: #2b81af;
|
||||
color: #fff;
|
||||
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-container {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/** Tests: Pass/Fail */
|
||||
|
||||
#qunit-tests {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#qunit-tests li {
|
||||
padding: 0.4em 0.5em 0.4em 2.5em;
|
||||
border-bottom: 1px solid #fff;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#qunit-tests li strong {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#qunit-tests li a {
|
||||
padding: 0.5em;
|
||||
color: #c2ccd1;
|
||||
text-decoration: none;
|
||||
}
|
||||
#qunit-tests li a:hover,
|
||||
#qunit-tests li a:focus {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#qunit-tests ol {
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
background-color: #fff;
|
||||
|
||||
border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
}
|
||||
|
||||
#qunit-tests table {
|
||||
border-collapse: collapse;
|
||||
margin-top: .2em;
|
||||
}
|
||||
|
||||
#qunit-tests th {
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
padding: 0 .5em 0 0;
|
||||
}
|
||||
|
||||
#qunit-tests td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#qunit-tests pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#qunit-tests del {
|
||||
background-color: #e0f2be;
|
||||
color: #374e0c;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#qunit-tests ins {
|
||||
background-color: #ffcaca;
|
||||
color: #500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*** Test Counts */
|
||||
|
||||
#qunit-tests b.counts { color: black; }
|
||||
#qunit-tests b.passed { color: #5E740B; }
|
||||
#qunit-tests b.failed { color: #710909; }
|
||||
|
||||
#qunit-tests li li {
|
||||
padding: 5px;
|
||||
background-color: #fff;
|
||||
border-bottom: none;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/*** Passing Styles */
|
||||
|
||||
#qunit-tests li li.pass {
|
||||
color: #3c510c;
|
||||
background-color: #fff;
|
||||
border-left: 10px solid #C6E746;
|
||||
}
|
||||
|
||||
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
|
||||
#qunit-tests .pass .test-name { color: #366097; }
|
||||
|
||||
#qunit-tests .pass .test-actual,
|
||||
#qunit-tests .pass .test-expected { color: #999999; }
|
||||
|
||||
#qunit-banner.qunit-pass { background-color: #C6E746; }
|
||||
|
||||
/*** Failing Styles */
|
||||
|
||||
#qunit-tests li li.fail {
|
||||
color: #710909;
|
||||
background-color: #fff;
|
||||
border-left: 10px solid #EE5757;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#qunit-tests > li:last-child {
|
||||
border-radius: 0 0 5px 5px;
|
||||
-moz-border-radius: 0 0 5px 5px;
|
||||
-webkit-border-bottom-right-radius: 5px;
|
||||
-webkit-border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
#qunit-tests .fail { color: #000000; background-color: #EE5757; }
|
||||
#qunit-tests .fail .test-name,
|
||||
#qunit-tests .fail .module-name { color: #000000; }
|
||||
|
||||
#qunit-tests .fail .test-actual { color: #EE5757; }
|
||||
#qunit-tests .fail .test-expected { color: green; }
|
||||
|
||||
#qunit-banner.qunit-fail { background-color: #EE5757; }
|
||||
|
||||
|
||||
/** Result */
|
||||
|
||||
#qunit-testresult {
|
||||
padding: 0.5em 0.5em 0.5em 2.5em;
|
||||
|
||||
color: #2b81af;
|
||||
background-color: #D2E0E6;
|
||||
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
#qunit-testresult .module-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/** Fixture */
|
||||
|
||||
#qunit-fixture {
|
||||
position: absolute;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
width: 1000px;
|
||||
height: 1000px;
|
||||
}
|
259
hw5/Claudio_Maggioni/public/scripts/app.js
Normal file
259
hw5/Claudio_Maggioni/public/scripts/app.js
Normal file
|
@ -0,0 +1,259 @@
|
|||
// vim: set ts=2 sw=2 et tw=80:
|
||||
class App {
|
||||
static get BRUSHES() {
|
||||
return {
|
||||
"PenBrush": new PenBrush(),
|
||||
"DiscBrush": new DiscBrush(),
|
||||
"StarBrush": new StarBrush(),
|
||||
};
|
||||
}
|
||||
|
||||
constructor(conf) {
|
||||
if (!(typeof conf === 'object' && conf)) {
|
||||
throw new Error('Argument conf different from specification');
|
||||
}
|
||||
|
||||
this.canvas = document.getElementById(conf.canvas);
|
||||
if (!this.canvas || this.canvas.tagName !== 'CANVAS') {
|
||||
throw new Error(`canvas is not a canvas`);
|
||||
}
|
||||
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.favourites = document.querySelector('div#favourites');
|
||||
|
||||
if (typeof conf.buttons === 'object' && conf.buttons) {
|
||||
this.buttons = {}
|
||||
for (const b of ['clear', 'undo', 'camera'])
|
||||
this.buttons[b] = document.getElementById(conf.buttons[b]);
|
||||
|
||||
if (this.buttons.clear) {
|
||||
this.buttons.clear.addEventListener('click', () => {
|
||||
this.erase();
|
||||
this.background = null;
|
||||
history.clear();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.buttons.undo) {
|
||||
this.buttons.undo.addEventListener('click', () => {
|
||||
history.pop();
|
||||
this.redrawAll();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.buttons.camera) {
|
||||
this.buttons.camera.addEventListener('click', () => {
|
||||
const base64 = this.canvas.toDataURL();
|
||||
const img = document.createElement('img');
|
||||
img.src = base64;
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/favourites';
|
||||
|
||||
const image = document.createElement('input');
|
||||
image.type = 'hidden';
|
||||
image.name = 'dataURL';
|
||||
image.value = base64;
|
||||
form.appendChild(image);
|
||||
|
||||
const lblName = document.createElement('label');
|
||||
lblName.setAttribute('for', 'name');
|
||||
lblName.innerText = 'Name:';
|
||||
form.appendChild(lblName);
|
||||
|
||||
const name = document.createElement('input');
|
||||
name.type = 'text';
|
||||
name.name = 'name';
|
||||
name.placeholder = 'Name';
|
||||
name.value = 'New Name';
|
||||
form.appendChild(name);
|
||||
|
||||
const submit = document.createElement('button');
|
||||
submit.innerText = 'Save';
|
||||
form.appendChild(submit);
|
||||
|
||||
this.favourites.appendChild(img);
|
||||
this.favourites.appendChild(form);
|
||||
});
|
||||
}
|
||||
|
||||
const player = document.createElement('video');
|
||||
player.addEventListener('loadeddata', () => {
|
||||
player.play();
|
||||
setTimeout(() => {
|
||||
const imgCanvas = document.createElement('canvas');
|
||||
imgCanvas.width = this.canvas.width;
|
||||
imgCanvas.height = this.canvas.height;
|
||||
const imgCtx = imgCanvas.getContext('2d');
|
||||
imgCtx.drawImage(player, 0, 0, canvas.width, canvas.height);
|
||||
const imgData = imgCtx.getImageData(0, 0, imgCanvas.width,
|
||||
imgCanvas.height);
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const pixel = (data[i] + 2 * data[i+1] + data[i+2]) / 4;
|
||||
data[i] = data[i+1] = data[i+2] = pixel;
|
||||
}
|
||||
imgCtx.putImageData(imgData, 0, 0);
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = imgCanvas.toDataURL();
|
||||
img.addEventListener('load', () => {
|
||||
this.background = img;
|
||||
this.redrawAll();
|
||||
});
|
||||
|
||||
player.srcObject.getVideoTracks().forEach(track => track.stop());
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.innerHTML = 'Photo';
|
||||
button.addEventListener('click', () => {
|
||||
navigator.mediaDevices.getUserMedia({ video: true })
|
||||
.then((stream) => {
|
||||
player.srcObject = stream;
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('left-toolbar').appendChild(button);
|
||||
}
|
||||
|
||||
this.ctx.lineWidth = 1;
|
||||
this.strokeStyle = this.constructor.defaultStrokeStyle;
|
||||
this.brush = "PenBrush";
|
||||
|
||||
const brushToolbar = document.querySelector('#brush-toolbar');
|
||||
if (brushToolbar) {
|
||||
for (const name in App.BRUSHES) {
|
||||
const b = document.createElement('button');
|
||||
b.innerText = name;
|
||||
b.addEventListener('click', () => this.brush = name);
|
||||
brushToolbar.appendChild(b);
|
||||
}
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.setAttribute('for', 'color');
|
||||
const color = document.createElement('input');
|
||||
color.type = 'color';
|
||||
color.name = 'color';
|
||||
color.value = this.constructor.defaultStrokeStyle;
|
||||
color.addEventListener('change', () => this.strokeStyle = color.value);
|
||||
brushToolbar.appendChild(label);
|
||||
brushToolbar.appendChild(color);
|
||||
}
|
||||
|
||||
const toMouse = (e, func) => {
|
||||
if (e && e.touches && e.touches[0]) {
|
||||
return func.bind(this)({
|
||||
offsetX: e.touches[0].pageX - this.canvas.offsetLeft,
|
||||
offsetY: e.touches[0].pageY - this.canvas.offsetTop
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.canvas.addEventListener('touchstart', e => toMouse(e, this.startPath));
|
||||
this.canvas.addEventListener('mousedown', this.startPath.bind(this));
|
||||
this.canvas.addEventListener('touchmove', e => toMouse(e, this.draw));
|
||||
this.canvas.addEventListener('mousemove', this.draw.bind(this));
|
||||
this.canvas.addEventListener('touchcancel', e => toMouse(e, this.endPath));
|
||||
this.canvas.addEventListener('mouseup', this.endPath.bind(this));
|
||||
this.canvas.addEventListener('mouseout', this.endPath.bind(this));
|
||||
}
|
||||
|
||||
static get defaultStrokeStyle() {
|
||||
return 'black';
|
||||
}
|
||||
|
||||
get strokeStyle() {
|
||||
if (this.ctx.strokeStyle == '#000000') {
|
||||
return 'black';
|
||||
}
|
||||
return this.ctx.strokeStyle;
|
||||
}
|
||||
|
||||
set strokeStyle(style) {
|
||||
if (typeof style !== 'string') {
|
||||
throw new Error('style is not a string');
|
||||
}
|
||||
this.ctx.strokeStyle = style;
|
||||
}
|
||||
|
||||
get brush() {
|
||||
return this._brush.name;
|
||||
}
|
||||
|
||||
set brush(brushName) {
|
||||
this._brush = App.BRUSHES[brushName];
|
||||
}
|
||||
|
||||
erase() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
startPath(e, record = true) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(e.offsetX, e.offsetY);
|
||||
|
||||
if (record) {
|
||||
history.initializeNewPath();
|
||||
history.push(new Stroke(this.brush, this.strokeStyle,
|
||||
e.offsetX, e.offsetY));
|
||||
}
|
||||
|
||||
if (e instanceof MouseEvent) {
|
||||
this.mousedown = true;
|
||||
}
|
||||
}
|
||||
|
||||
draw(e, beginNew = true, record = true) {
|
||||
if (this.mousedown || !(e instanceof MouseEvent)) {
|
||||
this._brush.draw(this.ctx, this.strokeStyle, e.offsetX, e.offsetY);
|
||||
|
||||
if (record) {
|
||||
history.push(new Stroke(this.brush, this.strokeStyle,
|
||||
e.offsetX, e.offsetY));
|
||||
}
|
||||
|
||||
if (beginNew) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(e.offsetX, e.offsetY);
|
||||
} else if (e instanceof MouseEvent) {
|
||||
this.mousedown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endPath(e, record = true) {
|
||||
this.draw(e, false, record);
|
||||
}
|
||||
|
||||
drawPath(path) {
|
||||
const last = path.length - 1;
|
||||
const lastBrush = this.brush;
|
||||
const lastStyle = this.strokeStyle;
|
||||
for (let i = 0; i <= last; i++) {
|
||||
this.brush = path[i].brushName;
|
||||
this.strokeStyle = path[i].strokeStyle;
|
||||
switch(i) {
|
||||
case 0: this.startPath(path[i], false); break;
|
||||
case last: this.endPath(path[i], false); break;
|
||||
default: this.draw(path[i], true, false);
|
||||
}
|
||||
}
|
||||
this.brush = lastBrush;
|
||||
this.strokeStyle = lastStyle;
|
||||
}
|
||||
|
||||
redrawAll() {
|
||||
this.erase();
|
||||
if (this.background) {
|
||||
this.ctx.drawImage(this.background, 0, 0, this.canvas.width,
|
||||
this.canvas.height);
|
||||
}
|
||||
for (const path of history.paths) {
|
||||
this.drawPath(path);
|
||||
}
|
||||
}
|
||||
}
|
53
hw5/Claudio_Maggioni/public/scripts/brushes.js
Normal file
53
hw5/Claudio_Maggioni/public/scripts/brushes.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
// vim: set ts=2 sw=2 et tw=80:
|
||||
|
||||
class PenBrush {
|
||||
constructor() {
|
||||
this.opacity = 1;
|
||||
this.name = "PenBrush";
|
||||
}
|
||||
|
||||
draw(ctx, strokeStyle, x, y) {
|
||||
ctx.lineJoin = ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
class DiscBrush extends PenBrush {
|
||||
static get RADIUS() { return 10; }
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "DiscBrush";
|
||||
}
|
||||
|
||||
draw(ctx, strokeStyle, x, y) {
|
||||
ctx.beginPath(); // clear previous path starting
|
||||
ctx.ellipse(x, y, 10, 10, 0, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
class StarBrush extends PenBrush {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "StarBrush";
|
||||
}
|
||||
|
||||
draw(ctx, strokeStyle, x, y) {
|
||||
ctx.moveTo(x - 8, y - 3);
|
||||
ctx.lineTo(x - 3, y - 3);
|
||||
ctx.lineTo(x, y - 8);
|
||||
ctx.lineTo(x + 3, y - 3);
|
||||
ctx.lineTo(x + 8, y - 3);
|
||||
ctx.lineTo(x + 4, y + 1);
|
||||
ctx.lineTo(x + 4, y + 6);
|
||||
ctx.lineTo(x, y + 3);
|
||||
ctx.lineTo(x - 4, y + 6);
|
||||
ctx.lineTo(x - 4, y + 1);
|
||||
ctx.lineTo(x - 8, y - 3);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
37
hw5/Claudio_Maggioni/public/scripts/undo.js
Normal file
37
hw5/Claudio_Maggioni/public/scripts/undo.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
// vim: set ts=2 sw=2 et tw=80:
|
||||
|
||||
const history = {
|
||||
paths: []
|
||||
}
|
||||
|
||||
history.pop = () => {
|
||||
if (history.paths.length == 0) return;
|
||||
return history.paths.pop();
|
||||
};
|
||||
|
||||
history.initializeNewPath = () => {
|
||||
history.paths.push([]);
|
||||
};
|
||||
|
||||
history.push = (stroke) => {
|
||||
if (!stroke || !stroke instanceof Stroke) {
|
||||
throw new Error(JSON.stringify(stroke) + ' is not a Stroke instance');
|
||||
}
|
||||
history.paths[history.paths.length - 1].push(stroke);
|
||||
return history.paths[history.paths.length - 1];
|
||||
}
|
||||
|
||||
history.clear = () => {
|
||||
history.paths = [];
|
||||
};
|
||||
|
||||
|
||||
|
||||
class Stroke {
|
||||
constructor(brushName, strokeStyle, x, y) {
|
||||
this.brushName = brushName;
|
||||
this.strokeStyle = strokeStyle;
|
||||
this.offsetX = x;
|
||||
this.offsetY = y;
|
||||
}
|
||||
}
|
133
hw5/Claudio_Maggioni/public/style.css
Normal file
133
hw5/Claudio_Maggioni/public/style.css
Normal file
|
@ -0,0 +1,133 @@
|
|||
/* vim: set ts=2 sw=2 et tw=80: */
|
||||
|
||||
body {
|
||||
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
border: 1px solid #bbb;
|
||||
}
|
||||
|
||||
#left-toolbar, #brush-toolbar {
|
||||
width: 6rem;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
.toolbar button, .toolbar input {
|
||||
width: 100%;
|
||||
padding: .5rem 0;
|
||||
margin-bottom: 10px;
|
||||
min-height: 3rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#brush-toolbar {
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
#palette {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
width: 53px;
|
||||
height: 204px;
|
||||
background-color: buttonface;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar #camera-btn {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
color: #444;
|
||||
background: #eee;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
height: 40px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.toolbar #camera-btn:hover {
|
||||
background: #70a0e8;
|
||||
}
|
||||
|
||||
.toolbar #camera-btn:active {
|
||||
background: #0e57c3;
|
||||
}
|
||||
|
||||
.p-color {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.black {background-color:rgb(0, 0, 0);}
|
||||
.dark-gray {background-color:rgb(87, 87, 87);}
|
||||
.red {background-color:rgb(173, 35, 35);}
|
||||
.blue {background-color:rgb(42, 75, 215);}
|
||||
.green {background-color:rgb(29, 105, 20);}
|
||||
.brown {background-color:rgb(129, 74, 25);}
|
||||
.purple {background-color:rgb(129, 38, 192);}
|
||||
.light-gray {background-color:rgb(160, 160, 160);}
|
||||
.light-green {background-color:rgb(129, 197, 122);}
|
||||
.light-blue {background-color:rgb(157, 175, 255);}
|
||||
.cyan {background-color:rgb(41, 208, 208);}
|
||||
.orange {background-color:rgb(255, 146, 51);}
|
||||
.yellow {background-color:rgb(255, 238, 51);}
|
||||
.tan {background-color:rgb(233, 222, 187);}
|
||||
.pink {background-color:rgb(255, 205, 243);}
|
||||
.white {background-color:rgb(255, 255, 255);}
|
||||
|
||||
#clock {
|
||||
background-color: #ddd;
|
||||
width: 360px;
|
||||
height: 48px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
font-size: 2.5rem;
|
||||
margin-top: -10px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#clock-time,
|
||||
#progress-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#clock-time {
|
||||
z-index: 10;
|
||||
line-height: 48px; /* same height as #clock so that the time is vertically centered */
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
width: 0%;
|
||||
height: 48px;
|
||||
background-color: #8DFF80;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#favourites {
|
||||
margin: 5px;
|
||||
float: left;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#favourites img {
|
||||
width: 90%;
|
||||
height: 100px;
|
||||
box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.42);
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#favourites div.desc {
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
26
hw5/Claudio_Maggioni/public/test.html
Normal file
26
hw5/Claudio_Maggioni/public/test.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Exercise 3 - Object-oriented Javascript</title>
|
||||
<link rel="stylesheet" href="resources/qunit.css">
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="test-canvas" width="600" height="400" style="display: none"></canvas>
|
||||
|
||||
<div id="qunit"></div>
|
||||
<div id="qunit-fixture"></div>
|
||||
<script src="scripts/brushes.js"></script>
|
||||
<script src="scripts/undo.js"></script>
|
||||
<script src="scripts/clock.js"></script>
|
||||
<script src="scripts/app.js"></script>
|
||||
<script src="resources/qunit-2.4.0.js"></script>
|
||||
<script src="resources/lodash.js"></script>
|
||||
<script src="resources/jsverify.standalone.js"></script>
|
||||
<script src="qunit-compat.js"></script>
|
||||
<script src="test.js"></script>
|
||||
<script>
|
||||
tests();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
103
hw5/Claudio_Maggioni/public/test.js
Normal file
103
hw5/Claudio_Maggioni/public/test.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
function tests() {
|
||||
|
||||
test("App class constructors and methods", function(assert) {
|
||||
// The App class must be defined
|
||||
equal(typeof App === 'function', true, "The App class must be defined");
|
||||
equal(/^\s*class\s+/.test(App.toString()), true, "App is a function but it is not defined using the class keyword")
|
||||
|
||||
|
||||
// The App class constructor should throw an error if its argument is undefined
|
||||
assert.throws(function() {
|
||||
new App(undefined)
|
||||
}, "The App class constructor should throw an error if its argument is undefined")
|
||||
// The App class constructor should throw an error if its argument is not a canvas
|
||||
assert.throws(function() {
|
||||
new App("");
|
||||
}, "The App class constructor should throw an error if its argument is not an object")
|
||||
assert.throws(function() {
|
||||
new App(1);
|
||||
}, "The App class constructor should throw an error if its argument is a number")
|
||||
assert.throws(function() {
|
||||
new App([]);
|
||||
}, "The App class constructor should throw an error if its argument is an array")
|
||||
assert.throws(function() {
|
||||
new App(true);
|
||||
}, "The App class constructor should throw an error if its argument is a boolean")
|
||||
|
||||
// The default Stroke Style should be accessible in a static way, and should be equal to "black"
|
||||
equal(App.defaultStrokeStyle === 'black', true, 'The default Stroke Style should be accessible in a static way, and should be equal to "black"')
|
||||
|
||||
assert.throws(function() {
|
||||
new App({});
|
||||
}, "The App class constructor should throw an error if its argument options object is not pointing to a canvas element under the 'canvas' property")
|
||||
|
||||
const app = new App({canvas: 'test-canvas'})
|
||||
equal(app.strokeStyle, "black", "Getter for strokeStyle is not defined")
|
||||
|
||||
// The draw method must be defined
|
||||
equal(typeof app.draw === 'function', true, "The draw method must be defined")
|
||||
});
|
||||
|
||||
|
||||
test("History and Stroke object literals fields and methods", function(assert) {
|
||||
// The Stroke class must be defined
|
||||
equal(typeof Stroke === 'function', true, "The Stroke class must be defined");
|
||||
equal(/^\s*class\s+/.test(Stroke.toString()), true, "Stroke is a function but it is not defined using the class keyword")
|
||||
|
||||
equal(function(){
|
||||
try {
|
||||
const stroke = new Stroke('square')
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}(), true, "Stroke can be instantiated")
|
||||
|
||||
const stroke = new Stroke('square');
|
||||
const stroke1 = new Stroke('circle');
|
||||
const stroke2 = new Stroke('triangle');
|
||||
history.initializeNewPath()
|
||||
|
||||
assert.throws(function() {
|
||||
history.push()
|
||||
}, "Must pass a Stroke instance when you push in the history")
|
||||
|
||||
equal(function() {
|
||||
try {
|
||||
history.push(stroke);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}(), true, "History push accepts Stroke instances as a parameter");
|
||||
|
||||
equal(history.pop()[0] === stroke, true, "Pop returns an array containing the pushed Stroke instance")
|
||||
|
||||
history.initializeNewPath();
|
||||
|
||||
history.push(stroke);
|
||||
history.push(stroke1);
|
||||
history.push(stroke2);
|
||||
|
||||
equal(history.pop().length, 3, "Pop returns an array containing the pushed Stroke instances")
|
||||
|
||||
history.initializeNewPath();
|
||||
|
||||
equal(history.pop().length, 0, "Pop on an empty history should return an empty array")
|
||||
|
||||
history.initializeNewPath(); //simulate mouse down
|
||||
|
||||
history.push(stroke); //simulate mouse move
|
||||
history.push(stroke1); //simulate mouse move
|
||||
|
||||
history.initializeNewPath(); //simulate mouse up and down again
|
||||
|
||||
history.push(stroke2); //simulate mouse move
|
||||
|
||||
equal(history.pop().length, 1, "Pop returns an array containing the most recent path (Expected path with 1 Stroke)")
|
||||
|
||||
equal(history.pop().length, 2, "Pop returns an array containing the most recent path (Expected path with 2 Strokes)")
|
||||
|
||||
});
|
||||
}
|
133
hw5/Claudio_Maggioni/routes/favourites/router.js
Normal file
133
hw5/Claudio_Maggioni/routes/favourites/router.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
/** @module root/router */
|
||||
'use strict';
|
||||
// vim: set ts=2 sw=2 et tw=80:
|
||||
|
||||
const fs = require('fs');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
let id = 1;
|
||||
|
||||
function nextId() {
|
||||
return id++;
|
||||
}
|
||||
|
||||
function createFav(req, res) {
|
||||
if (!req.body.name || !req.body.dataURL) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
||||
res.end('Bad create form parameters');
|
||||
return;
|
||||
}
|
||||
|
||||
const favourite = {
|
||||
_id: nextId(),
|
||||
name: req.body.name,
|
||||
dataURL: req.body.dataURL
|
||||
};
|
||||
|
||||
req.app.locals.favourites.push(favourite);
|
||||
req.app.locals.writeFavs();
|
||||
|
||||
res.status = 201;
|
||||
renderOne(res, favourite);
|
||||
}
|
||||
|
||||
router.post('/', createFav);
|
||||
|
||||
function renderAll(res, favs) {
|
||||
res.format({
|
||||
html: () => res.render('favourites.dust', { favs: favs }),
|
||||
json: () => res.send(favs)
|
||||
});
|
||||
}
|
||||
|
||||
function renderOne(res, fav) {
|
||||
res.format({
|
||||
html: () => res.render('favourite.dust', fav),
|
||||
json: () => res.send(fav)
|
||||
});
|
||||
}
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
renderAll(res, req.app.locals.favourites);
|
||||
});
|
||||
|
||||
router.get('/:id', (req, res) => {
|
||||
const id = parseInt(req.params.id);
|
||||
const fav = req.app.locals.favourites.filter(e => e._id === id)[0];
|
||||
if (fav) {
|
||||
renderOne(res, fav);
|
||||
} else {
|
||||
res.writeHead(404, {'Content-Type': 'text/plain'});
|
||||
res.end('Not found');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/search', (req, res) => {
|
||||
const filtered = req.app.locals.favourites.filter(e => {
|
||||
for (const k in req.query) {
|
||||
if (k != 'dataURL' && req.query[k] != e[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
renderAll(res, filtered);
|
||||
});
|
||||
|
||||
router.put('/:id', (req, res) => {
|
||||
const edit = req.app.locals.favourites
|
||||
.find(e => e._id === parseInt(req.params.id));
|
||||
|
||||
if (!edit) {
|
||||
createFav(req, res);
|
||||
} else {
|
||||
for (const key of ['dataURL', 'name']) {
|
||||
if (req.body[key]) {
|
||||
edit[key] = req.body[key];
|
||||
} else {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
||||
res.end('Bad PUT form parameters');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
edit.bookmarked = !!req.params.bookmarked;
|
||||
req.app.locals.writeFavs();
|
||||
|
||||
res.status = 200;
|
||||
renderOne(res, edit);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
let idx = -1;
|
||||
const id = parseInt(req.params.id);
|
||||
for (let i = 0; i < req.app.locals.favourites.length; i++) {
|
||||
if (req.app.locals.favourites[i]._id === id) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx == -1) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Favourite not found');
|
||||
return;
|
||||
} else {
|
||||
req.app.locals.favourites.splice(idx, 1);
|
||||
req.app.locals.writeFavs();
|
||||
|
||||
res.format({
|
||||
json: () => res.writeHead(202),
|
||||
html: () => res.writeHead(302, { 'Location': '/favourites' })
|
||||
});
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
/** router for /root */
|
||||
module.exports = router;
|
13
hw5/Claudio_Maggioni/routes/root/router.js
Normal file
13
hw5/Claudio_Maggioni/routes/root/router.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/** @module root/router */
|
||||
'use strict';
|
||||
// vim: set ts=2 sw=2 et tw=80:
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('index.dust', { favs: req.app.locals.favourites });
|
||||
});
|
||||
|
||||
/** router for /root */
|
||||
module.exports = router;
|
35
hw5/Claudio_Maggioni/routes/routers.js
Normal file
35
hw5/Claudio_Maggioni/routes/routers.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
/** @module routes/routers
|
||||
* Exposes all routers
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const dirEntries = fs.readdirSync(__dirname);
|
||||
const base = __dirname + '/';
|
||||
const routers = {};
|
||||
|
||||
try{
|
||||
dirEntries.forEach(function(dirEntry){
|
||||
const stats = fs.statSync(base + dirEntry);
|
||||
//try to load router of dir
|
||||
if(stats.isDirectory()){
|
||||
try{
|
||||
const router = require(base + dirEntry + '/router');
|
||||
//add router to our list of routers;
|
||||
routers[dirEntry] = router;
|
||||
}catch(err){
|
||||
console.log('Could not get router for ' + dirEntry);
|
||||
console.log(err.toString() + err.stack);
|
||||
}
|
||||
}
|
||||
});
|
||||
}catch(err){
|
||||
console.log('Error while loading routers');
|
||||
console.log(err.stack);
|
||||
//We don't know what happened, export empty object
|
||||
routers = {}
|
||||
}finally{
|
||||
module.exports = routers;
|
||||
}
|
||||
|
9
hw5/Claudio_Maggioni/seed.js
Normal file
9
hw5/Claudio_Maggioni/seed.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Standalond db seed
|
||||
*/
|
||||
var seed = require('./test/seed').seed;
|
||||
|
||||
seed(function(seedData){
|
||||
console.log("Seeding complete!")
|
||||
process.exit();
|
||||
})
|
56
hw5/Claudio_Maggioni/test/routes/2.favorite.create.js
Normal file
56
hw5/Claudio_Maggioni/test/routes/2.favorite.create.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
|
||||
var config = require('../../config');
|
||||
var should = require('should');
|
||||
var seedDb = require('../seed');
|
||||
var request = require('supertest');
|
||||
|
||||
describe('Task 2: Testing Create /favorites routes', function(){
|
||||
describe('POST /favorites', function(){
|
||||
it('should create a new favorite if the request data is valid', function(done){
|
||||
var newFavData = {}
|
||||
newFavData[config.form._id] = "tt1",
|
||||
newFavData[config.form.name] = "NicePicture",
|
||||
newFavData[config.form.dataURL] = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
|
||||
newFavData[config.form.bookmarked] = "true"
|
||||
|
||||
request(config.url)
|
||||
.post('/favorites')
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.send(newFavData)
|
||||
.expect('Content-Type', /json/, 'it should respond with Content-Type: application/json' )
|
||||
.expect(201)
|
||||
.end(function(err, res){
|
||||
var resFav = JSON.parse(res.text);
|
||||
should.equal(resFav[config.form._id], newFavData[config.form._id]);
|
||||
should.equal(resFav[config.form.dataURL], newFavData[config.form.dataURL]);
|
||||
should.equal(resFav[config.form.bookmarked], newFavData[config.form.bookmarked]);
|
||||
should.equal(resFav[config.form.name], newFavData[config.form.name]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a 400 Bad Request if data is invalid #1', function(done){
|
||||
var newFavData = {
|
||||
"invalid": "this object does not have the correct structure",
|
||||
};
|
||||
|
||||
request(config.url)
|
||||
.post('/favorites')
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.send(newFavData)
|
||||
.expect(400, done);
|
||||
});
|
||||
|
||||
it('should get a 400 Bad Request if data is not in json', function(done){
|
||||
request(config.url)
|
||||
.post('/favorites')
|
||||
.set('Content-Type', 'text/plain')
|
||||
.set('Accept', 'application/json')
|
||||
.send("This is a plain text request, it should result in a 400 bad request")
|
||||
.expect(400, done);
|
||||
});
|
||||
});
|
||||
});
|
126
hw5/Claudio_Maggioni/test/routes/3.favorite.read.js
Normal file
126
hw5/Claudio_Maggioni/test/routes/3.favorite.read.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
'use strict';
|
||||
|
||||
var config = require('../../config');
|
||||
var should = require('should');
|
||||
var seedDb = require('../seed');
|
||||
var request = require('supertest');
|
||||
var favsOriginal = require('../seedData');
|
||||
var favs = []
|
||||
for (let i = 0; i < favsOriginal.length; i++) {
|
||||
let o = {}
|
||||
o[config.form._id] = favsOriginal[i]._id
|
||||
o[config.form.name] = favsOriginal[i].name
|
||||
o[config.form.dataURL] = favsOriginal[i].dataURL
|
||||
o[config.form.bookmarked] = favsOriginal[i].bookmarked
|
||||
favs.push(o)
|
||||
}
|
||||
|
||||
describe('Task 3: Testing Read for /favorites routes', function(){
|
||||
before(seed)
|
||||
|
||||
describe('GET /favorites', function(){
|
||||
it('should list all the favs with correct data', function(done){
|
||||
request(config.url)
|
||||
.get('/favorites')
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFavs = JSON.parse(res.text);
|
||||
resFavs.forEach(function(fav) {
|
||||
for (let i = 0; i < favs.length; i++) {
|
||||
if(favs[i][config.form._id] == fav[config.form._id]) {
|
||||
should.equal(fav[config.form.dataURL], favs[i][config.form.dataURL]);
|
||||
should.equal(fav[config.form.bookmarked], favs[i][config.form.bookmarked]);
|
||||
should.equal(fav[config.form.name], favs[i][config.form.name]);
|
||||
}
|
||||
}
|
||||
})
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /favorites/:favoriteid', function(){
|
||||
|
||||
it('should get the favorite with correct data', function(done){
|
||||
request(config.url)
|
||||
.get('/favorites/' + favs[1][config.form._id])
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFav = JSON.parse(res.text);
|
||||
should.equal(resFav[config.form._id], favs[1][config.form._id]);
|
||||
should.equal(resFav[config.form.dataURL], favs[1][config.form.dataURL]);
|
||||
should.equal(resFav[config.form.bookmarked], favs[1][config.form.bookmarked]);
|
||||
should.equal(resFav[config.form.name], favs[1][config.form.name]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 404 if the favorite does not exist', function(done){
|
||||
request(config.url)
|
||||
.get('/favorites/notValidId')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(404, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`GET /favorites/search`, function(){
|
||||
it(`should get the favorite with correct data: GET /favorites/search?${config.form._id}`, function(done){
|
||||
request(config.url)
|
||||
.get(`/favorites/search?${config.form._id}=${favs[1][config.form._id]}`)
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFavArray = JSON.parse(res.text);
|
||||
should.equal(resFavArray.length, 1)
|
||||
var resFav = resFavArray[0]
|
||||
should.equal(resFav[config.form._id], favs[1][config.form._id]);
|
||||
should.equal(resFav[config.form.dataURL], favs[1][config.form.dataURL]);
|
||||
should.equal(resFav[config.form.bookmarked], favs[1][config.form.bookmarked]);
|
||||
should.equal(resFav[config.form.name], favs[1][config.form.name]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should get the favorite with correct data: GET /favorites/search?${config.form._id}&${config.form.name}`, function(done){
|
||||
request(config.url)
|
||||
.get(`/favorites/search?${config.form._id}=${favs[5][config.form._id]}&${config.form.name}=${favs[5][config.form.name]}`)
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFav = JSON.parse(res.text)[0];
|
||||
should.equal(resFav[config.form._id], favs[5][config.form._id]);
|
||||
should.equal(resFav[config.form.dataURL], favs[5][config.form.dataURL]);
|
||||
should.equal(resFav[config.form.bookmarked], favs[5][config.form.bookmarked]);
|
||||
should.equal(resFav[config.form.name], favs[5][config.form.name]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should get empty array if there is no match: GET /favorites/search?${config.form.name}`, function(done){
|
||||
request(config.url)
|
||||
.get(`/favorites/search?&${config.form.name}=NoName`)
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFavArray = JSON.parse(res.text);
|
||||
should.equal(resFavArray.length, 0)
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function seed(done){
|
||||
//seed the db
|
||||
seedDb.seed(function(seedData){
|
||||
done();
|
||||
}, favs);
|
||||
}
|
43
hw5/Claudio_Maggioni/test/routes/4.favorite.update.js
Normal file
43
hw5/Claudio_Maggioni/test/routes/4.favorite.update.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
'use strict';
|
||||
|
||||
var config = require('../../config');
|
||||
var should = require('should');
|
||||
var seedDb = require('../seed');
|
||||
var request = require('supertest');
|
||||
var favs = require('../seedData');
|
||||
|
||||
describe('Task 4: Testing Update on /favorites routes', function(){
|
||||
describe('PUT /favorites/:favoriteid', function(){
|
||||
it('should change the name of an existing favorite', function(done){
|
||||
let reqBody = {}
|
||||
reqBody[config.form.name] = 'newName'
|
||||
|
||||
request(config.url)
|
||||
.put('/favorites/' + favs[3]._id)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.send(reqBody)
|
||||
.expect(201)
|
||||
.end(function(err, res){
|
||||
let resPutFav = JSON.parse(res.text)
|
||||
should(resPutFav[config.form.name], 'newName')
|
||||
done()
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
describe('GET /favorites/:favoriteid', function(){
|
||||
it('the name should be changed', function(done){
|
||||
request(config.url)
|
||||
.get('/favorites/' + favs[3]._id)
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with Content-Type: application/json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var favGetFav = JSON.parse(res.text);
|
||||
should.equal(favGetFav[config.form.name], 'newName');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
60
hw5/Claudio_Maggioni/test/routes/5.favorite.delete.js
Normal file
60
hw5/Claudio_Maggioni/test/routes/5.favorite.delete.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
'use strict';
|
||||
|
||||
var config = require('../../config');
|
||||
var should = require('should');
|
||||
var seedDb = require('../seed');
|
||||
var request = require('supertest');
|
||||
var favs = require('../seedData')
|
||||
|
||||
describe('Task 5: Testing Delete for /favorites routes', function(){
|
||||
describe('DELETE /favorites/:favoriteid', function(){
|
||||
it('should delete an existing favorite', function(done){
|
||||
request(config.url)
|
||||
.del('/favorites/' + favs[1]._id)
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(204)
|
||||
.end(function(err, res){
|
||||
res.text.should.be.empty;
|
||||
res.body.should.be.empty;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not list the previously deleted resource', function(done){
|
||||
request(config.url)
|
||||
.del('/favorites/' + favs[2]._id)
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
|
||||
request(config.url)
|
||||
.get('/favorites')
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFavs = JSON.parse(res.text);
|
||||
resFavs.forEach(function(fav){
|
||||
should.notEqual(fav[config.form._id], favs[2]._id);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 404 for a previously deleted resource', function(done){
|
||||
request(config.url)
|
||||
.delete('/favorites/' + favs[1]._id)
|
||||
.set('Accept', 'application/json')
|
||||
.expect(404, done);
|
||||
});
|
||||
|
||||
it('should respond with a 404 deleting a resource which does not exist', function(done){
|
||||
request(config.url)
|
||||
.delete('/favorites/invalidId')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(404, done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
91
hw5/Claudio_Maggioni/test/routes/6.bookmarked.read.js
Normal file
91
hw5/Claudio_Maggioni/test/routes/6.bookmarked.read.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
'use strict';
|
||||
|
||||
var config = require('../../config');
|
||||
var should = require('should');
|
||||
var seedDb = require('../seed');
|
||||
var request = require('supertest');
|
||||
var favs = require('../seedData')
|
||||
|
||||
describe('Task 6: Testing /bookmarked routes and /favorites/:favoriteid/bookmarked', function(){
|
||||
|
||||
describe('GET /bookmarked', function(){
|
||||
it('should list only the bookmarked favs', function(done){
|
||||
request(config.url)
|
||||
.get('/bookmarked')
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFavs = JSON.parse(res.text);
|
||||
resFavs.forEach(function(fav){
|
||||
should.equal(fav[config.form.bookmarked], "true");
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /favorites/:favoriteid/bookmarked', function(){
|
||||
after(drop)
|
||||
|
||||
it('initial bookmarked value should be false', function(done){
|
||||
request(config.url)
|
||||
.get(`/favorites/${favs[0]._id}`)
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFav = JSON.parse(res.text);
|
||||
should(resFav[config.form.bookmarked], "false")
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the bookmarked value', function(done){
|
||||
const reqBody = {}
|
||||
reqBody[config.form.bookmarked] = "true"
|
||||
|
||||
request(config.url)
|
||||
.put(`/favorites/${favs[0]._id}/bookmarked`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.send(reqBody)
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFav = JSON.parse(res.text);
|
||||
should(resFav[config.form.bookmarked], "true")
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('bookmarked value should be changed', function(done){
|
||||
request(config.url)
|
||||
.get(`/favorites/${favs[0]._id}`)
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/, 'it should respond with json' )
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
var resFav = JSON.parse(res.text);
|
||||
should(resFav[config.form.bookmarked], "true")
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function drop(done){
|
||||
let deleteIds = ['t0','t3','t4','t5', 'tt1']
|
||||
let count = 0
|
||||
|
||||
for (let i = 0; i < deleteIds.length; i++) {
|
||||
request(config.url)
|
||||
.delete(`/favorites/${deleteIds[i]}`)
|
||||
.end(function() {
|
||||
count++
|
||||
if(count == deleteIds.length) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
46
hw5/Claudio_Maggioni/test/seed.js
Normal file
46
hw5/Claudio_Maggioni/test/seed.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
var config = require('../config');
|
||||
var request = require('request');
|
||||
|
||||
//seedData
|
||||
var seedData = require('./seedData')
|
||||
|
||||
//total callbacks (one for each model)
|
||||
var totalCbs = 0;
|
||||
var cbCnt = 0;
|
||||
|
||||
/**
|
||||
* Recursive function that goes through
|
||||
* seedData populating each item of it
|
||||
*/
|
||||
var seedModel = function(done, s){
|
||||
if(s != undefined) {
|
||||
seedData = s
|
||||
}
|
||||
totalCbs = seedData.length
|
||||
|
||||
for (let i = 0; i < seedData.length; i++) {
|
||||
const form = {}
|
||||
form[config.form._id] = seedData[i]._id
|
||||
form[config.form.name] = seedData[i].name
|
||||
form[config.form.dataURL] = seedData[i].dataURL
|
||||
form[config.form.bookmarked] = seedData[i].bookmarked
|
||||
|
||||
request.post(`${config.url}/favorites`, {
|
||||
form: form
|
||||
}, function(error, response, body){
|
||||
cbCnt++
|
||||
if(cbCnt == totalCbs) {
|
||||
done(seedData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is where everything starts
|
||||
*/
|
||||
module.exports.seed = function (done, s){
|
||||
seedModel(done, s)
|
||||
}
|
47
hw5/Claudio_Maggioni/test/seedData.js
Normal file
47
hw5/Claudio_Maggioni/test/seedData.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
'use strict';
|
||||
|
||||
var favorites = [
|
||||
{
|
||||
"_id" : "t0",
|
||||
"name" : "My Nice Image",
|
||||
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
|
||||
"bookmarked" : "false"
|
||||
},
|
||||
|
||||
{
|
||||
"_id" : "t1",
|
||||
"name" : "My Nice Image 1",
|
||||
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
|
||||
"bookmarked" : "true"
|
||||
},
|
||||
|
||||
{
|
||||
"_id" : "t2",
|
||||
"name" : "My Nice Image 2",
|
||||
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
|
||||
"bookmarked" : "false"
|
||||
},
|
||||
|
||||
{
|
||||
"_id" : "t3",
|
||||
"name" : "My Nice Image 3",
|
||||
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
|
||||
"bookmarked" : "true"
|
||||
},
|
||||
|
||||
{
|
||||
"_id" : "t4",
|
||||
"name" : "My Nice Image 4",
|
||||
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
|
||||
"bookmarked" : "false"
|
||||
},
|
||||
|
||||
{
|
||||
"_id" : "t5",
|
||||
"name" : "MyNiceImage5",
|
||||
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
|
||||
"bookmarked" : "true"
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = favorites;
|
13
hw5/Claudio_Maggioni/views/favourite.dust
Normal file
13
hw5/Claudio_Maggioni/views/favourite.dust
Normal file
|
@ -0,0 +1,13 @@
|
|||
{! vim: set ft=html ts=2 sw=2 et tw=120: !}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{name}</title>
|
||||
</head>
|
||||
<body>
|
||||
{>"favourite_partial" /}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
17
hw5/Claudio_Maggioni/views/favourite_partial.dust
Normal file
17
hw5/Claudio_Maggioni/views/favourite_partial.dust
Normal file
|
@ -0,0 +1,17 @@
|
|||
{! vim: set ft=html ts=2 sw=2 et tw=120: !}
|
||||
|
||||
<h3>{name}</h3>
|
||||
<img src="{dataURL}" alt="{name}">
|
||||
{?details}
|
||||
<a href="/favourites/{_id}">Details</a>
|
||||
{:else}
|
||||
<form method="POST" action="/favourites/{_id}?_method=PUT">
|
||||
<input type="hidden" name="dataURL" value="{dataURL}">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" name="name" placeholder="Name" value="{name}"><br>
|
||||
<button>Update</button><br>
|
||||
<button formaction="/favourites/{_id}?_method=DELETE">Delete</button><br>
|
||||
<button formaction="/favourites/{_id}/bookmarked?_method=PUT">Add bookmark</button>
|
||||
</form>
|
||||
<a href="/favourites">Favourites list</a>
|
||||
{/details}
|
20
hw5/Claudio_Maggioni/views/favourites.dust
Normal file
20
hw5/Claudio_Maggioni/views/favourites.dust
Normal file
|
@ -0,0 +1,20 @@
|
|||
{! vim: set ft=html ts=2 sw=2 et tw=120: !}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Favourites</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Favourites</h1>
|
||||
{#favs}
|
||||
<div>
|
||||
{>"favourite_partial" name=name dataURL=dataURL _id=_id details="true" /}
|
||||
</div>
|
||||
{:else}
|
||||
<strong>No favourites.</strong>
|
||||
{/favs}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
41
hw5/Claudio_Maggioni/views/index.dust
Normal file
41
hw5/Claudio_Maggioni/views/index.dust
Normal file
|
@ -0,0 +1,41 @@
|
|||
{! vim: set ft=html ts=2 sw=2 et tw=120: !}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OO-JS Exercise - Web Atelier 2017</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body onload="init()">
|
||||
<h1>
|
||||
OO-JS Exercise: Canvas
|
||||
</h1>
|
||||
<div id="app">
|
||||
<div id="left-toolbar" class="toolbar">
|
||||
<button id="clear-btn">Clear</button>
|
||||
<button id="undo-btn">Undo</button>
|
||||
<button id="camera-btn"><i class="fa fa-camera" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<canvas id="canvas" width="600" height="400"></canvas>
|
||||
<div id="brush-toolbar" class="toolbar">
|
||||
<!-- Brushes buttons go here (programmatically). Each button should be a <button> element -->
|
||||
</div>
|
||||
</div>
|
||||
<h2>Favourites</h2>
|
||||
<div id="favourites">
|
||||
{#favs}
|
||||
{>"favourite_partial" name=name dataURL=dataURL _id=_id bookmarked=bookmarked details="true" /}
|
||||
{/favs}
|
||||
</div>
|
||||
<script src="scripts/brushes.js"></script>
|
||||
<script src="scripts/undo.js"></script>
|
||||
<!-- <script src="scripts/clock.js"></script> -->
|
||||
<script src="scripts/app.js"></script>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
Reference in a new issue