@@ -0,0 +1,3 @@ | |||
node_modules | |||
db.json | |||
conf.json |
@@ -0,0 +1,3 @@ | |||
{ | |||
"port": 8080 | |||
} |
@@ -0,0 +1,32 @@ | |||
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; | |||
} |
@@ -0,0 +1,17 @@ | |||
{ | |||
"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": "" | |||
} |
@@ -0,0 +1,73 @@ | |||
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); |
@@ -0,0 +1,8 @@ | |||
<?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> |
@@ -0,0 +1,23 @@ | |||
<!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> |
@@ -0,0 +1,188 @@ | |||
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; | |||
}); |
@@ -0,0 +1,106 @@ | |||
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; | |||
} |