mirror of
https://github.com/badlogic/heissepreise.git
synced 2024-06-30 04:16:05 +02:00
New index.html/index.js using the new framework. State serialization fixes.
This commit is contained in:
parent
846f9b5861
commit
75367c6852
|
@ -105,7 +105,7 @@ async function bundleJS(inputDir, outputDir, watch) {
|
||||||
entryPoints: {
|
entryPoints: {
|
||||||
carts: `${inputDir}/carts.js`,
|
carts: `${inputDir}/carts.js`,
|
||||||
"changes-new": `${inputDir}/changes-new.js`,
|
"changes-new": `${inputDir}/changes-new.js`,
|
||||||
"index-new": `${inputDir}/index-new.js`,
|
index: `${inputDir}/index.js`,
|
||||||
},
|
},
|
||||||
bundle: true,
|
bundle: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
%%_templates/_header.html%% %%_templates/_menu.html%%
|
|
||||||
|
|
||||||
<div class="w-full max-w-5-xl relative px-4 flex-1">
|
|
||||||
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">Produktsuche</h1>
|
|
||||||
<items-filter x-id="items-filter" emptyquery stores misc></items-filter>
|
|
||||||
<items-list chart></items-list>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="index-new.js"></script>
|
|
||||||
|
|
||||||
%%_templates/_footer.html%%
|
|
|
@ -1,10 +0,0 @@
|
||||||
const model = require("./model");
|
|
||||||
require("./views");
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
await model.load();
|
|
||||||
const itemsFilter = document.querySelector("items-filter");
|
|
||||||
const itemsList = document.querySelector("items-list");
|
|
||||||
itemsFilter.model = itemsList.model = model.items;
|
|
||||||
itemsFilter.filter();
|
|
||||||
})();
|
|
31
site/index-old.html
Normal file
31
site/index-old.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
%%_templates/_header.html%% %%_templates/_menu.html%%
|
||||||
|
|
||||||
|
<div class="w-full relative px-4 flex-1">
|
||||||
|
<div class="max-w-5xl text-center mx-auto">
|
||||||
|
<h1 class="text-2xl font-bold pb-2 pt-8">Produktsuche</h1>
|
||||||
|
<div>
|
||||||
|
<div id="chart" class="hidden bg-stone-200 rounded-xl p-4 max-w-4xl mx-auto md:mb-12 md:mt-6">
|
||||||
|
<canvas class="bg-white rounded-lg mb-2"></canvas>
|
||||||
|
<div class="flex items-center flex-wrap justify-center gap-2">
|
||||||
|
<div id="sum-container"></div>
|
||||||
|
<div id="sumstores-container"></div>
|
||||||
|
<div id="todayonly-container"></div>
|
||||||
|
<div
|
||||||
|
class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600"
|
||||||
|
>
|
||||||
|
<input id="start" type="date" />
|
||||||
|
-
|
||||||
|
<input id="end" type="date" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="search" class="w-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="js/alasql.js"></script>
|
||||||
|
<script src="js/chart.js"></script>
|
||||||
|
<script src="utils.js"></script>
|
||||||
|
<script src="index.js"></script>
|
||||||
|
|
||||||
|
%%_templates/_footer.html%%
|
109
site/index-old.js
Normal file
109
site/index-old.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
function updateCharts(canvasDom, items) {
|
||||||
|
const now = performance.now();
|
||||||
|
const sum = document.querySelector("#sum").checked;
|
||||||
|
const sumStores = document.querySelector("#sumstores").checked;
|
||||||
|
const todayOnly = document.querySelector("#todayonly").checked;
|
||||||
|
let startDate = document.querySelector("#start").value;
|
||||||
|
let endDate = document.querySelector("#end").value;
|
||||||
|
if (start > endDate) {
|
||||||
|
let tmp = start;
|
||||||
|
start = endDate;
|
||||||
|
endDate = tmp;
|
||||||
|
}
|
||||||
|
showCharts(canvasDom, items, sum, sumStores, todayOnly, startDate, endDate);
|
||||||
|
console.log("Updating charts took: " + (performance.now() - now) / 1000 + " seconds");
|
||||||
|
}
|
||||||
|
async function load() {
|
||||||
|
const items = await loadItems();
|
||||||
|
const chartDom = document.querySelector("#chart");
|
||||||
|
const canvasDom = chartDom.querySelector("canvas");
|
||||||
|
let lastHits = null;
|
||||||
|
|
||||||
|
document.querySelector("#sum-container").innerHTML = customCheckbox(`sum`, "Preissumme Gesamt", false, "gray");
|
||||||
|
document.querySelector("#sumstores-container").innerHTML = customCheckbox(`sumstores`, "Preissumme pro Kette", false, "gray");
|
||||||
|
document.querySelector("#todayonly-container").innerHTML = customCheckbox(`todayonly`, "Nur heutige Preise", false, "gray");
|
||||||
|
|
||||||
|
document.querySelector("#sum").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
||||||
|
document.querySelector("#sumstores").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
||||||
|
document.querySelector("#todayonly").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
||||||
|
|
||||||
|
document.querySelector("#start").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
||||||
|
document.querySelector("#end").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
||||||
|
document.querySelector("#start").value = getOldestDate(items);
|
||||||
|
document.querySelector("#end").value = currentDate();
|
||||||
|
|
||||||
|
let chartEnabled = false;
|
||||||
|
let search = newSearchComponent(
|
||||||
|
document.querySelector("#search"),
|
||||||
|
items,
|
||||||
|
(hits) => {
|
||||||
|
items.forEach((item) => (item.chart = false));
|
||||||
|
if (!chartEnabled) {
|
||||||
|
chartDom.classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
if (hits.length > 0) {
|
||||||
|
chartDom.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
chartDom.classList.add("hidden");
|
||||||
|
}
|
||||||
|
updateCharts(canvasDom, hits);
|
||||||
|
}
|
||||||
|
lastHits = hits;
|
||||||
|
return hits;
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
(header) => {
|
||||||
|
header.innerHTML += "<th></th>";
|
||||||
|
return header;
|
||||||
|
},
|
||||||
|
(item, itemDom, items, setQuery) => {
|
||||||
|
const checked = (item.chart = (getQueryParameter("c") ?? []).includes(`${item.store}:${item.id}`));
|
||||||
|
const dataId = item.store + ":" + item.id;
|
||||||
|
const cell = dom(
|
||||||
|
"td",
|
||||||
|
`<label class="flex">
|
||||||
|
<input class="hidden peer" type="checkbox" ${checked ? "checked" : ""} data-id="${dataId}">
|
||||||
|
<span class="ml-auto peer-checked:bg-blue-700 group-[.decreased]:scale-x-flip btn-action">📈</span>
|
||||||
|
</label>`
|
||||||
|
);
|
||||||
|
itemDom.appendChild(cell);
|
||||||
|
|
||||||
|
const handleClick = (eventShouldSetQuery = false) => {
|
||||||
|
item.chart = cell.children[0].children[0].checked;
|
||||||
|
if (item.chart && !search.chart.checked) {
|
||||||
|
search.chart.checked = true;
|
||||||
|
search.chart.dispatchEvent(new Event("change"));
|
||||||
|
}
|
||||||
|
updateCharts(canvasDom, lastHits);
|
||||||
|
!!eventShouldSetQuery && setQuery();
|
||||||
|
};
|
||||||
|
cell.addEventListener("click", handleClick);
|
||||||
|
checked && handleClick();
|
||||||
|
return itemDom;
|
||||||
|
},
|
||||||
|
(checked) => {
|
||||||
|
chartEnabled = checked;
|
||||||
|
if (checked) {
|
||||||
|
if (lastHits.length > 0) {
|
||||||
|
chartDom.classList.remove("hidden");
|
||||||
|
updateCharts(canvasDom, lastHits);
|
||||||
|
} else {
|
||||||
|
chartDom.classList.add("hidden");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chartDom.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const query = getQueryParameter("q");
|
||||||
|
if (query) {
|
||||||
|
document.querySelector(".search").value = query;
|
||||||
|
const inputEvent = new Event("input", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: false,
|
||||||
|
});
|
||||||
|
document.querySelector(".search").dispatchEvent(inputEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
|
@ -1,31 +1,11 @@
|
||||||
%%_templates/_header.html%% %%_templates/_menu.html%%
|
%%_templates/_header.html%% %%_templates/_menu.html%%
|
||||||
|
|
||||||
<div class="w-full relative px-4 flex-1">
|
<div class="w-full max-w-5-xl relative px-4 flex-1">
|
||||||
<div class="max-w-5xl text-center mx-auto">
|
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">Produktsuche</h1>
|
||||||
<h1 class="text-2xl font-bold pb-2 pt-8">Produktsuche</h1>
|
<items-filter x-id="items-filter" emptyquery stores misc></items-filter>
|
||||||
<div>
|
<items-list share json chart></items-list>
|
||||||
<div id="chart" class="hidden bg-stone-200 rounded-xl p-4 max-w-4xl mx-auto md:mb-12 md:mt-6">
|
|
||||||
<canvas class="bg-white rounded-lg mb-2"></canvas>
|
|
||||||
<div class="flex items-center flex-wrap justify-center gap-2">
|
|
||||||
<div id="sum-container"></div>
|
|
||||||
<div id="sumstores-container"></div>
|
|
||||||
<div id="todayonly-container"></div>
|
|
||||||
<div
|
|
||||||
class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600"
|
|
||||||
>
|
|
||||||
<input id="start" type="date" />
|
|
||||||
-
|
|
||||||
<input id="end" type="date" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="search" class="w-full"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<script src="js/alasql.js"></script>
|
|
||||||
<script src="js/chart.js"></script>
|
|
||||||
<script src="utils.js"></script>
|
|
||||||
<script src="index.js"></script>
|
<script src="index.js"></script>
|
||||||
|
|
||||||
%%_templates/_footer.html%%
|
%%_templates/_footer.html%%
|
||||||
|
|
131
site/index.js
131
site/index.js
|
@ -1,109 +1,30 @@
|
||||||
function updateCharts(canvasDom, items) {
|
const { getQueryParameter } = require("./misc");
|
||||||
const now = performance.now();
|
const model = require("./model");
|
||||||
const sum = document.querySelector("#sum").checked;
|
require("./views");
|
||||||
const sumStores = document.querySelector("#sumstores").checked;
|
|
||||||
const todayOnly = document.querySelector("#todayonly").checked;
|
|
||||||
let startDate = document.querySelector("#start").value;
|
|
||||||
let endDate = document.querySelector("#end").value;
|
|
||||||
if (start > endDate) {
|
|
||||||
let tmp = start;
|
|
||||||
start = endDate;
|
|
||||||
endDate = tmp;
|
|
||||||
}
|
|
||||||
showCharts(canvasDom, items, sum, sumStores, todayOnly, startDate, endDate);
|
|
||||||
console.log("Updating charts took: " + (performance.now() - now) / 1000 + " seconds");
|
|
||||||
}
|
|
||||||
async function load() {
|
|
||||||
const items = await loadItems();
|
|
||||||
const chartDom = document.querySelector("#chart");
|
|
||||||
const canvasDom = chartDom.querySelector("canvas");
|
|
||||||
let lastHits = null;
|
|
||||||
|
|
||||||
document.querySelector("#sum-container").innerHTML = customCheckbox(`sum`, "Preissumme Gesamt", false, "gray");
|
(async () => {
|
||||||
document.querySelector("#sumstores-container").innerHTML = customCheckbox(`sumstores`, "Preissumme pro Kette", false, "gray");
|
await model.load();
|
||||||
document.querySelector("#todayonly-container").innerHTML = customCheckbox(`todayonly`, "Nur heutige Preise", false, "gray");
|
const itemsFilter = document.querySelector("items-filter");
|
||||||
|
const itemsList = document.querySelector("items-list");
|
||||||
|
const itemsChart = document.querySelector("items-chart");
|
||||||
|
itemsFilter.model = itemsList.model = model.items;
|
||||||
|
|
||||||
document.querySelector("#sum").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
const stateToUrl = (event) => {
|
||||||
document.querySelector("#sumstores").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
const filterState = JSON.stringify(itemsFilter.state);
|
||||||
document.querySelector("#todayonly").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
const listState = JSON.stringify(itemsList.state);
|
||||||
|
const chartState = JSON.stringify(itemsChart.state);
|
||||||
document.querySelector("#start").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
history.pushState({}, null, location.pathname + "?f=" + filterState + "&l=" + listState + "&c=" + chartState);
|
||||||
document.querySelector("#end").addEventListener("change", () => updateCharts(canvasDom, lastHits));
|
|
||||||
document.querySelector("#start").value = getOldestDate(items);
|
|
||||||
document.querySelector("#end").value = currentDate();
|
|
||||||
|
|
||||||
let chartEnabled = false;
|
|
||||||
let search = newSearchComponent(
|
|
||||||
document.querySelector("#search"),
|
|
||||||
items,
|
|
||||||
(hits) => {
|
|
||||||
items.forEach((item) => (item.chart = false));
|
|
||||||
if (!chartEnabled) {
|
|
||||||
chartDom.classList.add("hidden");
|
|
||||||
} else {
|
|
||||||
if (hits.length > 0) {
|
|
||||||
chartDom.classList.remove("hidden");
|
|
||||||
} else {
|
|
||||||
chartDom.classList.add("hidden");
|
|
||||||
}
|
|
||||||
updateCharts(canvasDom, hits);
|
|
||||||
}
|
|
||||||
lastHits = hits;
|
|
||||||
return hits;
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
(header) => {
|
|
||||||
header.innerHTML += "<th></th>";
|
|
||||||
return header;
|
|
||||||
},
|
|
||||||
(item, itemDom, items, setQuery) => {
|
|
||||||
const checked = (item.chart = (getQueryParameter("c") ?? []).includes(`${item.store}:${item.id}`));
|
|
||||||
const dataId = item.store + ":" + item.id;
|
|
||||||
const cell = dom(
|
|
||||||
"td",
|
|
||||||
`<label class="flex">
|
|
||||||
<input class="hidden peer" type="checkbox" ${checked ? "checked" : ""} data-id="${dataId}">
|
|
||||||
<span class="ml-auto peer-checked:bg-blue-700 group-[.decreased]:scale-x-flip btn-action">📈</span>
|
|
||||||
</label>`
|
|
||||||
);
|
|
||||||
itemDom.appendChild(cell);
|
|
||||||
|
|
||||||
const handleClick = (eventShouldSetQuery = false) => {
|
|
||||||
item.chart = cell.children[0].children[0].checked;
|
|
||||||
if (item.chart && !search.chart.checked) {
|
|
||||||
search.chart.checked = true;
|
|
||||||
search.chart.dispatchEvent(new Event("change"));
|
|
||||||
}
|
|
||||||
updateCharts(canvasDom, lastHits);
|
|
||||||
!!eventShouldSetQuery && setQuery();
|
|
||||||
};
|
};
|
||||||
cell.addEventListener("click", handleClick);
|
|
||||||
checked && handleClick();
|
|
||||||
return itemDom;
|
|
||||||
},
|
|
||||||
(checked) => {
|
|
||||||
chartEnabled = checked;
|
|
||||||
if (checked) {
|
|
||||||
if (lastHits.length > 0) {
|
|
||||||
chartDom.classList.remove("hidden");
|
|
||||||
updateCharts(canvasDom, lastHits);
|
|
||||||
} else {
|
|
||||||
chartDom.classList.add("hidden");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
chartDom.classList.add("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const query = getQueryParameter("q");
|
|
||||||
if (query) {
|
|
||||||
document.querySelector(".search").value = query;
|
|
||||||
const inputEvent = new Event("input", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: false,
|
|
||||||
});
|
|
||||||
document.querySelector(".search").dispatchEvent(inputEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
load();
|
itemsFilter.addEventListener("x-change", stateToUrl);
|
||||||
|
itemsList.addEventListener("x-change", stateToUrl);
|
||||||
|
|
||||||
|
const f = getQueryParameter("f");
|
||||||
|
const l = getQueryParameter("l");
|
||||||
|
const c = getQueryParameter("c");
|
||||||
|
if (f) itemsFilter.state = JSON.parse(f);
|
||||||
|
if (l) itemsList.state = JSON.parse(l);
|
||||||
|
if (c) itemsChart.state = JSON.parse(c);
|
||||||
|
|
||||||
|
itemsFilter.filter();
|
||||||
|
})();
|
||||||
|
|
21
site/misc.js
21
site/misc.js
|
@ -38,8 +38,6 @@ if (typeof window !== "undefined") {
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
let lastChangeTimestamp = null;
|
let lastChangeTimestamp = null;
|
||||||
let socket = io({ transports: ["websocket"] });
|
let socket = io({ transports: ["websocket"] });
|
||||||
socket.on("connect", () => console.log("Connected"));
|
|
||||||
socket.on("disconnect", () => console.log("Disconnected"));
|
|
||||||
socket.on("message", (timestamp) => {
|
socket.on("message", (timestamp) => {
|
||||||
if (lastChangeTimestamp != timestamp) {
|
if (lastChangeTimestamp != timestamp) {
|
||||||
setTimeout(() => location.reload(), 100);
|
setTimeout(() => location.reload(), 100);
|
||||||
|
@ -89,6 +87,12 @@ exports.dom = (element, innerHTML) => {
|
||||||
return el;
|
return el;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.getQueryParameter = (name) => {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const params = url.searchParams.getAll(name);
|
||||||
|
return params.length > 1 ? params : params?.[0];
|
||||||
|
};
|
||||||
|
|
||||||
exports.getBooleanAttribute = (element, name) => {
|
exports.getBooleanAttribute = (element, name) => {
|
||||||
return element.hasAttribute(name) && (element.getAttribute(name).length == 0 || element.getAttribute(name) === "true");
|
return element.hasAttribute(name) && (element.getAttribute(name).length == 0 || element.getAttribute(name) === "true");
|
||||||
};
|
};
|
||||||
|
@ -220,3 +224,16 @@ exports.onVisibleOnce = (target, callback) => {
|
||||||
});
|
});
|
||||||
observer.observe(target);
|
observer.observe(target);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.log = (message) => {
|
||||||
|
const now = new Date();
|
||||||
|
const hours = String(now.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||||||
|
|
||||||
|
console.log(`${hours}:${minutes}:${seconds}: ${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.deltaTime = (start) => {
|
||||||
|
return (performance.now() - start) / 1000;
|
||||||
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const { deltaTime, log } = require("../misc");
|
||||||
const { stores, STORE_KEYS } = require("./stores");
|
const { stores, STORE_KEYS } = require("./stores");
|
||||||
const { Model } = require("./model");
|
const { Model } = require("./model");
|
||||||
|
|
||||||
|
@ -69,32 +70,29 @@ class Items extends Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
let now = performance.now();
|
let start = performance.now();
|
||||||
const compressedItemsPerStore = [];
|
const compressedItemsPerStore = [];
|
||||||
for (const store of STORE_KEYS) {
|
for (const store of STORE_KEYS) {
|
||||||
compressedItemsPerStore.push(
|
compressedItemsPerStore.push(
|
||||||
new Promise(async (resolve) => {
|
new Promise(async (resolve) => {
|
||||||
const now = performance.now();
|
const start = performance.now();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`data/latest-canonical.${store}.compressed.json`);
|
const response = await fetch(`data/latest-canonical.${store}.compressed.json`);
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
console.log(`Loading compressed items for ${store} took ${(performance.now() - now) / 1000} secs`);
|
log(`Items - loading compressed items for ${store} took ${deltaTime(start)} secs`);
|
||||||
resolve(decompress(json));
|
resolve(decompress(json));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
log(`Items - error while loading compressed items for ${store} ${e.message}`);
|
||||||
console.log(
|
|
||||||
`Error while loading compressed items for ${store}. It took ${(performance.now() - now) / 1000} secs, continueing...`
|
|
||||||
);
|
|
||||||
resolve([]);
|
resolve([]);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const items = [].concat(...(await Promise.all(compressedItemsPerStore)));
|
const items = [].concat(...(await Promise.all(compressedItemsPerStore)));
|
||||||
console.log("Loading compressed items in parallel took " + (performance.now() - now) / 1000 + " secs");
|
log(`Items - loaded ${items.length} items took ${deltaTime(start).toFixed(4)} secs`);
|
||||||
|
|
||||||
const lookup = {};
|
const lookup = {};
|
||||||
now = performance.now();
|
start = performance.now();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
lookup[item.store + item.id] = item;
|
lookup[item.store + item.id] = item;
|
||||||
item.search = item.name + " " + item.quantity + " " + item.unit;
|
item.search = item.name + " " + item.quantity + " " + item.unit;
|
||||||
|
@ -137,7 +135,7 @@ class Items extends Model {
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Processing items took " + (performance.now() - now) / 1000 + " secs");
|
log(`Items - processing ${items.length} items took ${deltaTime(start).toFixed(4)} secs`);
|
||||||
|
|
||||||
this._items = items;
|
this._items = items;
|
||||||
this._lookup = lookup;
|
this._lookup = lookup;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { STORE_KEYS } = require("../model/stores");
|
const { STORE_KEYS } = require("../model/stores");
|
||||||
const { today } = require("../misc");
|
const { today, log, deltaTime } = require("../misc");
|
||||||
const { View } = require("./view");
|
const { View } = require("./view");
|
||||||
require("./custom-checkbox");
|
require("./custom-checkbox");
|
||||||
const { Chart, registerables } = require("chart.js");
|
const { Chart, registerables } = require("chart.js");
|
||||||
|
@ -13,20 +13,20 @@ class ItemsChart extends View {
|
||||||
<div class="bg-stone-200 p-4 mx-auto">
|
<div class="bg-stone-200 p-4 mx-auto">
|
||||||
<canvas x-id="canvas" class="bg-white rounded-lg py-4"></canvas>
|
<canvas x-id="canvas" class="bg-white rounded-lg py-4"></canvas>
|
||||||
<div class="filters flex items-center flex-wrap justify-center gap-2 pt-2">
|
<div class="filters flex items-center flex-wrap justify-center gap-2 pt-2">
|
||||||
<custom-checkbox x-id="sumTotal" x-change label="Preissumme Gesamt"></custom-checkbox>
|
<custom-checkbox x-id="sumTotal" x-change x-state label="Preissumme Gesamt"></custom-checkbox>
|
||||||
<custom-checkbox x-id="sumStores" x-change label="Preissumme Ketten"></custom-checkbox>
|
<custom-checkbox x-id="sumStores" x-change x-state label="Preissumme Ketten"></custom-checkbox>
|
||||||
<custom-checkbox x-id="onlyToday" x-change label="Nur heutige Preise"></custom-checkbox>
|
<custom-checkbox x-id="onlyToday" x-change x-state label="Nur heutige Preise"></custom-checkbox>
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
||||||
<input x-id="startDate" x-change type="date" value="2020-01-01" />
|
<input x-id="startDate" x-change x-state type="date" value="2020-01-01" />
|
||||||
-
|
-
|
||||||
<input x-id="endDate" x-change type="date" value="${today()}"/>
|
<input x-id="endDate" x-change x-state type="date" value="${today()}"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.setupEventHandlers();
|
this.setupEventHandlers();
|
||||||
this.addEventListener("change", () => {
|
this.addEventListener("x-change", () => {
|
||||||
this.render();
|
this.render();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -140,6 +140,7 @@ class ItemsChart extends View {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.model) return;
|
if (!this.model) return;
|
||||||
|
const start = performance.now();
|
||||||
const items = this.model.filteredItems;
|
const items = this.model.filteredItems;
|
||||||
const elements = this.elements;
|
const elements = this.elements;
|
||||||
const onlyToday = this.elements.onlyToday.checked;
|
const onlyToday = this.elements.onlyToday.checked;
|
||||||
|
@ -177,9 +178,8 @@ class ItemsChart extends View {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Items to show " + itemsToShow.length);
|
|
||||||
|
|
||||||
this.renderChart(itemsToShow, onlyToday ? "bar" : "line");
|
this.renderChart(itemsToShow, onlyToday ? "bar" : "line");
|
||||||
|
log(`ItemsChart - charted ${itemsToShow.length} items in ${deltaTime(start).toFixed(2)} secs`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define("items-chart", ItemsChart);
|
customElements.define("items-chart", ItemsChart);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { today, parseNumber, dom, getBooleanAttribute, queryItems } = require("../misc");
|
const { today, parseNumber, dom, getBooleanAttribute, queryItems, log, deltaTime } = require("../misc");
|
||||||
const { stores, STORE_KEYS, BUDGET_BRANDS } = require("../model/stores");
|
const { stores, STORE_KEYS, BUDGET_BRANDS } = require("../model/stores");
|
||||||
const { View } = require("./view");
|
const { View } = require("./view");
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class ItemsFilter extends View {
|
||||||
const placeholder = this.hasAttribute("placeholder") ? this.getAttribute("placeholder") : "Produkte suchen...";
|
const placeholder = this.hasAttribute("placeholder") ? this.getAttribute("placeholder") : "Produkte suchen...";
|
||||||
|
|
||||||
this.innerHTML = /*html*/ `
|
this.innerHTML = /*html*/ `
|
||||||
<input x-id="query" x-state x-input class="rounded-lg px-2 py-1 w-full" type="text" placeholder="${placeholder}" />
|
<input x-id="query" x-state x-input-debounce class="rounded-lg px-2 py-1 w-full" type="text" placeholder="${placeholder}" />
|
||||||
|
|
||||||
<div x-id="stores" class="flex justify-center gap-2 flex-wrap mt-4 ${hideStores}">
|
<div x-id="stores" class="flex justify-center gap-2 flex-wrap mt-4 ${hideStores}">
|
||||||
<custom-checkbox x-id="allStores" label="Alle" checked></custom-checkbox>
|
<custom-checkbox x-id="allStores" label="Alle" checked></custom-checkbox>
|
||||||
|
@ -70,7 +70,7 @@ class ItemsFilter extends View {
|
||||||
|
|
||||||
const elements = this.elements;
|
const elements = this.elements;
|
||||||
|
|
||||||
elements.query.addEventListener("input", (event) => {
|
elements.query.addEventListener("input", () => {
|
||||||
let query = elements.query.value.trim();
|
let query = elements.query.value.trim();
|
||||||
if (query.length > 0 && query.charAt(0) == "!") {
|
if (query.length > 0 && query.charAt(0) == "!") {
|
||||||
elements.stores.classList.add("hidden");
|
elements.stores.classList.add("hidden");
|
||||||
|
@ -81,22 +81,19 @@ class ItemsFilter extends View {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.allStores.addEventListener("change", (event) => {
|
elements.allStores.addEventListener("change", () => {
|
||||||
event.stopPropagation();
|
|
||||||
const checked = elements.allStores.checked;
|
const checked = elements.allStores.checked;
|
||||||
STORE_KEYS.forEach((store) => (elements[store].checked = checked));
|
STORE_KEYS.forEach((store) => (elements[store].checked = checked));
|
||||||
this.fireChangeEvent();
|
this.fireChangeEvent();
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.priceChangesToday.addEventListener("change", (event) => {
|
elements.priceChangesToday.addEventListener("change", () => {
|
||||||
event.stopPropagation();
|
|
||||||
if (elements.priceChangesToday.checked) elements.priceDirection.classList.remove("hidden");
|
if (elements.priceChangesToday.checked) elements.priceDirection.classList.remove("hidden");
|
||||||
else elements.priceDirection.classList.add("hidden");
|
else elements.priceDirection.classList.add("hidden");
|
||||||
this.fireChangeEvent();
|
this.fireChangeEvent();
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.priceChangesSinceLast.addEventListener("change", (event) => {
|
elements.priceChangesSinceLast.addEventListener("change", () => {
|
||||||
event.stopPropagation();
|
|
||||||
if (elements.priceChangesSinceLast.checked) elements.priceDirection.classList.add("hidden");
|
if (elements.priceChangesSinceLast.checked) elements.priceDirection.classList.add("hidden");
|
||||||
else elements.priceDirection.classList.remove("hidden");
|
else elements.priceDirection.classList.remove("hidden");
|
||||||
this.fireChangeEvent();
|
this.fireChangeEvent();
|
||||||
|
@ -104,7 +101,7 @@ class ItemsFilter extends View {
|
||||||
|
|
||||||
this.setupEventHandlers();
|
this.setupEventHandlers();
|
||||||
|
|
||||||
this.addEventListener("x-change", (event) => {
|
this.addEventListener("x-change", () => {
|
||||||
this.filter();
|
this.filter();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -112,7 +109,7 @@ class ItemsFilter extends View {
|
||||||
filter() {
|
filter() {
|
||||||
if (!this.model) return;
|
if (!this.model) return;
|
||||||
|
|
||||||
const now = performance.now();
|
const start = performance.now();
|
||||||
const elements = this.elements;
|
const elements = this.elements;
|
||||||
this.model.totalItems = this.model.items.length;
|
this.model.totalItems = this.model.items.length;
|
||||||
let filteredItems = [...this.model.items];
|
let filteredItems = [...this.model.items];
|
||||||
|
@ -193,7 +190,7 @@ class ItemsFilter extends View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.length > 0) {
|
if (query.length > 0) {
|
||||||
filteredItems = queryItems(query, filteredItems);
|
filteredItems = queryItems(query, filteredItems, elements.exact.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._lastQuery != query) {
|
if (this._lastQuery != query) {
|
||||||
|
@ -201,7 +198,7 @@ class ItemsFilter extends View {
|
||||||
}
|
}
|
||||||
this._lastQuery = query;
|
this._lastQuery = query;
|
||||||
|
|
||||||
console.log("Filtering items took " + (performance.now() - now) / 1000 + " secs");
|
log(`ItemsFilter - Filtering ${this.model.items.length} took ${deltaTime(start).toFixed(4)} secs`);
|
||||||
|
|
||||||
this.model.removeListener(this._listener);
|
this.model.removeListener(this._listener);
|
||||||
this.model.filteredItems = filteredItems;
|
this.model.filteredItems = filteredItems;
|
||||||
|
@ -209,7 +206,7 @@ class ItemsFilter extends View {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const now = performance.now();
|
const start = performance.now();
|
||||||
const elements = this.elements;
|
const elements = this.elements;
|
||||||
const items = this.model.items;
|
const items = this.model.items;
|
||||||
const dates = {};
|
const dates = {};
|
||||||
|
@ -236,7 +233,7 @@ class ItemsFilter extends View {
|
||||||
priceChangesDates.append(dateDom);
|
priceChangesDates.append(dateDom);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Rendering items filter took " + (performance.now() - now) / 1000);
|
log(`ItemsFilter - rendering items filter took ${deltaTime(start)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
get checkedStores() {
|
get checkedStores() {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { downloadJSON, dom, onVisibleOnce, isMobile, getBooleanAttribute } = require("../misc");
|
const { downloadJSON, dom, onVisibleOnce, isMobile, getBooleanAttribute, deltaTime, log } = require("../misc");
|
||||||
const { vectorizeItems, similaritySortItems } = require("../knn");
|
const { vectorizeItems, similaritySortItems } = require("../knn");
|
||||||
const { stores } = require("../model/stores");
|
const { stores } = require("../model/stores");
|
||||||
const { View } = require("./view");
|
const { View } = require("./view");
|
||||||
|
@ -8,7 +8,6 @@ class ItemsList extends View {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._share = getBooleanAttribute(this, "share");
|
|
||||||
this._json = getBooleanAttribute(this, "json");
|
this._json = getBooleanAttribute(this, "json");
|
||||||
this._chart = getBooleanAttribute(this, "chart");
|
this._chart = getBooleanAttribute(this, "chart");
|
||||||
this._remove = getBooleanAttribute(this, "remove");
|
this._remove = getBooleanAttribute(this, "remove");
|
||||||
|
@ -21,15 +20,14 @@ class ItemsList extends View {
|
||||||
<div class="flex flex-col md:flex-row gap-2 items-center">
|
<div class="flex flex-col md:flex-row gap-2 items-center">
|
||||||
<span x-id="numItems"></span>
|
<span x-id="numItems"></span>
|
||||||
<span>
|
<span>
|
||||||
<a x-id="shareLink" class="hidden querylink text-primary font-medium hover:underline">Teilen</a>
|
|
||||||
<a x-id="json" class="hidden text-primary font-medium hover:underline" href="">JSON</a>
|
<a x-id="json" class="hidden text-primary font-medium hover:underline" href="">JSON</a>
|
||||||
</span>
|
</span>
|
||||||
<custom-checkbox x-id="enableChart" label="Diagramm" class="${this._chart}"></custom-checkbox>
|
<custom-checkbox x-id="enableChart" x-change x-state label="Diagramm" class="${this._chart}"></custom-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
Sortieren
|
Sortieren
|
||||||
<select x-id="sort" x-change>
|
<select x-id="sort" x-change x-state>
|
||||||
<option value="price-asc">Preis aufsteigend</option>
|
<option value="price-asc">Preis aufsteigend</option>
|
||||||
<option value="price-desc">Preis absteigend</option>
|
<option value="price-desc">Preis absteigend</option>
|
||||||
<option value="quantity-asc">Menge aufsteigend</option>
|
<option value="quantity-asc">Menge aufsteigend</option>
|
||||||
|
@ -71,10 +69,6 @@ class ItemsList extends View {
|
||||||
else elements.chart.classList.add("hidden");
|
else elements.chart.classList.add("hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.chart.addEventListener("change", (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache in a field, so we don't have to call this.elements in each renderItem() call.
|
// Cache in a field, so we don't have to call this.elements in each renderItem() call.
|
||||||
this._showAllPriceHistories = false;
|
this._showAllPriceHistories = false;
|
||||||
elements.expandPriceHistories.addEventListener("click", () => {
|
elements.expandPriceHistories.addEventListener("click", () => {
|
||||||
|
@ -83,7 +77,9 @@ class ItemsList extends View {
|
||||||
elements.tableBody.querySelectorAll(".priceinfo").forEach((el) => (showAll ? el.classList.remove("hidden") : el.classList.add("hidden")));
|
elements.tableBody.querySelectorAll(".priceinfo").forEach((el) => (showAll ? el.classList.remove("hidden") : el.classList.add("hidden")));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addEventListener("change", () => {
|
this.setupEventHandlers();
|
||||||
|
|
||||||
|
this.addEventListener("x-change", (event) => {
|
||||||
this.render();
|
this.render();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -270,7 +266,6 @@ class ItemsList extends View {
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
});
|
});
|
||||||
elements.chartCheckbox.addEventListener("change", (event) => {
|
elements.chartCheckbox.addEventListener("change", (event) => {
|
||||||
event.stopPropagation();
|
|
||||||
item.chart = elements.chartCheckbox.checked;
|
item.chart = elements.chartCheckbox.checked;
|
||||||
this.elements.chart.render();
|
this.elements.chart.render();
|
||||||
});
|
});
|
||||||
|
@ -280,12 +275,13 @@ class ItemsList extends View {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const start = performance.now();
|
||||||
const elements = this.elements;
|
const elements = this.elements;
|
||||||
if (this.model.filteredItems.length != 0 && this.model.filteredItems.length <= (isMobile() ? 200 : 1000)) {
|
if (this.model.filteredItems.length != 0 && this.model.filteredItems.length <= (isMobile() ? 200 : 1000)) {
|
||||||
elements.nameSimilarity.removeAttribute("disabled");
|
elements.nameSimilarity.removeAttribute("disabled");
|
||||||
} else {
|
} else {
|
||||||
elements.nameSimilarity.setAttribute("disabled", "true");
|
elements.nameSimilarity.setAttribute("disabled", "true");
|
||||||
if (elements.sort.value == "name-similarity") elements.sort.value = "price-asc";
|
if (this.model.filteredItems.length != 0 && elements.sort.value == "name-similarity") elements.sort.value = "price-asc";
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = this.sort([...this.model.filteredItems]);
|
const items = this.sort([...this.model.filteredItems]);
|
||||||
|
@ -303,7 +299,6 @@ class ItemsList extends View {
|
||||||
const tableBody = elements.tableBody;
|
const tableBody = elements.tableBody;
|
||||||
tableBody.innerHTML = "";
|
tableBody.innerHTML = "";
|
||||||
|
|
||||||
const now = performance.now();
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const batches = [];
|
const batches = [];
|
||||||
let batch = [];
|
let batch = [];
|
||||||
|
@ -329,7 +324,7 @@ class ItemsList extends View {
|
||||||
|
|
||||||
renderBatch();
|
renderBatch();
|
||||||
|
|
||||||
console.log("Rendering items list took " + ((performance.now() - now) / 1000).toFixed(4) + " secs");
|
log(`ItemsList - rendering ${items.length} items took ${deltaTime(start).toFixed(4)} secs`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { getBooleanAttribute } = require("../misc");
|
const { getBooleanAttribute, log } = require("../misc");
|
||||||
|
|
||||||
class View extends HTMLElement {
|
class View extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -63,41 +63,51 @@ class View extends HTMLElement {
|
||||||
return this._model;
|
return this._model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getStateProperty(element) {
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
if (element.type === "checkbox" || element.type === "radio") {
|
||||||
|
return "checked";
|
||||||
|
} else {
|
||||||
|
return "value";
|
||||||
|
}
|
||||||
|
} else if (element instanceof HTMLOptionElement) {
|
||||||
|
return "selected";
|
||||||
|
} else if (element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
||||||
|
return "value";
|
||||||
|
} else if (element.localName === "custom-checkbox") {
|
||||||
|
return "checked";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get state() {
|
get state() {
|
||||||
const elements = this.elements;
|
const elements = this.elements;
|
||||||
const properties = ["checked", "value"];
|
|
||||||
const state = {};
|
const state = {};
|
||||||
for (const key of Object.keys(elements)) {
|
for (const key of Object.keys(elements)) {
|
||||||
const element = elements[key];
|
const element = elements[key];
|
||||||
if (!element.hasAttribute("x-state")) continue;
|
if (!element.hasAttribute("x-state")) continue;
|
||||||
const elementState = {};
|
const property = View.getStateProperty(element);
|
||||||
for (const property of properties) {
|
if (property == null) {
|
||||||
|
log(`View.state() - Unknown state property for element ${element.getAttribute("x-id")} in ${this.localName}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (property in element) {
|
if (property in element) {
|
||||||
elementState[property] = element[property];
|
state[key] = element[property];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state[key] = elementState;
|
|
||||||
}
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
set state(state) {
|
set state(state) {
|
||||||
const elements = this.elements;
|
const elements = this.elements;
|
||||||
this._disableChangeEvent = true;
|
this._disableChangeEvent = true;
|
||||||
for (const key of Object.keys(elements)) {
|
for (const key of Object.keys(state)) {
|
||||||
const elementState = state[key];
|
const elementState = state[key];
|
||||||
if (elementState) {
|
|
||||||
const element = elements[key];
|
const element = elements[key];
|
||||||
for (const property in elementState) {
|
if (element) {
|
||||||
element[property] = elementState[property];
|
const property = View.getStateProperty(element);
|
||||||
if (element.localName === "input" && element.getAttribute("type") === "radio") {
|
element[property] = elementState;
|
||||||
const changeEvent = new CustomEvent("x-change", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
element.dispatchEvent(changeEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._disableChangeEvent = false;
|
this._disableChangeEvent = false;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user