initial commit

This commit is contained in:
Timon Ringwald 2022-09-08 22:55:43 +02:00
commit 0b0e08bcb0
9 changed files with 11765 additions and 0 deletions

11174
data.js Normal file

File diff suppressed because it is too large Load Diff

BIN
icons.ttf Normal file

Binary file not shown.

49
index.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kcalculator</title>
<link href="style.css" type="text/css" rel="stylesheet">
<script src="jst.js"></script>
<script src="utils.js"></script>
<script src="data.js"></script>
<script defer src="main.js"></script>
</head>
<body>
<header>
<h1>Kcalculator</h1>
</header>
<datalist id="food-list"></datalist>
<main>
<div id="food-search-container">
<input type="search" list="food-list" id="food-search" name="food-search" placeholder="Lebensmittel">
<div id="search-entries">
</div>
</div>
<div id="food-menu">
</div>
<div id="summary">
<span class="summary-name">Gesamt</span>
<a class="summary-delete-icon"><span class="material-icons">delete</span></a>
<div class="summary-amount-container">
<div class="summary-kcal">123</div>
<span class="summary-kcal-unit">kcal</span><span class="summary-kcal-unit-sep">/</span><input class="summary-amount" type="number" pattern="[0-9]+"><span class="summary-unit">g</span>
</div>
</div>
</main>
</body>
</html>

76
jst.js Normal file
View File

@ -0,0 +1,76 @@
const JST = {
_cachedTemplates: {},
_fetchTemplate: (name) => {
if (JST._cachedTemplates[name] !== undefined) {
return new Promise(resolve => resolve(JST._cachedTemplates[name]));
}
return fetch(`/templates/${name}.html`).then(resp => resp.text()).then(resp => {
const div = document.createElement("div");
div.innerHTML = resp;
const template = div.querySelector("template");
JST._cachedTemplates[name] = template;
return template;
});
},
// load returns a Promise for the template node
load: (templateName, visitedTemplates) => {
visitedTemplates = visitedTemplates ?? [];
const visited = visitedTemplates.includes(templateName);
visitedTemplates.push(templateName);
if (visited) {
const errorMsg = document.createElement("span");
errorMsg.textContent = `recursive templates: ${visitedTemplates.join(" → ")}`;
errorMsg.style.color = "red";
return new Promise(resolve => resolve(errorMsg));
}
return JST._fetchTemplate(templateName).then(template => {
const c = template.content.cloneNode(true);
JST.init(c, visitedTemplates);
return c;
});
},
// replaceElement replaces the given element with the template node
replaceElement: async (templateName, element) => {
await JST._replaceElement(templateName, element, []);
},
_replaceElement: async (templateName, element, visitedTemplates) => {
element.textContent = "";
await JST.load(templateName, visitedTemplates).then(node => element.replaceWith(node))
},
// replaceChildren removes all children in element and adds the template node
replaceChildren: async (templateName, element) => {
element = element ?? document.querySelector("main");
const node = await JST.load(templateName);
while (element.hasChildNodes()) {
element.removeChild(element.firstChild);
}
element.appendChild(node);
},
// append appends the template node to elements childrens
append: async (templateName, element) => {
element = element ?? document.querySelector("main");
const node = await JST.load(templateName);
element.appendChild(node);
},
init: (element, visitedTemplates) => {
element = element ?? document;
visitedTemplates = visitedTemplates ?? [];
element.querySelectorAll("jst-template").forEach(element => JST._replaceElement(element.textContent, element, visitedTemplates));
}
}
window.addEventListener("load", () => JST.init());

129
main.js Normal file
View File

@ -0,0 +1,129 @@
const foodSearch = document.querySelector("#food-search");
const searchEntries = document.querySelector("#search-entries");
const foodMenu = document.querySelector("#food-menu");
const summary = document.querySelector("#summary");
const summaryKcal = document.querySelector("#summary .summary-kcal");
updateSummary();
initFoodList();
foodSearch.addEventListener("input", onFoodSearched);
if (foodSearch.value != "") onFoodSearched();
function initFoodList() {
const dataList = document.querySelector("#food-list");
for (let food of foods) {
const option = document.createElement("option");
option.setAttribute("value", food.name);
dataList.appendChild(option);
}
}
function onFoodSearched() {
const food = foodSearch.value;
const foodLC = food.toLowerCase();
if (food == "") {
createSearchEntries([]);
return;
}
const foodsCopy = structuredClone(foods);
foodsCopy.sort((a, b) => {
const aNameLC = a.name.toLowerCase();
const bNameLC = b.name.toLowerCase();
const ainc = aNameLC.includes(foodLC);
const binc = bNameLC.includes(foodLC);
if (ainc && binc) {
return 0;
} else if (ainc) {
return -1;
} else if (binc) {
return 1;
}
const distA = levenshtein(foodLC, aNameLC);
const distB = levenshtein(foodLC, bNameLC);
return distA - distB;
})
const maxDist = 10;
let maxAmount = 10;
let amount = 0;
for (let f of foodsCopy) {
const fnameLC = f.name.toLowerCase();
if (fnameLC.includes(foodLC)) {
amount++;
maxAmount++;
continue;
}
const dist = levenshtein(foodLC, fnameLC);
if (dist >= maxDist) break;
amount++;
}
createSearchEntries(foodsCopy.slice(0, Math.min(amount, maxAmount, 30)));
}
async function createSearchEntries(foods) {
const nodes = [];
for (let food of foods) {
const node = await JST.load("search-entry");
node.querySelector(".search-entry-name").textContent = food.name;
node.querySelector(".search-entry").addEventListener("click", () => addMenuItem(food));
nodes.push(node);
}
searchEntries.replaceChildren(...nodes);
}
async function addMenuItem(food) {
const alreadyAddedMenuItem = document.querySelector(`#food-menu .menu-item[data-food-name="${food.name}"]`);
if (alreadyAddedMenuItem !== null) {
alreadyAddedMenuItem.style.animation = "highlight 0.25s linear";
setTimeout(() => alreadyAddedMenuItem.style.animation = "", 250);
return;
}
const node = await JST.load("menu-item");
const nodeRoot = node.querySelector(".menu-item");
const itemAmount = node.querySelector(".menu-item-amount");
const kcalAmount = node.querySelector(".menu-item-kcal-for-amount");
nodeRoot.setAttribute("data-food-name", food.name);
node.querySelector(".menu-item-name").textContent = food.name;
node.querySelector(".menu-item-kcal").textContent = `${food.kcal} kcal/100${food.unit}`;
node.querySelector(".menu-item-unit").textContent = food.unit;
node.querySelector(".delete-menu-item").addEventListener("click", () => {
foodMenu.removeChild(nodeRoot);
updateSummary();
});
const updateKcalAmount = () => {
if (!/[0-9]+/.test(itemAmount.value)) {
console.log("no match");
return;
}
const amount = parseInt(itemAmount.value);
kcalAmount.textContent = parseInt(Math.round(food.kcal / 100 * amount));
updateSummary();
}
itemAmount.value = 100;
itemAmount.addEventListener("input", updateKcalAmount);
updateKcalAmount();
foodMenu.appendChild(node);
updateSummary();
}
function updateSummary() {
let sum = 0;
for (let menuItem of foodMenu.querySelectorAll(".menu-item")) {
sum += parseInt(menuItem.querySelector(".menu-item-kcal-for-amount").textContent);
}
summaryKcal.textContent = sum;
summary.style.display = sum > 0 ? "block" : "none";
}

223
style.css Normal file
View File

@ -0,0 +1,223 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url("/icons.ttf");
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url("/icons.ttf") format('truetype');
}
.material-icons {
transform: translateY(0.1em);
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 1em; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
user-select: none;
/* Support for all WebKit browsers. */
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}
* {
margin: 0;
padding: 0;
}
html, body {
min-height: 100vh;
font-size: 100%;
}
header {
font-size: 1.5rem;
line-height: 5em;
text-align: center;
background-color: #add8e6;
color: #333;
}
#food-search-container {
margin: 1em;
text-align: center;
}
#food-search {
margin-bottom: 0.25em;
border: 1px solid darkgray;
border-radius: 0.15em;
padding: 0.25em;
outline: 0;
box-sizing: border-box;
width: 20rem;
max-width: 100%;
}
#search-entries {
margin: 0 auto;
max-width: 100%;
display: flex;
flex-wrap: wrap;
gap: 0.25em;
justify-content: center;
}
.search-entry {
background-color: #eee;
border: 1px solid darkgray;
padding: 0.15em;
border-radius: 0.15em;
cursor: pointer;
transition: all 0.25s ease-in-out;
}
.search-entry:hover {
background-color: #ddd;
transition: all 0.25s ease-in-out;
}
#food-menu,
#summary {
width: 60em;
max-width: 100%;
margin: 0 auto;
}
.menu-item {
padding: 0.5em;
box-sizing: border-box;
}
.menu-item:not(:last-child) {
border-bottom: 1px solid lightgray;
}
.delete-menu-item,
.summary-delete-icon {
display: block;
float: right;
color: #cc3d3d;
width: 1.5em;
height: 1.5em;
text-align: center;
cursor: pointer;
transition: all 0.25s ease-in-out;
}
.delete-menu-item:hover,
.delete-menu-item:focus {
scale: 1.5;
transition: all 0.25s ease-in-out;
}
.menu-item-kcal {
color: darkgray;
font-size: 0.65em;
transform: translateY(-0.15em);
display: inline-block;
}
.menu-item-amount-container,
.summary-amount-container {
float: right;
margin-right: 0.2em;
}
.menu-item-amount,
.summary-amount {
font-size: 1rem;
border: 1px solid lightgray;
box-sizing: border-box;
outline: 0;
width: 3em;
text-align: right;
margin-left: 0.3em;
border-radius: 0.2em;
}
.menu-item-unit,
.summary-unit {
display: inline-block;
margin-left: 0.2em;
min-width: 1.5em;
transform: translateX(-0.1em);
}
.menu-item-amount:hover,
.menu-item-amount:focus {
border: 1px solid darkgray;
}
.menu-item-amount:invalid {
border: 1px solid red;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
@keyframes highlight {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.menu-item-kcal-unit,
.summary-kcal-unit {
margin-right: 0.25em;
}
.menu-item-kcal-for-amount,
.menu-item-kcal-unit {
color: #555;
}
#summary {
border-top: 1px solid black;
padding: 0.5em;
box-sizing: border-box;
}
.summary-amount,
.summary-unit,
.summary-delete-icon,
.summary-kcal-unit-sep {
visibility: hidden;
}
.summary-kcal,
.summary-kcal-unit,
.summary-name {
display: inline;
font-weight: bold;
}

13
templates/menu-item.html Normal file
View File

@ -0,0 +1,13 @@
<template>
<div class="menu-item">
<span class="menu-item-name"></span>
<span class="menu-item-kcal"></span>
<a class="delete-menu-item"><span class="material-icons">delete</span></a>
<div class="menu-item-amount-container">
<span class="menu-item-kcal-for-amount">123</span>
<span class="menu-item-kcal-unit">kcal</span>/<input class="menu-item-amount" type="number" pattern="[0-9]+"><span class="menu-item-unit"></span>
</div>
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div class="search-entry">
<span class="search-entry-name"></span>
</div>
</template>

96
utils.js Normal file
View File

@ -0,0 +1,96 @@
function levenshtein(s, t) {
if (s === t) {
return 0;
}
var n = s.length, m = t.length;
if (n === 0 || m === 0) {
return n + m;
}
var x = 0, y, a, b, c, d, g, h;
var p = new Uint16Array(n);
var u = new Uint32Array(n);
for (y = 0; y < n;) {
u[y] = s.charCodeAt(y);
p[y] = ++y;
}
for (; (x + 3) < m; x += 4) {
var e1 = t.charCodeAt(x);
var e2 = t.charCodeAt(x + 1);
var e3 = t.charCodeAt(x + 2);
var e4 = t.charCodeAt(x + 3);
c = x;
b = x + 1;
d = x + 2;
g = x + 3;
h = x + 4;
for (y = 0; y < n; y++) {
a = p[y];
if (a < c || b < c) {
c = (a > b ? b + 1 : a + 1);
}
else {
if (e1 !== u[y]) {
c++;
}
}
if (c < b || d < b) {
b = (c > d ? d + 1 : c + 1);
}
else {
if (e2 !== u[y]) {
b++;
}
}
if (b < d || g < d) {
d = (b > g ? g + 1 : b + 1);
}
else {
if (e3 !== u[y]) {
d++;
}
}
if (d < g || h < g) {
g = (d > h ? h + 1 : d + 1);
}
else {
if (e4 !== u[y]) {
g++;
}
}
p[y] = h = g;
g = d;
d = b;
b = c;
c = a;
}
}
for (; x < m;) {
var e = t.charCodeAt(x);
c = x;
d = ++x;
for (y = 0; y < n; y++) {
a = p[y];
if (a < c || d < c) {
d = (a > d ? d + 1 : a + 1);
}
else {
if (e !== u[y]) {
d = c + 1;
}
else {
d = c;
}
}
p[y] = d;
c = a;
}
h = d;
}
return h;
}