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