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