Browse Source

yay commit

master
mortie 7 years ago
commit
4c36839f42
9 changed files with 453 additions and 0 deletions
  1. 3
    0
      .gitignore
  2. 3
    0
      conf.json.example
  3. 32
    0
      js/db.js
  4. 17
    0
      package.json
  5. 73
    0
      server.js
  6. 8
    0
      web/icon-check.svg
  7. 23
    0
      web/index.html
  8. 188
    0
      web/script.js
  9. 106
    0
      web/style.css

+ 3
- 0
.gitignore View File

@@ -0,0 +1,3 @@
node_modules
db.json
conf.json

+ 3
- 0
conf.json.example View File

@@ -0,0 +1,3 @@
{
"port": 8080
}

+ 32
- 0
js/db.js View File

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

+ 17
- 0
package.json View File

@@ -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": ""
}

+ 73
- 0
server.js View File

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

+ 8
- 0
web/icon-check.svg View File

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

+ 23
- 0
web/index.html View File

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

+ 188
- 0
web/script.js View File

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

+ 106
- 0
web/style.css View File

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

Loading…
Cancel
Save