hw6: done task 1 & 2 (tests pass)

This commit is contained in:
Claudio Maggioni 2019-10-28 15:00:36 +01:00
parent f8d3a2a323
commit c6293aea8f
37 changed files with 31910 additions and 0 deletions

1
hw6/Claudio_Maggioni/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
db.json

View file

@ -0,0 +1,45 @@
// 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 mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/SA3_hw6', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
require('./models/Favorites');
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'));
// Initialize routers here
const routers = require('./routes/routers');
app.use('/', routers.root);
app.use('/favorites', routers.favourites_db);
app.use('/bookmarked', routers.bookmarked);
module.exports = app;

9
hw6/Claudio_Maggioni/bin/www Executable file
View 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);
});

View 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
}
}

View file

@ -0,0 +1,20 @@
// vim: set ts=2 sw=2 et tw=80:
const mongoose = require('mongoose');
const schema = new mongoose.Schema({
_id: {},
dataURL: { type: String, required: true },
name: { type: String, required: true, default: '' },
bookmarked: {
type: Boolean,
required: false,
default: false,
},
dateCreated: { type: Date, required: true, default: Date.now }
});
const Favorite = mongoose.model('Favorite', schema);

View file

@ -0,0 +1,36 @@
{
"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' nodemon ./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",
"mongoose": "^5.7.7",
"morgan": "^1.9.0",
"request": "^2.88.0",
"supertest": "^3.0.0"
},
"devDependencies": {
"mocha": "^4.0.1",
"should": "^13.1.3"
}
}

View 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'
});
}

View 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);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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;
}

View 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 = '/favorites';
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);
}
}
}

View 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();
}
}

View 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;
}
}

View 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;
}

View 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>

View 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)")
});
}

View file

@ -0,0 +1,24 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, renderFav } = require('../utils');
router.get('/', (req, res) => {
Favorite.find({ bookmarked: true }, (err, favs) => {
if (err) {
return error(err, res);
}
renderFav(req, res, favs);
});
});
/** router for /root */
module.exports = router;

View file

@ -0,0 +1,158 @@
/** @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();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, renderFav } = require('../utils');
router.post('/', (req, res) => {
if (!req.body.name || !req.body.dataURL) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad create form parameters');
return;
}
const data = {
name: req.body.name,
dataURL: req.body.dataURL,
bookmarked: req.body.bookmarked,
};
const favourite = new Favorite(data);
if (req.body._id) {
favourite._id = req.body._id;
}
favourite.save((err, fav) => {
if (err) {
return error(err, res);
}
res.status = 201;
const _id = fav._id;
renderFav(req, res, Object.assign({ _id }, data), false);
});
});
router.get('/', (req, res) => {
Favorite.find({}, (err, favs) => {
if (err) {
return error(err, res);
}
renderFav(req, res, favs);
});
});
router.get('/search', (req, res) => {
const filter = Object.assign({}, req.query);
delete filter['dataURL'];
delete filter['_method'];
Favorite.find(filter, (err, favs) => {
if (err) {
return error(err, res);
}
renderFav(req, res, favs);
});
});
router.get('/:id', (req, res) => {
Favorite.findById(req.params.id, (err, fav) => {
if (err) {
return error(err, res);
} else if (!fav) {
res.writeHead(404, {'Content-Type': 'text/plain'});
res.end('Not found');
} else {
renderFav(req, res, fav, false);
}
});
});
function handleUpdate(partial = false) {
return (req, res) => {
const edit = {};
for (const key of ['dataURL', 'name']) {
if (req.body[key]) {
edit[key] = req.body[key];
} else if (!partial) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT form parameters');
return;
}
}
if (req.body.bookmarked !== undefined) {
edit.bookmarked = !!req.body.bookmarked;
}
Favorite.findByIdAndUpdate(req.params.id, { $set: edit }, {
new: true,
upsert: true,
setDefaultsOnInsert: true,
}, (err, fav) => {
if (err) {
return error(err, res);
}
console.log(arguments);
// FIXME: return 201 on creation
res.status = 200;
renderFav(req, res, fav, false);
});
};
}
router.put('/:id', handleUpdate());
router.patch('/:id', handleUpdate(true));
router.delete('/:id', (req, res) => {
Favorite.findByIdAndDelete(req.params.id, (err, fav) => {
if (err) {
return error(err, res);
}
if (fav == null) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Favourite not found');
return;
}
res.format({
json: () => res.writeHead(204),
html: () => res.writeHead(302, { 'Location': '/favorites' })
});
res.end();
});
});
router.put('/:id/bookmarked', (req, res) => {
Favorite.findByIdAndUpdate(req.params.id, {
$set: { bookmarked: !!req.body.bookmarked }
}, { new: true }, (err, fav) => {
if (false) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Favourite to bookmark not found');
} else if (!req.body.bookmarked) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT bookmark form parameters');
} else {
res.format({
html: () => renderFav(req, res, fav, false),
json: () => res.json(fav)
});
}
});
});
/** router for /root */
module.exports = router;

View file

@ -0,0 +1,24 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error } = require('../utils');
router.get('/', (req, res) => {
Favorite.find({}, (err, favs) => {
if (err) {
return error(err, res);
}
res.render('index.dust', { favs });
});
});
/** router for /root */
module.exports = router;

View 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;
}

View file

@ -0,0 +1,39 @@
function error(err, res) {
console.error(err);
res.status = 500;
res.format({
json: () => res.json({error: err}),
html: () => res.render('500.dust', { err: JSON.stringify(err, null, 2) }),
});
res.end();
}
function renderFav(req, res, favs, list = true) {
const makeTestsPass = e => {
return {
_id: e._id,
name: e.name,
dataURL: e.dataURL,
bookmarked: '' + e.bookmarked
}
};
if (list) {
favs = favs.map(makeTestsPass);
} else {
favs = makeTestsPass(favs);
}
if (req.accepts('html')) {
res.render(favs ? 'favourites.dust' : 'favourite.dust', list ? { favs } : favs);
} else if (req.accepts('json')) {
res.json(favs);
} else {
res.writeHead(406);
res.end();
}
}
module.exports = { error, renderFav };

View file

@ -0,0 +1,9 @@
/**
* Standalond db seed
*/
var seed = require('./test/seed').seed;
seed(function(seedData){
console.log("Seeding complete!")
process.exit();
})

View 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);
});
});
});

View file

@ -0,0 +1,127 @@
'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);
console.log(resFavs);
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);
}

View file

@ -0,0 +1,44 @@
'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'
reqBody[config.form.dataURL] = favs[3][config.form.dataURL] // maggicl: added to comply with assignment
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();
});
});
});
});

View 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);
});
});
});

View 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()
}
})
}
}

View 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)
}

View 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;

View file

@ -0,0 +1,14 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<html>
<head>
<meta charset='utf-8'>
</head>
<body>
<h1>Error 500</h1>
<pre>
{err}
</pre>
</body>
</html>

View 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>

View file

@ -0,0 +1,28 @@
{! vim: set ft=html ts=2 sw=2 et tw=120: !}
<h3>{name}</h3>
<img src="{dataURL}" alt="{name}">
{?b}
<p>
<strong>Bookmarked</strong>
</p>
{/b}
{?details}
<a href="/favorites/{_id}">Details</a>
{:else}
<form method="POST" action="/favorites/{_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="/favorites/{_id}?_method=DELETE">Delete</button><br>
{?b}
<button name="bookmarked" value="false"
formaction="/favorites/{_id}/bookmarked?_method=PUT">Remove bookmark</button>
{:else}
<button name="bookmarked" value="true"
formaction="/favorites/{_id}/bookmarked?_method=PUT">Add bookmark</button>
{/b}
</form>
<a href="/favorites">Favourites list</a>
{/details}

View file

@ -0,0 +1,28 @@
{! vim: set ft=html ts=2 sw=2 et tw=120: !}
<html>
<head>
<meta charset="utf-8">
{?bookmarked}
<title>Bookmarked</title>
{:else}
<title>Favourites</title>
{/bookmarked}
</head>
<body>
{?bookmarked}
<h1>Bookmarked</h1>
{:else}
<h1>Favourites</h1>
{/bookmarked}
{#favs}
<div>
{>"favourite_partial" name=name dataURL=dataURL _id=_id b=b details="true" /}
</div>
{:else}
<strong>No favourites.</strong>
{/favs}
</body>
</html>

View 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 b=b 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>

File diff suppressed because it is too large Load diff