node_modules | |||||
db.json | |||||
conf.json |
{ | |||||
"port": 8080 | |||||
} |
var fs = require("fs"); | |||||
module.exports = function(file) { | |||||
var schema = ["list", "words"]; | |||||
var self = { | |||||
list: [], | |||||
words: [] | |||||
}; | |||||
function toobj() { | |||||
var obj = {} | |||||
schema.forEach(i => obj[i] = self[i]); | |||||
return obj; | |||||
} | |||||
self.flush = function() { | |||||
fs.writeFileSync(file, JSON.stringify(toobj())); | |||||
} | |||||
try { | |||||
db = JSON.parse(fs.readFileSync(file)); | |||||
schema.forEach(i => self[i] = db[i] || self[i]); | |||||
} catch (err) { | |||||
if (err.code !== "ENOENT") | |||||
throw err; | |||||
self.flush(); | |||||
} | |||||
return self; | |||||
} |
{ | |||||
"name": "shoplist", | |||||
"version": "1.0.0", | |||||
"main": "server.js", | |||||
"scripts": { | |||||
"test": "echo \"Error: no test specified\" && exit 1", | |||||
"start": "node server.js" | |||||
}, | |||||
"author": "Martin Dørum Nygaard <martid0311@gmail.com> (https://www.mort.coffee)", | |||||
"license": "ISC", | |||||
"dependencies": { | |||||
"express": "^4.13.4", | |||||
"webevents": "^1.0.0" | |||||
}, | |||||
"devDependencies": {}, | |||||
"description": "" | |||||
} |
var DB = require("./js/db"); | |||||
var express = require("express"); | |||||
var fs = require("fs"); | |||||
var WebEvents = require("webevents"); | |||||
var db = DB("db.json"); | |||||
var conf = JSON.parse(fs.readFileSync("conf.json")); | |||||
var events = WebEvents(); | |||||
var app = express(); | |||||
app.use(express.static("web")); | |||||
app.listen(conf.port); | |||||
app.post("/webevents/*", events.handle); | |||||
app.get("/client.js", (req, res) => { | |||||
fs.createReadStream("node_modules/webevents/client.js") | |||||
.pipe(res) | |||||
.on("error", err => res.end(err.toString())); | |||||
}); | |||||
app.get("/list", (req, res) => { | |||||
res.json(db.list); | |||||
}); | |||||
app.get("/words", (req, res) => { | |||||
res.json(db.words); | |||||
}); | |||||
app.post("/remove/:index", (req, res) => { | |||||
res.end(); | |||||
if (!req.params.index) | |||||
return; | |||||
delete db.list[req.params.index]; | |||||
events.emit("remove", { index: req.params.index }); | |||||
// Clear the entire list array if there are no elements in it | |||||
var containsElems = false; | |||||
for (var i in db.list) { | |||||
if (db.list[i] != null) { | |||||
containsElems = true; | |||||
break; | |||||
} | |||||
} | |||||
if (!containsElems) { | |||||
db.list = []; | |||||
} | |||||
db.flush(); | |||||
}); | |||||
app.post("/add/:name", (req, res) => { | |||||
res.end(); | |||||
if (!req.params.name) | |||||
return; | |||||
var name = req.params.name.toLowerCase(); | |||||
db.list.push(name); | |||||
events.emit("add", { name: name, index: db.list.length - 1 }); | |||||
var word = db.words.filter(w => w.name === name)[0]; | |||||
if (word) | |||||
word.count += 1; | |||||
else | |||||
db.words.push({ name: name, count: 1 }); | |||||
db.flush(); | |||||
}); | |||||
console.log("Server listening on port "+conf.port); |
<?xml version="1.0" encoding="utf-8"?> | |||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | |||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |||||
width="8px" height="8px" viewBox="0 0 8 8" enable-background="new 0 0 8 8" xml:space="preserve"> | |||||
<rect x="-0.013" y="4.258" transform="matrix(-0.707 -0.7072 0.7072 -0.707 0.0891 10.1702)" width="4.33" height="1.618"/> | |||||
<rect x="2.227" y="2.899" transform="matrix(-0.7071 0.7071 -0.7071 -0.7071 11.6877 2.6833)" width="6.121" height="1.726"/> | |||||
</svg> |
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<meta charset="utf-8"> | |||||
<title>Shopping List</title> | |||||
<link rel="stylesheet" href="/style.css"> | |||||
</head> | |||||
<body> | |||||
<div id="container"> | |||||
<form id="add"> | |||||
<input class="name"> | |||||
<button class="submit" type="submit">+</button> | |||||
<div class="suggestions hidden"></div> | |||||
</form> | |||||
<div id="list"> | |||||
</div> | |||||
</div> | |||||
<script src="/client.js"></script> | |||||
<script src="/script.js"></script> | |||||
</body> | |||||
</html> |
var elemAdd = document.querySelector("#add"); | |||||
var elemName = elemAdd.querySelector(".name"); | |||||
var elemSuggestions = elemAdd.querySelector(".suggestions"); | |||||
var elemList = document.querySelector("#list"); | |||||
elemSuggestions.show = function() { | |||||
elemSuggestions.className = "suggestions"; | |||||
elmeSuggestions.style.display = "block"; | |||||
} | |||||
elemSuggestions.hide = function() { | |||||
elemSuggestions.className = "suggestions hidden"; | |||||
setTimeout(function() { | |||||
elmeSuggestions.style.display = "none"; | |||||
}, 500); | |||||
} | |||||
var events = WebEvents(); | |||||
var words = []; | |||||
function capitalize(word) { | |||||
var first = word[0].toUpperCase(); | |||||
return first + word.substring(1); | |||||
} | |||||
function remove(index) { | |||||
var elem; | |||||
for (var i in elemList.childNodes) { | |||||
var e = elemList.childNodes[i]; | |||||
var eindex = parseInt(e.querySelector(".index").value); | |||||
if (eindex == index) { | |||||
elem = e; | |||||
break; | |||||
} | |||||
} | |||||
if (!elem) | |||||
return; | |||||
elem.className += " hidden"; | |||||
setTimeout(function() { | |||||
elem.parentNode.removeChild(elem); | |||||
}, 1000); | |||||
} | |||||
function add(word, index, animate) { | |||||
if (!word) | |||||
return; | |||||
var elem = document.createElement("div"); | |||||
if (animate) | |||||
elem.className = "elem hidden"; | |||||
else | |||||
elem.className = "elem"; | |||||
var elemName = document.createElement("span"); | |||||
elemName.className = "name"; | |||||
elemName.textContent = capitalize(word); | |||||
var elemIndex = document.createElement("input"); | |||||
elemIndex.className = "index"; | |||||
elemIndex.value = index; | |||||
elemIndex.type = "hidden"; | |||||
var elemOk = document.createElement("span"); | |||||
elemOk.className = "ok"; | |||||
elemOk.addEventListener("click", function() { | |||||
post("/remove/"+index); | |||||
}); | |||||
elem.appendChild(elemIndex); | |||||
elem.appendChild(elemName); | |||||
elem.appendChild(elemOk); | |||||
list.insertBefore(elem, list.firstChild); | |||||
if (animate) { | |||||
setTimeout(function() { | |||||
elem.className = "elem"; | |||||
}, 100); | |||||
} | |||||
} | |||||
function request(method, url, cb) { | |||||
var xhr = new XMLHttpRequest(); | |||||
xhr.addEventListener("load", function() { | |||||
cb(null, JSON.parse(xhr.responseText)); | |||||
}); | |||||
xhr.addEventListener("error", function(err) { | |||||
if (cb) | |||||
cb(err); | |||||
}); | |||||
xhr.open(method, url); | |||||
xhr.send(); | |||||
} | |||||
function get(url, cb) { | |||||
request("GET", url, cb); | |||||
} | |||||
function post(url, cb) { | |||||
request("POST", url, cb); | |||||
} | |||||
function clearChildren(elem) { | |||||
while (elem.firstChild) { | |||||
elem.removeChild(elem.firstChild); | |||||
} | |||||
} | |||||
elemAdd.addEventListener("submit", function(evt) { | |||||
evt.preventDefault(); | |||||
if (elemName.value == "") | |||||
return; | |||||
post("/add/"+encodeURIComponent(elemName.value)); | |||||
elemName.value = ""; | |||||
setTimeout(function() { | |||||
elemName.blur(); | |||||
}, 1); | |||||
}); | |||||
function displaySuggestions() { | |||||
setTimeout(function() { | |||||
var name = elemName.value; | |||||
var suggestions = words | |||||
.filter(function(w) { | |||||
return w.name.indexOf(name.toLowerCase()) !== -1; | |||||
}) | |||||
.sort(function(a, b) { | |||||
return a.count - b.count; | |||||
}); | |||||
clearChildren(elemSuggestions); | |||||
suggestions.forEach(function(w) { | |||||
var elem = document.createElement("div"); | |||||
elem.className = "suggestion"; | |||||
elem.textContent = capitalize(w.name); | |||||
elemSuggestions.insertBefore(elem, elemSuggestions.firstChild); | |||||
elem.addEventListener("click", function() { | |||||
console.log(w); | |||||
post("/add/"+encodeURIComponent(w.name)); | |||||
elemSuggestions.hide(); | |||||
elemName.value = ""; | |||||
elemName.blur(); | |||||
}); | |||||
}); | |||||
if (suggestions.length > 0) | |||||
elemSuggestions.show(); | |||||
else | |||||
elemSuggestions.hide(); | |||||
}, 10); | |||||
} | |||||
elemName.addEventListener("keydown", displaySuggestions); | |||||
elemName.addEventListener("focus", displaySuggestions); | |||||
elemName.addEventListener("blur", function(evt) { | |||||
elemSuggestions.hide(); | |||||
}); | |||||
events.on("add", function(evt) { | |||||
add(evt.name, evt.index, true); | |||||
var word = words.filter(function(w) { | |||||
return w.name === evt.name; | |||||
})[0]; | |||||
if (word) | |||||
word.count += 1; | |||||
else | |||||
words.push({ name: evt.name, count: 1 }); | |||||
}); | |||||
events.on("remove", function(evt) { | |||||
remove(evt.index); | |||||
}); | |||||
get("/list", function(err, list) { | |||||
list.forEach(function(word, index) { add(word, index); }); | |||||
}); | |||||
get("/words", function(err, w) { | |||||
if (err) | |||||
return console.error(err); | |||||
words = w; | |||||
}); |
html, body { | |||||
margin: 0px; | |||||
height: 100%; | |||||
overflow: hidden; | |||||
} | |||||
* { | |||||
box-sizing: border-box; | |||||
} | |||||
#container { | |||||
position: relative; | |||||
overflow: hidden; | |||||
margin: auto; | |||||
text-align: center; | |||||
width: 100%; | |||||
max-width: 500px; | |||||
height: 100%; | |||||
} | |||||
#add { | |||||
margin-top: 12px; | |||||
} | |||||
#add .name { | |||||
width: 80%; | |||||
height: 30px; | |||||
} | |||||
#add .submit { | |||||
width: 10%; | |||||
height: 30px; | |||||
} | |||||
#add .suggestions { | |||||
transition: opacity 0.2s; | |||||
display: none; | |||||
z-index: 2; | |||||
display: inline; | |||||
background: white; | |||||
box-shadow: 0px 0px 10px 1px black; | |||||
position: absolute; | |||||
width: 90%; | |||||
margin: auto; | |||||
left: 0px; | |||||
right: 0px; | |||||
margin-top: 45px; | |||||
padding-top: 12px; | |||||
padding-bottom: 12px; | |||||
max-height: 80%; | |||||
overflow-y: auto; | |||||
} | |||||
#add .suggestions.hidden { | |||||
opacity: 0; | |||||
} | |||||
#add .suggestions .suggestion:last-child { | |||||
border-bottom: none; | |||||
} | |||||
#add .suggestions .suggestion { | |||||
padding: 12px; | |||||
cursor: pointer; | |||||
border-bottom: 1px solid #666; | |||||
} | |||||
#list { | |||||
margin-top: 24px; | |||||
} | |||||
#list .elem { | |||||
transition: padding 0.3s, height 0.3s; | |||||
padding-left: 12px; | |||||
padding-top: 32px; | |||||
padding-bottom: 3px; | |||||
position: relative; | |||||
margin: auto; | |||||
text-align: left; | |||||
width: 100%; | |||||
height: 50px; | |||||
overflow: hidden; | |||||
border-bottom: 1px solid #666; | |||||
} | |||||
#list .elem.hidden { | |||||
transition: transform 0.3s ease-in, | |||||
padding 0.2s 0.1s ease-in, | |||||
height 0.2s 0.1s ease-in; | |||||
transform: translateX(-150%); | |||||
height: 0px; | |||||
padding: 0px; | |||||
} | |||||
#list .elem .ok { | |||||
position: relative; | |||||
display: inline-block; | |||||
float: right; | |||||
margin-right: 6px; | |||||
height: 40px; | |||||
width: 40px; | |||||
bottom: 25px; | |||||
background-image: url(/icon-check.svg); | |||||
background-size: 24px 24px; | |||||
background-repeat: no-repeat; | |||||
background-position: center; | |||||
cursor: pointer; | |||||
border-radius: 100px; | |||||
background-color: #eee; | |||||
} |