initial commit
This commit is contained in:
commit
0b0e08bcb0
49
index.html
Normal file
49
index.html
Normal 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
76
jst.js
Normal 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
129
main.js
Normal 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
223
style.css
Normal 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
13
templates/menu-item.html
Normal 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>
|
5
templates/search-entry.html
Normal file
5
templates/search-entry.html
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="search-entry">
|
||||
<span class="search-entry-name"></span>
|
||||
</div>
|
||||
</template>
|
96
utils.js
Normal file
96
utils.js
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user