Increased maxWidth to 150 in prettier config, formatted all the things. See #52.
This commit is contained in:
parent
1845760384
commit
c6bbd0e03b
|
@ -1,6 +1,8 @@
|
||||||
data/
|
data/
|
||||||
.github/
|
.github/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
alasql.js
|
||||||
|
charts.js
|
||||||
latest-canonical*.json
|
latest-canonical*.json
|
||||||
momentum-cart.json
|
momentum-cart.json
|
||||||
package-lock.json
|
package-lock.json
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"printWidth": 100
|
"printWidth": 150
|
||||||
}
|
}
|
||||||
|
|
36
README.md
36
README.md
|
@ -4,13 +4,13 @@ A terrible grocery price search "app". Fetches data from big Austrian grocery ch
|
||||||
|
|
||||||
You can also get the [raw data](https://heisse-preise.io/api/index). The raw data is returned as a JSON array of items. An item has the following fields:
|
You can also get the [raw data](https://heisse-preise.io/api/index). The raw data is returned as a JSON array of items. An item has the following fields:
|
||||||
|
|
||||||
* `store`: (`billa`, `spar`, `hofer`, `dm`, `lidl`, `mpreis`)
|
- `store`: (`billa`, `spar`, `hofer`, `dm`, `lidl`, `mpreis`)
|
||||||
* `name`: the product name.
|
- `name`: the product name.
|
||||||
* `price`: the current price in €.
|
- `price`: the current price in €.
|
||||||
* `priceHistory`: an array of `{ date: "yyyy-mm-dd", price: number }` objects, sorted in descending order of date.
|
- `priceHistory`: an array of `{ date: "yyyy-mm-dd", price: number }` objects, sorted in descending order of date.
|
||||||
* `unit`: unit the product is sold at. May be undefined.
|
- `unit`: unit the product is sold at. May be undefined.
|
||||||
* `quantity`: quantity the product is sold at for the given price
|
- `quantity`: quantity the product is sold at for the given price
|
||||||
* `bio`: whether this product is classified as organic/"Bio"
|
- `bio`: whether this product is classified as organic/"Bio"
|
||||||
|
|
||||||
The project consists of a trivial NodeJS Express server responsible for fetching the product data, massaging it, and serving it to the front end (see `index.js`). The front end is a least-effort vanilla HTML/JS search form (see sources in `site/`).
|
The project consists of a trivial NodeJS Express server responsible for fetching the product data, massaging it, and serving it to the front end (see `index.js`). The front end is a least-effort vanilla HTML/JS search form (see sources in `site/`).
|
||||||
|
|
||||||
|
@ -51,14 +51,14 @@ Create a GitHub account and pick a username. Below, we assume your user name is
|
||||||
1. Log in to your GitHub account.
|
1. Log in to your GitHub account.
|
||||||
2. [Fork](https://github.com/badlogic/heissepreise/fork) this repository and name the repository `hotprices123.github.io`.
|
2. [Fork](https://github.com/badlogic/heissepreise/fork) this repository and name the repository `hotprices123.github.io`.
|
||||||
3. **In your forked repository**:
|
3. **In your forked repository**:
|
||||||
1. go to `Settings > Pages`, then under `Branch` select the `main` branch, and the `docs/` directory as shown in this screenshot.
|
1. go to `Settings > Pages`, then under `Branch` select the `main` branch, and the `docs/` directory as shown in this screenshot.
|
||||||
![docs/github-pages.png](docs/github-pages.png)
|
![docs/github-pages.png](docs/github-pages.png)
|
||||||
2. go to `Settings > Actions > General`, then under `Workflow permissions`, select `Read and write permissions` as shown in this screenshot.
|
2. go to `Settings > Actions > General`, then under `Workflow permissions`, select `Read and write permissions` as shown in this screenshot.
|
||||||
![docs/github-permissions.png](docs/github-permissions.png)
|
![docs/github-permissions.png](docs/github-permissions.png)
|
||||||
3. go to the `Actions` tab, then select the `Pages Update` workflow in the list to the left, then click `Enable workflow`. Confirm that you know what you are doing.
|
3. go to the `Actions` tab, then select the `Pages Update` workflow in the list to the left, then click `Enable workflow`. Confirm that you know what you are doing.
|
||||||
![docs/github-workflow.png](docs/github-workflow.png)
|
![docs/github-workflow.png](docs/github-workflow.png)
|
||||||
4. Trigger the workflow once manually to build the initial site and data.
|
4. Trigger the workflow once manually to build the initial site and data.
|
||||||
![docs/github-workflow2.png](docs/github-workflow2.png)
|
![docs/github-workflow2.png](docs/github-workflow2.png)
|
||||||
5. Once the workflow has finished, go to `https:/hotprices123.github.io` and enjoy your price comparisons.
|
5. Once the workflow has finished, go to `https:/hotprices123.github.io` and enjoy your price comparisons.
|
||||||
|
|
||||||
The data will be automatically fetched once a day at 8am (no idea what timezone), and the site will be updated.
|
The data will be automatically fetched once a day at 8am (no idea what timezone), and the site will be updated.
|
||||||
|
@ -67,17 +67,17 @@ To get the latest code changes from this repository into your fork:
|
||||||
|
|
||||||
1. Go to `https://github.com/hotprices123/hotprices123.github.io/compare/main...badlogic:heissepreise:main`
|
1. Go to `https://github.com/hotprices123/hotprices123.github.io/compare/main...badlogic:heissepreise:main`
|
||||||
2. Click on `Create pull request`
|
2. Click on `Create pull request`
|
||||||
![docs/github-pullrequest.png](docs/github-pullrequest.png)
|
![docs/github-pullrequest.png](docs/github-pullrequest.png)
|
||||||
3. Enter a Title like "Updated from upstream", then click `Create pull request``
|
3. Enter a Title like "Updated from upstream", then click `Create pull request``
|
||||||
![docs/github-pullrequest2.png](docs/github-pullrequest2.png)
|
![docs/github-pullrequest2.png](docs/github-pullrequest2.png)
|
||||||
4. Click `Merge pull request`
|
4. Click `Merge pull request`
|
||||||
![docs/github-pullrequest3.png](docs/github-pullrequest3.png)
|
![docs/github-pullrequest3.png](docs/github-pullrequest3.png)
|
||||||
|
|
||||||
Your site will now use the latest source code changes from this repository. It will be automatically updated and is usually live under `https://hotprices123.github.io` within 10-15 minutes.
|
Your site will now use the latest source code changes from this repository. It will be automatically updated and is usually live under `https://hotprices123.github.io` within 10-15 minutes.
|
||||||
|
|
||||||
## Generating a self-contained executable
|
## Generating a self-contained executable
|
||||||
|
|
||||||
Run the `package.sh`script in a Bash shell. It will generate a folder `dist/` with executable for Windows, Linux, and MacOS. Run the executable for your OS.
|
Run the `package.sh`script in a Bash shell. It will generate a folder `dist/` with executable for Windows, Linux, and MacOS. Run the executable for your OS.
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
|
71
analysis.js
71
analysis.js
|
@ -8,8 +8,8 @@ exports.STORE_KEYS = STORE_KEYS;
|
||||||
function currentDate() {
|
function currentDate() {
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
|
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||||
const day = String(currentDate.getDate()).padStart(2, '0');
|
const day = String(currentDate.getDate()).padStart(2, "0");
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ function getCanonicalFor(store, rawItems, today) {
|
||||||
if (item)
|
if (item)
|
||||||
canonicalItems.push({
|
canonicalItems.push({
|
||||||
store,
|
store,
|
||||||
...item
|
...item,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return canonicalItems;
|
return canonicalItems;
|
||||||
|
@ -37,7 +37,7 @@ function getCanonicalFor(store, rawItems, today) {
|
||||||
function mergePriceHistory(oldItems, items) {
|
function mergePriceHistory(oldItems, items) {
|
||||||
if (oldItems == null) return items;
|
if (oldItems == null) return items;
|
||||||
|
|
||||||
const lookup = {}
|
const lookup = {};
|
||||||
for (oldItem of oldItems) {
|
for (oldItem of oldItems) {
|
||||||
lookup[oldItem.store + oldItem.id] = oldItem;
|
lookup[oldItem.store + oldItem.id] = oldItem;
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ function mergePriceHistory(oldItems, items) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`${Object.keys(lookup).length} not in latest list.`)
|
console.log(`${Object.keys(lookup).length} not in latest list.`);
|
||||||
for (key of Object.keys(lookup)) {
|
for (key of Object.keys(lookup)) {
|
||||||
items.push(lookup[key]);
|
items.push(lookup[key]);
|
||||||
}
|
}
|
||||||
|
@ -92,8 +92,8 @@ function compress(items) {
|
||||||
const compressed = {
|
const compressed = {
|
||||||
stores: STORE_KEYS,
|
stores: STORE_KEYS,
|
||||||
n: items.length,
|
n: items.length,
|
||||||
data: []
|
data: [],
|
||||||
}
|
};
|
||||||
const data = compressed.data;
|
const data = compressed.data;
|
||||||
for (item of items) {
|
for (item of items) {
|
||||||
data.push(STORE_KEYS.indexOf(item.store));
|
data.push(STORE_KEYS.indexOf(item.store));
|
||||||
|
@ -148,10 +148,9 @@ exports.compress = compress;
|
||||||
exports.replay = function (rawDataDir) {
|
exports.replay = function (rawDataDir) {
|
||||||
const today = currentDate();
|
const today = currentDate();
|
||||||
|
|
||||||
const files = fs.readdirSync(rawDataDir).filter(
|
const files = fs
|
||||||
file => file.indexOf("canonical") == -1 &&
|
.readdirSync(rawDataDir)
|
||||||
STORE_KEYS.some(store => file.indexOf(`${store}-`) == 0)
|
.filter((file) => file.indexOf("canonical") == -1 && STORE_KEYS.some((store) => file.indexOf(`${store}-`) == 0));
|
||||||
);
|
|
||||||
|
|
||||||
const dateSort = (a, b) => {
|
const dateSort = (a, b) => {
|
||||||
const dateA = new Date(a.match(/\d{4}-\d{2}-\d{2}/)[0]);
|
const dateA = new Date(a.match(/\d{4}-\d{2}-\d{2}/)[0]);
|
||||||
|
@ -159,25 +158,29 @@ exports.replay = function (rawDataDir) {
|
||||||
return dateA - dateB;
|
return dateA - dateB;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFilteredFilesFor = (store) => files.filter(file => file.indexOf(`${store}-`) == 0).sort(dateSort).map(file => rawDataDir + "/" + file);
|
const getFilteredFilesFor = (store) =>
|
||||||
|
files
|
||||||
|
.filter((file) => file.indexOf(`${store}-`) == 0)
|
||||||
|
.sort(dateSort)
|
||||||
|
.map((file) => rawDataDir + "/" + file);
|
||||||
|
|
||||||
const storeFiles = {};
|
const storeFiles = {};
|
||||||
const canonicalFiles = {};
|
const canonicalFiles = {};
|
||||||
|
|
||||||
for (const store of STORE_KEYS) {
|
for (const store of STORE_KEYS) {
|
||||||
storeFiles[store] = getFilteredFilesFor(store);
|
storeFiles[store] = getFilteredFilesFor(store);
|
||||||
canonicalFiles[store] = storeFiles[store].map(file => getCanonicalFor(store, readJSON(file), file.match(/\d{4}-\d{2}-\d{2}/)[0]));
|
canonicalFiles[store] = storeFiles[store].map((file) => getCanonicalFor(store, readJSON(file), file.match(/\d{4}-\d{2}-\d{2}/)[0]));
|
||||||
canonicalFiles[store].reverse();
|
canonicalFiles[store].reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
const allFilesCanonical = [];
|
const allFilesCanonical = [];
|
||||||
const len = Math.max(...Object.values(canonicalFiles).map(filesByStore => filesByStore.length));
|
const len = Math.max(...Object.values(canonicalFiles).map((filesByStore) => filesByStore.length));
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const canonical = [];
|
const canonical = [];
|
||||||
Object.values(canonicalFiles).forEach(filesByStore => {
|
Object.values(canonicalFiles).forEach((filesByStore) => {
|
||||||
const file = filesByStore.pop();
|
const file = filesByStore.pop();
|
||||||
if (file) canonical.push(...file);
|
if (file) canonical.push(...file);
|
||||||
})
|
});
|
||||||
allFilesCanonical.push(canonical);
|
allFilesCanonical.push(canonical);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,29 +195,31 @@ exports.replay = function (rawDataDir) {
|
||||||
prev = curr;
|
prev = curr;
|
||||||
}
|
}
|
||||||
return curr;
|
return curr;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.updateData = async function (dataDir, done) {
|
exports.updateData = async function (dataDir, done) {
|
||||||
const today = currentDate();
|
const today = currentDate();
|
||||||
console.log("Fetching data for date: " + today);
|
console.log("Fetching data for date: " + today);
|
||||||
const storeFetchPromises = []
|
const storeFetchPromises = [];
|
||||||
for (const store of STORE_KEYS) {
|
for (const store of STORE_KEYS) {
|
||||||
storeFetchPromises.push(new Promise(async (resolve) => {
|
storeFetchPromises.push(
|
||||||
const start = performance.now();
|
new Promise(async (resolve) => {
|
||||||
try {
|
const start = performance.now();
|
||||||
const storeItems = await stores[store].fetchData();
|
try {
|
||||||
fs.writeFileSync(`${dataDir}/${store}-${today}.json`, JSON.stringify(storeItems, null, 2));
|
const storeItems = await stores[store].fetchData();
|
||||||
const storeItemsCanonical = getCanonicalFor(store, storeItems, today);
|
fs.writeFileSync(`${dataDir}/${store}-${today}.json`, JSON.stringify(storeItems, null, 2));
|
||||||
console.log(`Fetched ${store.toUpperCase()} data, took ${(performance.now() - start) / 1000} seconds`);
|
const storeItemsCanonical = getCanonicalFor(store, storeItems, today);
|
||||||
resolve(storeItemsCanonical)
|
console.log(`Fetched ${store.toUpperCase()} data, took ${(performance.now() - start) / 1000} seconds`);
|
||||||
} catch (e) {
|
resolve(storeItemsCanonical);
|
||||||
console.error(`Error while fetching data from ${store}, continuing after ${(performance.now() - start) / 1000} seconds...`, e);
|
} catch (e) {
|
||||||
resolve([])
|
console.error(`Error while fetching data from ${store}, continuing after ${(performance.now() - start) / 1000} seconds...`, e);
|
||||||
}
|
resolve([]);
|
||||||
}));
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = [].concat(...await Promise.all(storeFetchPromises));
|
const items = [].concat(...(await Promise.all(storeFetchPromises)));
|
||||||
|
|
||||||
if (fs.existsSync(`${dataDir}/latest-canonical.json`)) {
|
if (fs.existsSync(`${dataDir}/latest-canonical.json`)) {
|
||||||
const oldItems = JSON.parse(fs.readFileSync(`${dataDir}/latest-canonical.json`));
|
const oldItems = JSON.parse(fs.readFileSync(`${dataDir}/latest-canonical.json`));
|
||||||
|
@ -227,4 +232,4 @@ exports.updateData = async function (dataDir, done) {
|
||||||
|
|
||||||
if (done) done(items);
|
if (done) done(items);
|
||||||
return items;
|
return items;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
ports:
|
ports:
|
||||||
- 3001:80
|
- 3001:80
|
||||||
site:
|
site:
|
||||||
ports:
|
ports:
|
||||||
- 9230:9230
|
- 9230:9230
|
||||||
environment:
|
environment:
|
||||||
- DEV=true
|
- DEV=true
|
||||||
|
|
||||||
|
|
27
index.js
27
index.js
|
@ -5,7 +5,7 @@ function copyItemsToSite(dataDir) {
|
||||||
fs.copyFileSync(`${dataDir}/latest-canonical.json`, `site/latest-canonical.json`);
|
fs.copyFileSync(`${dataDir}/latest-canonical.json`, `site/latest-canonical.json`);
|
||||||
const items = JSON.parse(fs.readFileSync(`${dataDir}/latest-canonical.json`));
|
const items = JSON.parse(fs.readFileSync(`${dataDir}/latest-canonical.json`));
|
||||||
for (const store of analysis.STORE_KEYS) {
|
for (const store of analysis.STORE_KEYS) {
|
||||||
const storeItems = items.filter(item => item.store === store);
|
const storeItems = items.filter((item) => item.store === store);
|
||||||
fs.writeFileSync(`site/latest-canonical.${store}.compressed.json`, JSON.stringify(analysis.compress(storeItems)));
|
fs.writeFileSync(`site/latest-canonical.${store}.compressed.json`, JSON.stringify(analysis.compress(storeItems)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,12 +31,11 @@ function scheduleFunction(hour, minute, second, func) {
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const dataDir = 'data';
|
const dataDir = "data";
|
||||||
|
|
||||||
if (!fs.existsSync(dataDir)) {
|
if (!fs.existsSync(dataDir)) {
|
||||||
fs.mkdirSync(dataDir)
|
fs.mkdirSync(dataDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(`${dataDir}/latest-canonical.json`)) {
|
if (fs.existsSync(`${dataDir}/latest-canonical.json`)) {
|
||||||
|
@ -45,23 +44,23 @@ function scheduleFunction(hour, minute, second, func) {
|
||||||
copyItemsToSite(dataDir);
|
copyItemsToSite(dataDir);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await analysis.updateData(dataDir)
|
await analysis.updateData(dataDir);
|
||||||
copyItemsToSite(dataDir);
|
copyItemsToSite(dataDir);
|
||||||
}
|
}
|
||||||
scheduleFunction(7, 0, 0, async () => {
|
scheduleFunction(7, 0, 0, async () => {
|
||||||
items = await analysis.updateData(dataDir)
|
items = await analysis.updateData(dataDir);
|
||||||
copyItemsToSite(dataDir);
|
copyItemsToSite(dataDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
const express = require('express')
|
const express = require("express");
|
||||||
const compression = require('compression');
|
const compression = require("compression");
|
||||||
const app = express()
|
const app = express();
|
||||||
const port = process?.argv?.[2] ?? 3000
|
const port = process?.argv?.[2] ?? 3000;
|
||||||
|
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
app.use(express.static('site'));
|
app.use(express.static("site"));
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Example app listening on port ${port}`)
|
console.log(`Example app listening on port ${port}`);
|
||||||
})
|
});
|
||||||
})();
|
})();
|
||||||
|
|
6
pages.js
6
pages.js
|
@ -3,9 +3,9 @@ const analysis = require("./analysis.js");
|
||||||
|
|
||||||
if (process.argv.length < 3) {
|
if (process.argv.length < 3) {
|
||||||
console.log("Usage: node pages.js <data-dir>");
|
console.log("Usage: node pages.js <data-dir>");
|
||||||
console.log()
|
console.log();
|
||||||
console.log("e.g. node pages.js data/");
|
console.log("e.g. node pages.js data/");
|
||||||
console.log()
|
console.log();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const dataDir = process.argv[2];
|
const dataDir = process.argv[2];
|
||||||
|
@ -19,7 +19,7 @@ if (!fs.existsSync(dataDir)) {
|
||||||
await analysis.updateData(dataDir);
|
await analysis.updateData(dataDir);
|
||||||
const items = JSON.parse(fs.readFileSync(`${dataDir}/latest-canonical.json`));
|
const items = JSON.parse(fs.readFileSync(`${dataDir}/latest-canonical.json`));
|
||||||
for (const store of analysis.STORE_KEYS) {
|
for (const store of analysis.STORE_KEYS) {
|
||||||
const storeItems = items.filter(item => item.store === store);
|
const storeItems = items.filter((item) => item.store === store);
|
||||||
fs.writeFileSync(`${dataDir}/latest-canonical.${store}.compressed.json`, JSON.stringify(analysis.compress(storeItems)));
|
fs.writeFileSync(`${dataDir}/latest-canonical.${store}.compressed.json`, JSON.stringify(analysis.compress(storeItems)));
|
||||||
}
|
}
|
||||||
console.log(`Wrote ${items.length} items to ${dataDir}/latest-canonical(-compressed).json`);
|
console.log(`Wrote ${items.length} items to ${dataDir}/latest-canonical(-compressed).json`);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const analysis = require("./analysis.js");
|
const analysis = require("./analysis.js");
|
||||||
const dataDir = process?.argv?.[2] ?? "docker/data"
|
const dataDir = process?.argv?.[2] ?? "docker/data";
|
||||||
console.log("Restoring data from raw data.");
|
console.log("Restoring data from raw data.");
|
||||||
(async function () {
|
(async function () {
|
||||||
/*console.log("Items: " + JSON.parse(fs.readFileSync("docker/data/latest-canonical.json")).length);
|
/*console.log("Items: " + JSON.parse(fs.readFileSync("docker/data/latest-canonical.json")).length);
|
||||||
|
|
|
@ -1,34 +1,31 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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>Heisse Preise</title>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8">
|
<div class="column">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<h2 id="date"></h2>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<div class="filters" id="filters-store"></div>
|
||||||
<title>Heisse Preise</title>
|
<div class="filters" id="filters-changes">
|
||||||
<link
|
<label><input id="increases" type="checkbox" checked="true" /> Teurer</label>
|
||||||
rel="icon"
|
<label><input id="decreases" type="checkbox" checked="true" /> Billiger</label>
|
||||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
</div>
|
||||||
/>
|
<input id="filter" type="text" style="max-width: 800px; width: 100%" placeholder="Filtern..." />
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
<div id="numresults" style="margin-top: 1em"></div>
|
||||||
</head>
|
<table id="result"></table>
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="column">
|
|
||||||
<h2 id="date"></h2>
|
|
||||||
<div class="filters" id="filters-store">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="filters" id="filters-changes">
|
<script src="alasql.js"></script>
|
||||||
<label><input id="increases" type="checkbox" checked="true"> Teurer</label>
|
<script src="utils.js"></script>
|
||||||
<label><input id="decreases" type="checkbox" checked="true"> Billiger</label>
|
<script src="aktionen.js"></script>
|
||||||
</div>
|
</body>
|
||||||
<input id="filter" type="text" style="max-width: 800px; width: 100%;" placeholder="Filtern...">
|
</html>
|
||||||
<div id="numresults" style="margin-top: 1em"></div>
|
|
||||||
<table id="result"></table>
|
|
||||||
</div>
|
|
||||||
<script src="alasql.js"></script>
|
|
||||||
<script src="utils.js"></script>
|
|
||||||
<script src="aktionen.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -23,13 +23,18 @@ async function load() {
|
||||||
document.querySelector("#date").innerText = "Preisänderungen am " + currentDate();
|
document.querySelector("#date").innerText = "Preisänderungen am " + currentDate();
|
||||||
|
|
||||||
const filtersStore = document.querySelector("#filters-store");
|
const filtersStore = document.querySelector("#filters-store");
|
||||||
filtersStore.innerHTML = STORE_KEYS.map(store => `<label><input id="${store}" type="checkbox" checked="true">${stores[store].name}</label>`).join(" ");
|
filtersStore.innerHTML = STORE_KEYS.map(
|
||||||
filtersStore.querySelectorAll("input").forEach(input => {
|
(store) => `<label><input id="${store}" type="checkbox" checked="true">${stores[store].name}</label>`
|
||||||
input.addEventListener("change", () => showResults(items, currentDate()));
|
).join(" ");
|
||||||
});
|
filtersStore.querySelectorAll("input").forEach((input) => {
|
||||||
document.querySelector("#filters-changes").querySelectorAll("input").forEach(input => {
|
|
||||||
input.addEventListener("change", () => showResults(items, currentDate()));
|
input.addEventListener("change", () => showResults(items, currentDate()));
|
||||||
});
|
});
|
||||||
|
document
|
||||||
|
.querySelector("#filters-changes")
|
||||||
|
.querySelectorAll("input")
|
||||||
|
.forEach((input) => {
|
||||||
|
input.addEventListener("change", () => showResults(items, currentDate()));
|
||||||
|
});
|
||||||
document.querySelector("#filter").addEventListener("input", () => showResults(items, currentDate()));
|
document.querySelector("#filter").addEventListener("input", () => showResults(items, currentDate()));
|
||||||
showResults(items, currentDate());
|
showResults(items, currentDate());
|
||||||
}
|
}
|
||||||
|
@ -37,8 +42,8 @@ async function load() {
|
||||||
function showResults(items, today) {
|
function showResults(items, today) {
|
||||||
const increases = document.querySelector("#increases").checked;
|
const increases = document.querySelector("#increases").checked;
|
||||||
const decreases = document.querySelector("#decreases").checked;
|
const decreases = document.querySelector("#decreases").checked;
|
||||||
const storeCheckboxes = STORE_KEYS.map(store => document.querySelector(`#${store}`));
|
const storeCheckboxes = STORE_KEYS.map((store) => document.querySelector(`#${store}`));
|
||||||
const checkedStores = STORE_KEYS.filter((store, i) => storeCheckboxes[i].checked)
|
const checkedStores = STORE_KEYS.filter((store, i) => storeCheckboxes[i].checked);
|
||||||
let changedItems = [];
|
let changedItems = [];
|
||||||
for (item of items) {
|
for (item of items) {
|
||||||
if (item.priceHistory.length < 2) continue;
|
if (item.priceHistory.length < 2) continue;
|
||||||
|
@ -47,28 +52,32 @@ function showResults(items, today) {
|
||||||
if (!checkedStores.includes(item.store)) continue;
|
if (!checkedStores.includes(item.store)) continue;
|
||||||
|
|
||||||
if (item.priceHistory[i].date == today && i + 1 < item.priceHistory.length) {
|
if (item.priceHistory[i].date == today && i + 1 < item.priceHistory.length) {
|
||||||
if (increases && (item.priceHistory[i].price > item.priceHistory[i + 1].price)) changedItems.push(item);
|
if (increases && item.priceHistory[i].price > item.priceHistory[i + 1].price) changedItems.push(item);
|
||||||
if (decreases && (item.priceHistory[i].price < item.priceHistory[i + 1].price)) changedItems.push(item);
|
if (decreases && item.priceHistory[i].price < item.priceHistory[i + 1].price) changedItems.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const query = document.querySelector("#filter").value.trim();
|
const query = document.querySelector("#filter").value.trim();
|
||||||
const total = changedItems.length;
|
const total = changedItems.length;
|
||||||
if (query.length >= 3) changedItems = searchItems(changedItems, document.querySelector("#filter").value, checkedStores, false, 0, 10000, false, false);
|
if (query.length >= 3)
|
||||||
|
changedItems = searchItems(changedItems, document.querySelector("#filter").value, checkedStores, false, 0, 10000, false, false);
|
||||||
document.querySelector("#numresults").innerText = "Resultate: " + changedItems.length + (total > changedItems.length ? " / " + total : "");
|
document.querySelector("#numresults").innerText = "Resultate: " + changedItems.length + (total > changedItems.length ? " / " + total : "");
|
||||||
|
|
||||||
const table = document.querySelector("#result");
|
const table = document.querySelector("#result");
|
||||||
table.innerHTML = "";
|
table.innerHTML = "";
|
||||||
const header = dom("thead", `
|
const header = dom(
|
||||||
|
"thead",
|
||||||
|
`
|
||||||
<tr><th>Kette</th><th>Name</th><th>Menge</th><th>Preis 📈</th></tr>
|
<tr><th>Kette</th><th>Name</th><th>Menge</th><th>Preis 📈</th></tr>
|
||||||
`)
|
`
|
||||||
const showHideAll = header.querySelectorAll('th:nth-child(4)')[0];
|
);
|
||||||
|
const showHideAll = header.querySelectorAll("th:nth-child(4)")[0];
|
||||||
showHideAll.style["cursor"] = "pointer";
|
showHideAll.style["cursor"] = "pointer";
|
||||||
showHideAll.showAll = true;
|
showHideAll.showAll = true;
|
||||||
showHideAll.addEventListener("click", () => {
|
showHideAll.addEventListener("click", () => {
|
||||||
table.querySelectorAll(".priceinfo").forEach(el => showHideAll.showAll ? el.classList.remove("hide") : el.classList.add("hide"));
|
table.querySelectorAll(".priceinfo").forEach((el) => (showHideAll.showAll ? el.classList.remove("hide") : el.classList.add("hide")));
|
||||||
showHideAll.showAll = !showHideAll.showAll;
|
showHideAll.showAll = !showHideAll.showAll;
|
||||||
})
|
});
|
||||||
|
|
||||||
table.appendChild(header);
|
table.appendChild(header);
|
||||||
|
|
||||||
|
@ -79,4 +88,4 @@ function showResults(items, today) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|
|
@ -1,30 +1,27 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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>Heisse Preise</title>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8">
|
<div class="column">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<h2>Billiger seit letzter Preisänderung</h2>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<div class="filters" id="filters-store"></div>
|
||||||
<title>Heisse Preise</title>
|
<input id="filter" type="text" style="max-width: 800px; width: 100%" placeholder="Filtern..." />
|
||||||
<link
|
<div id="numresults" style="margin-top: 1em"></div>
|
||||||
rel="icon"
|
<table id="result"></table>
|
||||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
|
||||||
/>
|
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="column">
|
|
||||||
<h2>Billiger seit letzter Preisänderung</h2>
|
|
||||||
<div class="filters" id="filters-store">
|
|
||||||
</div>
|
</div>
|
||||||
<input id="filter" type="text" style="max-width: 800px; width: 100%;" placeholder="Filtern...">
|
<script src="alasql.js"></script>
|
||||||
<div id="numresults" style="margin-top: 1em"></div>
|
<script src="utils.js"></script>
|
||||||
<table id="result"></table>
|
<script src="billiger.js"></script>
|
||||||
</div>
|
</body>
|
||||||
<script src="alasql.js"></script>
|
</html>
|
||||||
<script src="utils.js"></script>
|
|
||||||
<script src="billiger.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -20,8 +20,10 @@ async function load() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const filtersStore = document.querySelector("#filters-store");
|
const filtersStore = document.querySelector("#filters-store");
|
||||||
filtersStore.innerHTML = STORE_KEYS.map(store => `<label><input id="${store}" type="checkbox" checked="true">${stores[store].name}</label>`).join(" ");
|
filtersStore.innerHTML = STORE_KEYS.map(
|
||||||
filtersStore.querySelectorAll("input").forEach(input => {
|
(store) => `<label><input id="${store}" type="checkbox" checked="true">${stores[store].name}</label>`
|
||||||
|
).join(" ");
|
||||||
|
filtersStore.querySelectorAll("input").forEach((input) => {
|
||||||
input.addEventListener("change", () => showResults(items, currentDate()));
|
input.addEventListener("change", () => showResults(items, currentDate()));
|
||||||
});
|
});
|
||||||
document.querySelector("#filter").addEventListener("input", () => showResults(items, currentDate()));
|
document.querySelector("#filter").addEventListener("input", () => showResults(items, currentDate()));
|
||||||
|
@ -29,34 +31,37 @@ async function load() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showResults(items, _today) {
|
function showResults(items, _today) {
|
||||||
const storeCheckboxes = STORE_KEYS.map(store => document.querySelector(`#${store}`));
|
const storeCheckboxes = STORE_KEYS.map((store) => document.querySelector(`#${store}`));
|
||||||
const checkedStores = STORE_KEYS.filter((_store, i) => storeCheckboxes[i].checked)
|
const checkedStores = STORE_KEYS.filter((_store, i) => storeCheckboxes[i].checked);
|
||||||
let changedItems = [];
|
let changedItems = [];
|
||||||
for (item of items) {
|
for (item of items) {
|
||||||
if (item.priceHistory.length < 2) continue;
|
if (item.priceHistory.length < 2) continue;
|
||||||
if (!checkedStores.includes(item.store)) continue;
|
if (!checkedStores.includes(item.store)) continue;
|
||||||
|
|
||||||
if (item.priceHistory[0].price < item.priceHistory[1].price && item.priceHistory[1].date.indexOf("2020") != 0)
|
if (item.priceHistory[0].price < item.priceHistory[1].price && item.priceHistory[1].date.indexOf("2020") != 0) changedItems.push(item);
|
||||||
changedItems.push(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = document.querySelector("#filter").value.trim();
|
const query = document.querySelector("#filter").value.trim();
|
||||||
const total = changedItems.length;
|
const total = changedItems.length;
|
||||||
if (query.length >= 3) changedItems = searchItems(changedItems, document.querySelector("#filter").value, checkedStores, false, 0, 10000, false, false);
|
if (query.length >= 3)
|
||||||
|
changedItems = searchItems(changedItems, document.querySelector("#filter").value, checkedStores, false, 0, 10000, false, false);
|
||||||
document.querySelector("#numresults").innerText = "Resultate: " + changedItems.length + (total > changedItems.length ? " / " + total : "");
|
document.querySelector("#numresults").innerText = "Resultate: " + changedItems.length + (total > changedItems.length ? " / " + total : "");
|
||||||
|
|
||||||
const table = document.querySelector("#result");
|
const table = document.querySelector("#result");
|
||||||
table.innerHTML = "";
|
table.innerHTML = "";
|
||||||
const header = dom("thead", `
|
const header = dom(
|
||||||
|
"thead",
|
||||||
|
`
|
||||||
<tr><th>Kette</th><th>Name</th><th>Menge</th><th>Preis 📈</th></tr>
|
<tr><th>Kette</th><th>Name</th><th>Menge</th><th>Preis 📈</th></tr>
|
||||||
`)
|
`
|
||||||
const showHideAll = header.querySelectorAll('th:nth-child(4)')[0];
|
);
|
||||||
|
const showHideAll = header.querySelectorAll("th:nth-child(4)")[0];
|
||||||
showHideAll.style["cursor"] = "pointer";
|
showHideAll.style["cursor"] = "pointer";
|
||||||
showHideAll.showAll = true;
|
showHideAll.showAll = true;
|
||||||
showHideAll.addEventListener("click", () => {
|
showHideAll.addEventListener("click", () => {
|
||||||
table.querySelectorAll(".priceinfo").forEach(el => showHideAll.showAll ? el.classList.remove("hide") : el.classList.add("hide"));
|
table.querySelectorAll(".priceinfo").forEach((el) => (showHideAll.showAll ? el.classList.remove("hide") : el.classList.add("hide")));
|
||||||
showHideAll.showAll = !showHideAll.showAll;
|
showHideAll.showAll = !showHideAll.showAll;
|
||||||
})
|
});
|
||||||
|
|
||||||
table.appendChild(header);
|
table.appendChild(header);
|
||||||
|
|
||||||
|
@ -67,4 +72,4 @@ function showResults(items, _today) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|
|
@ -1,48 +1,45 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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>Heisse Preise</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8">
|
<div class="column">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<h2>Heisse Preise</h2>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<div class="filters">
|
||||||
<title>Heisse Preise</title>
|
<a href="index.html">Produktsuche</a>
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
<a href="changes.html">Tagespreisänderungen</a>
|
||||||
</head>
|
<a href="carts.html">Warenkörbe</a>
|
||||||
|
</div>
|
||||||
<body>
|
<div id="cart" class="cart column">
|
||||||
<div class="column">
|
<h3 id="cartname"></h3>
|
||||||
<h2>Heisse Preise</h2>
|
<input type="button" id="save" value="Speichern" class="hide" />
|
||||||
<div class="filters">
|
<canvas id="chart"></canvas>
|
||||||
<a href="index.html">Produktsuche</a>
|
<div class="filters" style="margin-top: 1em">
|
||||||
<a href="changes.html">Tagespreisänderungen</a>
|
<label><input type="checkbox" id="sum" checked /> Preissumme Warenkorb</label>
|
||||||
<a href="carts.html">Warenkörbe</a>
|
<label><input type="checkbox" id="sumstores" checked /> Preissumme pro Kette</label>
|
||||||
|
<label><input type="checkbox" id="todayonly" /> Nur heutige Preise</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Von <input id="start" type="date" /></label>
|
||||||
|
<label>Bis <input id="end" type="date" /></label>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<input id="filter" type="text" style="max-width: 800px; width: 100%; margin-bottom: 1em" placeholder="Filtern..." />
|
||||||
|
<div class="filters" id="filters-store"></div>
|
||||||
|
<div id="numitems"></div>
|
||||||
|
<table id="cartitems"></table>
|
||||||
|
</div>
|
||||||
|
<div id="search" class="column"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cart" class="cart column">
|
<script src="chart.js"></script>
|
||||||
<h3 id="cartname"></h3>
|
<script src="alasql.js"></script>
|
||||||
<input type="button" id="save" value="Speichern" class="hide">
|
<script src="utils.js"></script>
|
||||||
<canvas id="chart"></canvas>
|
<script src="cart.js"></script>
|
||||||
<div class="filters" style="margin-top: 1em;">
|
</body>
|
||||||
<label><input type="checkbox" id="sum" checked> Preissumme Warenkorb</label>
|
</html>
|
||||||
<label><input type="checkbox" id="sumstores" checked> Preissumme pro Kette</label>
|
|
||||||
<label><input type="checkbox" id="todayonly"> Nur heutige Preise</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Von <input id="start" type="date"></label>
|
|
||||||
<label>Bis <input id="end"type="date"></label>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<input id="filter" type="text" style="max-width: 800px; width: 100%; margin-bottom: 1em;" placeholder="Filtern...">
|
|
||||||
<div class="filters" id="filters-store">
|
|
||||||
</div>
|
|
||||||
<div id="numitems"></div>
|
|
||||||
<table id="cartitems"></table>
|
|
||||||
</div>
|
|
||||||
<div id="search" class="column"></div>
|
|
||||||
</div>
|
|
||||||
<script src="chart.js"></script>
|
|
||||||
<script src="alasql.js"></script>
|
|
||||||
<script src="utils.js"></script>
|
|
||||||
<script src="cart.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
70
site/cart.js
70
site/cart.js
|
@ -35,7 +35,7 @@ async function load() {
|
||||||
cart = {
|
cart = {
|
||||||
name: tokens[0],
|
name: tokens[0],
|
||||||
items: [],
|
items: [],
|
||||||
linked: true
|
linked: true,
|
||||||
};
|
};
|
||||||
for (let i = 1; i < tokens.length; i++) {
|
for (let i = 1; i < tokens.length; i++) {
|
||||||
const item = lookup[tokens[i]];
|
const item = lookup[tokens[i]];
|
||||||
|
@ -73,16 +73,18 @@ async function load() {
|
||||||
document.querySelector("#end").value = currentDate();
|
document.querySelector("#end").value = currentDate();
|
||||||
|
|
||||||
const filtersStore = document.querySelector("#filters-store");
|
const filtersStore = document.querySelector("#filters-store");
|
||||||
filtersStore.innerHTML = STORE_KEYS.map(store => `<label><input id="${store}" type="checkbox" checked="true">${stores[store].name}</label>`).join(" ");
|
filtersStore.innerHTML = STORE_KEYS.map(
|
||||||
filtersStore.querySelectorAll("input").forEach(input => input.addEventListener("change", () => showCart(cart)));
|
(store) => `<label><input id="${store}" type="checkbox" checked="true">${stores[store].name}</label>`
|
||||||
|
).join(" ");
|
||||||
|
filtersStore.querySelectorAll("input").forEach((input) => input.addEventListener("change", () => showCart(cart)));
|
||||||
document.querySelector("#filter").addEventListener("input", () => showCart(cart));
|
document.querySelector("#filter").addEventListener("input", () => showCart(cart));
|
||||||
showCart(cart);
|
showCart(cart);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filter(cartItems) {
|
function filter(cartItems) {
|
||||||
const query = document.querySelector("#filter").value.trim();
|
const query = document.querySelector("#filter").value.trim();
|
||||||
const storeCheckboxes = STORE_KEYS.map(store => document.querySelector(`#${store}`));
|
const storeCheckboxes = STORE_KEYS.map((store) => document.querySelector(`#${store}`));
|
||||||
const checkedStores = STORE_KEYS.filter((store, i) => storeCheckboxes[i].checked)
|
const checkedStores = STORE_KEYS.filter((store, i) => storeCheckboxes[i].checked);
|
||||||
let items = [];
|
let items = [];
|
||||||
if (query.charAt(0) != "!") {
|
if (query.charAt(0) != "!") {
|
||||||
for (item of cartItems) {
|
for (item of cartItems) {
|
||||||
|
@ -99,21 +101,28 @@ function filter(cartItems) {
|
||||||
function showSearch(cart, items) {
|
function showSearch(cart, items) {
|
||||||
const searchDom = document.querySelector("#search");
|
const searchDom = document.querySelector("#search");
|
||||||
searchDom.innerHTML = "";
|
searchDom.innerHTML = "";
|
||||||
newSearchComponent(searchDom, items, null, null, (header) => {
|
newSearchComponent(
|
||||||
header.append(dom("th", ""));
|
searchDom,
|
||||||
return header;
|
items,
|
||||||
}, (item, itemDom) => {
|
null,
|
||||||
const cell = dom("td", `<input type="button" value="+">`);
|
null,
|
||||||
cell.children[0].addEventListener("click", () => {
|
(header) => {
|
||||||
cart.items.push(item);
|
header.append(dom("th", ""));
|
||||||
shoppingCarts.save();
|
return header;
|
||||||
document.querySelector("#start").value = getOldestDate(cart.items);
|
},
|
||||||
document.querySelector("#end").value = currentDate();
|
(item, itemDom) => {
|
||||||
showCart(cart);
|
const cell = dom("td", `<input type="button" value="+">`);
|
||||||
});
|
cell.children[0].addEventListener("click", () => {
|
||||||
itemDom.appendChild(cell);
|
cart.items.push(item);
|
||||||
return itemDom;
|
shoppingCarts.save();
|
||||||
});
|
document.querySelector("#start").value = getOldestDate(cart.items);
|
||||||
|
document.querySelector("#end").value = currentDate();
|
||||||
|
showCart(cart);
|
||||||
|
});
|
||||||
|
itemDom.appendChild(cell);
|
||||||
|
return itemDom;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCharts(canvasDom, items) {
|
function updateCharts(canvasDom, items) {
|
||||||
|
@ -136,7 +145,7 @@ function updateCharts(canvasDom, items) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCart(cart) {
|
function showCart(cart) {
|
||||||
let link = encodeURIComponent(cart.name) + ";"
|
let link = encodeURIComponent(cart.name) + ";";
|
||||||
for (cartItem of cart.items) {
|
for (cartItem of cart.items) {
|
||||||
link += cartItem.store + cartItem.id + ";";
|
link += cartItem.store + cartItem.id + ";";
|
||||||
}
|
}
|
||||||
|
@ -157,14 +166,17 @@ function showCart(cart) {
|
||||||
itemTable.append(header);
|
itemTable.append(header);
|
||||||
|
|
||||||
items.forEach((cartItem, idx) => {
|
items.forEach((cartItem, idx) => {
|
||||||
const itemDom = itemToDOM(cartItem)
|
const itemDom = itemToDOM(cartItem);
|
||||||
|
|
||||||
const cell = dom("td", `
|
const cell = dom(
|
||||||
|
"td",
|
||||||
|
`
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<input type="button" value="-">
|
<input type="button" value="-">
|
||||||
<input type="button" value="⬆️">
|
<input type="button" value="⬆️">
|
||||||
<input type="button" value="⬇️">
|
<input type="button" value="⬇️">
|
||||||
`);
|
`
|
||||||
|
);
|
||||||
|
|
||||||
if (cartItem.chart) cell.children[0].setAttribute("checked", true);
|
if (cartItem.chart) cell.children[0].setAttribute("checked", true);
|
||||||
cell.children[0].addEventListener("change", () => {
|
cell.children[0].addEventListener("change", () => {
|
||||||
|
@ -179,7 +191,7 @@ function showCart(cart) {
|
||||||
shoppingCarts.save();
|
shoppingCarts.save();
|
||||||
document.querySelector("#start").value = getOldestDate(cart.items);
|
document.querySelector("#start").value = getOldestDate(cart.items);
|
||||||
document.querySelector("#end").value = currentDate();
|
document.querySelector("#end").value = currentDate();
|
||||||
showCart(cart)
|
showCart(cart);
|
||||||
});
|
});
|
||||||
|
|
||||||
cell.children[2].addEventListener("click", () => {
|
cell.children[2].addEventListener("click", () => {
|
||||||
|
@ -188,7 +200,7 @@ function showCart(cart) {
|
||||||
cart.items[idx - 1] = cartItem;
|
cart.items[idx - 1] = cartItem;
|
||||||
cart.items[idx] = otherItem;
|
cart.items[idx] = otherItem;
|
||||||
shoppingCarts.save();
|
shoppingCarts.save();
|
||||||
showCart(cart)
|
showCart(cart);
|
||||||
});
|
});
|
||||||
|
|
||||||
cell.children[3].addEventListener("click", () => {
|
cell.children[3].addEventListener("click", () => {
|
||||||
|
@ -197,10 +209,10 @@ function showCart(cart) {
|
||||||
cart.items[idx + 1] = cartItem;
|
cart.items[idx + 1] = cartItem;
|
||||||
cart.items[idx] = otherItem;
|
cart.items[idx] = otherItem;
|
||||||
shoppingCarts.save();
|
shoppingCarts.save();
|
||||||
showCart(cart)
|
showCart(cart);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cell.querySelectorAll("input[type='button']").forEach(button => button.classList.add("hide"));
|
cell.querySelectorAll("input[type='button']").forEach((button) => button.classList.add("hide"));
|
||||||
}
|
}
|
||||||
|
|
||||||
itemDom.append(cell);
|
itemDom.append(cell);
|
||||||
|
@ -208,4 +220,4 @@ function showCart(cart) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|
|
@ -1,37 +1,35 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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>Heisse Preise</title>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8">
|
<div class="column">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<h2>Heisse Preise</h2>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<div class="filters">
|
||||||
<title>Heisse Preise</title>
|
<a href="index.html">Produktsuche</a>
|
||||||
<link
|
<a href="changes.html">Tagespreisänderungen</a>
|
||||||
rel="icon"
|
<a href="carts.html"><strong>Warenkörbe</strong></a>
|
||||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
</div>
|
||||||
/>
|
<div class="filters">
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
<button id="newcart">Neuer Warenkorb</button>
|
||||||
</head>
|
<button id="export">Exportieren</button>
|
||||||
|
<button id="import">Importieren</button>
|
||||||
<body>
|
</div>
|
||||||
<div class="column">
|
<table id="carts" class="carts"></table>
|
||||||
<h2>Heisse Preise</h2>
|
|
||||||
<div class="filters">
|
|
||||||
<a href="index.html">Produktsuche</a>
|
|
||||||
<a href="changes.html">Tagespreisänderungen</a>
|
|
||||||
<a href="carts.html"><strong>Warenkörbe</strong></a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="filters">
|
<input type="file" id="fileInput" style="display: none" />
|
||||||
<button id="newcart">Neuer Warenkorb</button>
|
<script src="alasql.js"></script>
|
||||||
<button id="export">Exportieren</button>
|
<script src="utils.js"></script>
|
||||||
<button id="import">Importieren</button>
|
<script src="carts.js"></script>
|
||||||
</div>
|
</body>
|
||||||
<table id="carts" class="carts"></table>
|
</html>
|
||||||
</div>
|
|
||||||
<input type="file" id="fileInput" style="display: none;">
|
|
||||||
<script src="alasql.js"></script>
|
|
||||||
<script src="utils.js"></script>
|
|
||||||
<script src="carts.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ async function load() {
|
||||||
}
|
}
|
||||||
shoppingCarts.save();
|
shoppingCarts.save();
|
||||||
|
|
||||||
if (shoppingCarts.carts.findIndex(cart => cart.name === "Momentum Eigenmarken Vergleich") == -1) {
|
if (shoppingCarts.carts.findIndex((cart) => cart.name === "Momentum Eigenmarken Vergleich") == -1) {
|
||||||
response = await fetch("momentum-cart.json");
|
response = await fetch("momentum-cart.json");
|
||||||
momentumCart = await response.json();
|
momentumCart = await response.json();
|
||||||
shoppingCarts.carts.unshift(momentumCart);
|
shoppingCarts.carts.unshift(momentumCart);
|
||||||
|
@ -48,11 +48,11 @@ async function load() {
|
||||||
|
|
||||||
const importButton = document.querySelector("#import");
|
const importButton = document.querySelector("#import");
|
||||||
importButton.addEventListener("click", () => {
|
importButton.addEventListener("click", () => {
|
||||||
document.getElementById('fileInput').value = null
|
document.getElementById("fileInput").value = null;
|
||||||
document.getElementById('fileInput').click();
|
document.getElementById("fileInput").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#fileInput").addEventListener('change', function (event) {
|
document.querySelector("#fileInput").addEventListener("change", function (event) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function (event) {
|
reader.onload = function (event) {
|
||||||
|
@ -67,12 +67,12 @@ async function load() {
|
||||||
}
|
}
|
||||||
importedCart.items = items;
|
importedCart.items = items;
|
||||||
|
|
||||||
const index = shoppingCarts.carts.findIndex(cart => cart.name === importedCart.name);
|
const index = shoppingCarts.carts.findIndex((cart) => cart.name === importedCart.name);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
if (confirm("Existierenden Warenkorb '" + importedCart.name + " überschreiben?")) {
|
if (confirm("Existierenden Warenkorb '" + importedCart.name + " überschreiben?")) {
|
||||||
console.log(shoppingCarts.carts[index]);
|
console.log(shoppingCarts.carts[index]);
|
||||||
shoppingCarts.carts[index] = importedCart;
|
shoppingCarts.carts[index] = importedCart;
|
||||||
console.log(shoppingCarts.carts[index])
|
console.log(shoppingCarts.carts[index]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
shoppingCarts.carts.push(importedCart);
|
shoppingCarts.carts.push(importedCart);
|
||||||
|
@ -90,19 +90,24 @@ async function load() {
|
||||||
function showCarts(lookup) {
|
function showCarts(lookup) {
|
||||||
const cartsTable = document.querySelector("#carts");
|
const cartsTable = document.querySelector("#carts");
|
||||||
cartsTable.innerHTML = "";
|
cartsTable.innerHTML = "";
|
||||||
cartsTable.appendChild(dom("thead", `
|
cartsTable.appendChild(
|
||||||
|
dom(
|
||||||
|
"thead",
|
||||||
|
`
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Produkte</th>
|
<th>Produkte</th>
|
||||||
<th>Preis</th>
|
<th>Preis</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
`));
|
`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
shoppingCarts.carts.forEach(cart => {
|
shoppingCarts.carts.forEach((cart) => {
|
||||||
let oldPrice = 0;
|
let oldPrice = 0;
|
||||||
let currPrice = 0;
|
let currPrice = 0;
|
||||||
let link = encodeURIComponent(cart.name) + ";"
|
let link = encodeURIComponent(cart.name) + ";";
|
||||||
for (cartItem of cart.items) {
|
for (cartItem of cart.items) {
|
||||||
const item = lookup[cartItem.store + cartItem.id];
|
const item = lookup[cartItem.store + cartItem.id];
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
|
@ -110,7 +115,7 @@ function showCarts(lookup) {
|
||||||
currPrice += item.priceHistory[0].price;
|
currPrice += item.priceHistory[0].price;
|
||||||
link += item.store + item.id + ";";
|
link += item.store + item.id + ";";
|
||||||
}
|
}
|
||||||
const increase = oldPrice != 0 ? Math.round((currPrice - oldPrice) / oldPrice * 100) : 0;
|
const increase = oldPrice != 0 ? Math.round(((currPrice - oldPrice) / oldPrice) * 100) : 0;
|
||||||
|
|
||||||
const row = dom("tr", ``);
|
const row = dom("tr", ``);
|
||||||
|
|
||||||
|
@ -122,12 +127,15 @@ function showCarts(lookup) {
|
||||||
itemsDom.setAttribute("data-label", "Produkte");
|
itemsDom.setAttribute("data-label", "Produkte");
|
||||||
row.appendChild(itemsDom);
|
row.appendChild(itemsDom);
|
||||||
|
|
||||||
const priceDom = dom("td", `<span style="color: ${currPrice > oldPrice ? "red" : "green"}">${currPrice.toFixed(2)} ${(increase > 0 ? "+" : "") + increase + "%"}`);
|
const priceDom = dom(
|
||||||
|
"td",
|
||||||
|
`<span style="color: ${currPrice > oldPrice ? "red" : "green"}">${currPrice.toFixed(2)} ${(increase > 0 ? "+" : "") + increase + "%"}`
|
||||||
|
);
|
||||||
priceDom.setAttribute("data-label", "Preis");
|
priceDom.setAttribute("data-label", "Preis");
|
||||||
row.appendChild(priceDom);
|
row.appendChild(priceDom);
|
||||||
|
|
||||||
const actionsDom = dom("div", "");
|
const actionsDom = dom("div", "");
|
||||||
actionsDom.classList.add("cartactions")
|
actionsDom.classList.add("cartactions");
|
||||||
const linkDom = dom("a", "Teilen");
|
const linkDom = dom("a", "Teilen");
|
||||||
linkDom.setAttribute("href", "cart.html?cart=" + link);
|
linkDom.setAttribute("href", "cart.html?cart=" + link);
|
||||||
actionsDom.appendChild(linkDom);
|
actionsDom.appendChild(linkDom);
|
||||||
|
@ -137,7 +145,7 @@ function showCarts(lookup) {
|
||||||
jsonDom.addEventListener("click", (event) => {
|
jsonDom.addEventListener("click", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
downloadFile(cart.name + ".json", JSON.stringify(cart, null, 2));
|
downloadFile(cart.name + ".json", JSON.stringify(cart, null, 2));
|
||||||
})
|
});
|
||||||
actionsDom.appendChild(jsonDom);
|
actionsDom.appendChild(jsonDom);
|
||||||
|
|
||||||
if (cart.name != "Momentum Eigenmarken Vergleich") {
|
if (cart.name != "Momentum Eigenmarken Vergleich") {
|
||||||
|
@ -158,4 +166,4 @@ function showCarts(lookup) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|
1484
site/carts.json
1484
site/carts.json
File diff suppressed because it is too large
Load Diff
|
@ -1,40 +1,41 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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>Heisse Preise</title>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8">
|
<div class="column">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<h2>Heisse Preise</h2>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<div class="filters">
|
||||||
<title>Heisse Preise</title>
|
<a href="index.html">Produktsuche</a>
|
||||||
<link
|
<a href="changes.html"><strong>Tagespreisänderungen</strong></a>
|
||||||
rel="icon"
|
<a href="carts.html">Warenkörbe</a>
|
||||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
</div>
|
||||||
/>
|
<div class="filters">
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
<label
|
||||||
</head>
|
>Preisänderungen am
|
||||||
|
<select id="dates"></select
|
||||||
<body>
|
></label>
|
||||||
<div class="column">
|
</div>
|
||||||
<h2>Heisse Preise</h2>
|
<div class="filters">
|
||||||
<div class="filters">
|
<label><input id="increases" type="checkbox" checked="true" />Teurer</label>
|
||||||
<a href="index.html">Produktsuche</a>
|
<label><input id="decreases" type="checkbox" checked="true" />Billiger</label>
|
||||||
<a href="changes.html"><strong>Tagespreisänderungen</strong></a>
|
<label><input id="fullhistory" type="checkbox" checked="true" />Gesamte Preishistorie</label>
|
||||||
<a href="carts.html">Warenkörbe</a>
|
</div>
|
||||||
|
<div id="results"></div>
|
||||||
|
<table id="result"></table>
|
||||||
</div>
|
</div>
|
||||||
<div class="filters">
|
<script src="alasql.js"></script>
|
||||||
<label>Preisänderungen am <select id="dates"></select></label>
|
<script src="utils.js"></script>
|
||||||
</div>
|
<script src="changes.js"></script>
|
||||||
<div class="filters">
|
</body>
|
||||||
<label><input id="increases" type="checkbox" checked="true">Teurer</label>
|
</html>
|
||||||
<label><input id="decreases" type="checkbox" checked="true">Billiger</label>
|
|
||||||
<label><input id="fullhistory" type="checkbox" checked="true">Gesamte Preishistorie</label>
|
|
||||||
</div>
|
|
||||||
<div id="results"></div>
|
|
||||||
<table id="result"></table>
|
|
||||||
</div>
|
|
||||||
<script src="alasql.js"></script>
|
|
||||||
<script src="utils.js"></script>
|
|
||||||
<script src="changes.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -38,13 +38,13 @@ async function load() {
|
||||||
|
|
||||||
dateSelection.addEventListener("change", () => {
|
dateSelection.addEventListener("change", () => {
|
||||||
showResults(items, dateSelection.value);
|
showResults(items, dateSelection.value);
|
||||||
})
|
});
|
||||||
document.querySelector("#increases").addEventListener("change", () => {
|
document.querySelector("#increases").addEventListener("change", () => {
|
||||||
showResults(items, dateSelection.value);
|
showResults(items, dateSelection.value);
|
||||||
})
|
});
|
||||||
document.querySelector("#decreases").addEventListener("change", () => {
|
document.querySelector("#decreases").addEventListener("change", () => {
|
||||||
showResults(items, dateSelection.value);
|
showResults(items, dateSelection.value);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showResults(items, today) {
|
function showResults(items, today) {
|
||||||
|
@ -57,24 +57,27 @@ function showResults(items, today) {
|
||||||
|
|
||||||
for (let i = 0; i < item.priceHistory.length; i++) {
|
for (let i = 0; i < item.priceHistory.length; i++) {
|
||||||
if (item.priceHistory[i].date == today && i + 1 < item.priceHistory.length) {
|
if (item.priceHistory[i].date == today && i + 1 < item.priceHistory.length) {
|
||||||
if (increases && (item.priceHistory[i].price > item.priceHistory[i + 1].price)) changedItems.push(item);
|
if (increases && item.priceHistory[i].price > item.priceHistory[i + 1].price) changedItems.push(item);
|
||||||
if (decreases && (item.priceHistory[i].price < item.priceHistory[i + 1].price)) changedItems.push(item);
|
if (decreases && item.priceHistory[i].price < item.priceHistory[i + 1].price) changedItems.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = document.querySelector("#result");
|
const table = document.querySelector("#result");
|
||||||
table.innerHTML = "";
|
table.innerHTML = "";
|
||||||
const header = dom("thead", `
|
const header = dom(
|
||||||
|
"thead",
|
||||||
|
`
|
||||||
<tr><th>Kette</th><th>Name</th><th>Menge</th><th>Preis 📈</th></tr>
|
<tr><th>Kette</th><th>Name</th><th>Menge</th><th>Preis 📈</th></tr>
|
||||||
`)
|
`
|
||||||
const showHideAll = header.querySelectorAll('th:nth-child(4)')[0];
|
);
|
||||||
|
const showHideAll = header.querySelectorAll("th:nth-child(4)")[0];
|
||||||
showHideAll.style["cursor"] = "pointer";
|
showHideAll.style["cursor"] = "pointer";
|
||||||
showHideAll.showAll = true;
|
showHideAll.showAll = true;
|
||||||
showHideAll.addEventListener("click", () => {
|
showHideAll.addEventListener("click", () => {
|
||||||
table.querySelectorAll(".priceinfo").forEach(el => showHideAll.showAll ? el.classList.remove("hide") : el.classList.add("hide"));
|
table.querySelectorAll(".priceinfo").forEach((el) => (showHideAll.showAll ? el.classList.remove("hide") : el.classList.add("hide")));
|
||||||
showHideAll.showAll = !showHideAll.showAll;
|
showHideAll.showAll = !showHideAll.showAll;
|
||||||
})
|
});
|
||||||
|
|
||||||
table.appendChild(header);
|
table.appendChild(header);
|
||||||
|
|
||||||
|
@ -82,7 +85,7 @@ function showResults(items, today) {
|
||||||
item = JSON.parse(JSON.stringify(item));
|
item = JSON.parse(JSON.stringify(item));
|
||||||
if (!fullHistory) {
|
if (!fullHistory) {
|
||||||
let priceHistory = [];
|
let priceHistory = [];
|
||||||
for(let i = 0;i < item.priceHistory.length; i++) {
|
for (let i = 0; i < item.priceHistory.length; i++) {
|
||||||
priceHistory.push(item.priceHistory[i]);
|
priceHistory.push(item.priceHistory[i]);
|
||||||
if (item.priceHistory[i].date == today) break;
|
if (item.priceHistory[i].date == today) break;
|
||||||
}
|
}
|
||||||
|
@ -92,4 +95,4 @@ function showResults(items, today) {
|
||||||
document.querySelector("#results").innerText = "Resultate: " + changedItems.length;
|
document.querySelector("#results").innerText = "Resultate: " + changedItems.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|
9673
site/chart.js
9673
site/chart.js
File diff suppressed because one or more lines are too long
|
@ -1,19 +1,19 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Heisse Preise</title>
|
<title>Heisse Preise</title>
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body style="background: black;">
|
<body style="background: black">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<img src="joeh.png">
|
<img src="joeh.png" />
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,45 +1,42 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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>Heisse Preise</title>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8">
|
<div class="column" style="max-width: 1000px">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<h2>Heisse Preise</h2>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<div class="filters">
|
||||||
<title>Heisse Preise</title>
|
<a href="index.html"><strong>Produktsuche</strong></a>
|
||||||
<link
|
<a href="changes.html">Tagespreisänderungen</a>
|
||||||
rel="icon"
|
<a href="carts.html">Warenkörbe</a>
|
||||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
|
||||||
/>
|
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="column" style="max-width: 1000px">
|
|
||||||
<h2>Heisse Preise</h2>
|
|
||||||
<div class="filters">
|
|
||||||
<a href="index.html"><strong>Produktsuche</strong></a>
|
|
||||||
<a href="changes.html">Tagespreisänderungen</a>
|
|
||||||
<a href="carts.html">Warenkörbe</a>
|
|
||||||
</div>
|
|
||||||
<div id="chart" class="column hide" style="width: 80%">
|
|
||||||
<canvas></canvas>
|
|
||||||
<div class="filters" style="margin-top: 1em;">
|
|
||||||
<label><input type="checkbox" id="sum"> Preissumme Gesamt</label>
|
|
||||||
<label><input type="checkbox" id="sumstores"> Preissumme pro Kette</label>
|
|
||||||
<label><input type="checkbox" id="todayonly"> Nur heutige Preise</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div id="chart" class="column hide" style="width: 80%">
|
||||||
<label>Von <input id="start" type="date"></label>
|
<canvas></canvas>
|
||||||
<label>Bis <input id="end"type="date"></label>
|
<div class="filters" style="margin-top: 1em">
|
||||||
|
<label><input type="checkbox" id="sum" /> Preissumme Gesamt</label>
|
||||||
|
<label><input type="checkbox" id="sumstores" /> Preissumme pro Kette</label>
|
||||||
|
<label><input type="checkbox" id="todayonly" /> Nur heutige Preise</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Von <input id="start" type="date" /></label>
|
||||||
|
<label>Bis <input id="end" type="date" /></label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="search" class="column"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="search" class="column">
|
<script src="alasql.js"></script>
|
||||||
</div>
|
<script src="chart.js"></script>
|
||||||
</div>
|
<script src="utils.js"></script>
|
||||||
<script src="alasql.js"></script>
|
<script src="main.js"></script>
|
||||||
<script src="chart.js"></script>
|
</body>
|
||||||
<script src="utils.js"></script>
|
</html>
|
||||||
<script src="main.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
37
site/main.js
37
site/main.js
|
@ -1,5 +1,5 @@
|
||||||
function updateCharts(canvasDom, items) {
|
function updateCharts(canvasDom, items) {
|
||||||
const now =performance.now();
|
const now = performance.now();
|
||||||
const sum = document.querySelector("#sum").checked;
|
const sum = document.querySelector("#sum").checked;
|
||||||
const sumStores = document.querySelector("#sumstores").checked;
|
const sumStores = document.querySelector("#sumstores").checked;
|
||||||
const todayOnly = document.querySelector("#todayonly").checked;
|
const todayOnly = document.querySelector("#todayonly").checked;
|
||||||
|
@ -26,9 +26,11 @@ async function load() {
|
||||||
document.querySelector("#start").value = getOldestDate(items);
|
document.querySelector("#start").value = getOldestDate(items);
|
||||||
document.querySelector("#end").value = currentDate();
|
document.querySelector("#end").value = currentDate();
|
||||||
|
|
||||||
newSearchComponent(document.querySelector("#search"), items,
|
newSearchComponent(
|
||||||
|
document.querySelector("#search"),
|
||||||
|
items,
|
||||||
(hits) => {
|
(hits) => {
|
||||||
items.forEach(item => item.chart = false);
|
items.forEach((item) => (item.chart = false));
|
||||||
if (hits.length > 0) {
|
if (hits.length > 0) {
|
||||||
chartDom.classList.remove("hide");
|
chartDom.classList.remove("hide");
|
||||||
} else {
|
} else {
|
||||||
|
@ -40,36 +42,39 @@ async function load() {
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
(header) => {
|
(header) => {
|
||||||
header = dom("tr", `<th>Kette</th><th>Name</th><th>Menge</th><th>Preis 📈</th><th></th>`)
|
header = dom("tr", `<th>Kette</th><th>Name</th><th>Menge</th><th>Preis 📈</th><th></th>`);
|
||||||
const showHideAll = header.querySelectorAll('th:nth-child(4)')[0];
|
const showHideAll = header.querySelectorAll("th:nth-child(4)")[0];
|
||||||
showHideAll.style["cursor"] = "pointer";
|
showHideAll.style["cursor"] = "pointer";
|
||||||
showHideAll.showAll = true;
|
showHideAll.showAll = true;
|
||||||
showHideAll.addEventListener("click", () => {
|
showHideAll.addEventListener("click", () => {
|
||||||
document.querySelectorAll(".priceinfo").forEach(el => showHideAll.showAll ? el.classList.remove("hide") : el.classList.add("hide"));
|
document
|
||||||
|
.querySelectorAll(".priceinfo")
|
||||||
|
.forEach((el) => (showHideAll.showAll ? el.classList.remove("hide") : el.classList.add("hide")));
|
||||||
showHideAll.showAll = !showHideAll.showAll;
|
showHideAll.showAll = !showHideAll.showAll;
|
||||||
})
|
});
|
||||||
return header;
|
return header;
|
||||||
}, (item, itemDom, items, setQuery) => {
|
},
|
||||||
const checked = item.chart = (getQueryParameter("c") ?? []).includes(`${item.store}:${item.id}`);
|
(item, itemDom, items, setQuery) => {
|
||||||
|
const checked = (item.chart = (getQueryParameter("c") ?? []).includes(`${item.store}:${item.id}`));
|
||||||
const dataId = item.store + ":" + item.id;
|
const dataId = item.store + ":" + item.id;
|
||||||
const cell = dom("td", `<input type="checkbox" ${checked ? "checked" : ""} data-id="${dataId}">`);
|
const cell = dom("td", `<input type="checkbox" ${checked ? "checked" : ""} data-id="${dataId}">`);
|
||||||
itemDom.appendChild(cell);
|
itemDom.appendChild(cell);
|
||||||
const handleClick = (eventShouldSetQuery = false) =>{
|
const handleClick = (eventShouldSetQuery = false) => {
|
||||||
item.chart = cell.children[0].checked;
|
item.chart = cell.children[0].checked;
|
||||||
updateCharts(canvasDom, lastHits)
|
updateCharts(canvasDom, lastHits);
|
||||||
!!eventShouldSetQuery && setQuery();
|
!!eventShouldSetQuery && setQuery();
|
||||||
}
|
};
|
||||||
cell.children[0].addEventListener("click", handleClick);
|
cell.children[0].addEventListener("click", handleClick);
|
||||||
checked && handleClick();
|
checked && handleClick();
|
||||||
return itemDom;
|
return itemDom;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
const query = getQueryParameter("q");
|
const query = getQueryParameter("q");
|
||||||
if (query) {
|
if (query) {
|
||||||
|
|
||||||
document.querySelector(".search").value = query;
|
document.querySelector(".search").value = query;
|
||||||
const inputEvent = new Event('input', {
|
const inputEvent = new Event("input", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: false
|
cancelable: false,
|
||||||
});
|
});
|
||||||
document.querySelector(".search").dispatchEvent(inputEvent);
|
document.querySelector(".search").dispatchEvent(inputEvent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ label {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: .5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle--hidden {
|
.toggle--hidden {
|
||||||
|
@ -57,7 +57,7 @@ label {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
gap: .5em;
|
gap: 0.5em;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.wrapper--search {
|
.wrapper--search {
|
||||||
|
@ -88,7 +88,7 @@ label {
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper--pinned ~ .wrapper--sticky::before {
|
.wrapper--pinned ~ .wrapper--sticky::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -96,7 +96,7 @@ label {
|
||||||
max-width: 650px;
|
max-width: 650px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
opacity: .95;
|
opacity: 0.95;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
@ -106,13 +106,13 @@ label {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.toggle:checked ~ .wrapper--sticky,
|
.toggle:checked ~ .wrapper--sticky,
|
||||||
.toggle:checked ~ .wrapper--sticky * {
|
.toggle:checked ~ .wrapper--sticky * {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
}
|
}
|
||||||
.wrapper--pinned::before {
|
.wrapper--pinned::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: block;
|
display: block;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -214,7 +214,7 @@ td:nth-child(4) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchresults td:nth-child(4):before {
|
.searchresults td:nth-child(4):before {
|
||||||
content: "€ "
|
content: "€ ";
|
||||||
}
|
}
|
||||||
|
|
||||||
.querylink {
|
.querylink {
|
||||||
|
@ -230,7 +230,7 @@ td:nth-child(4) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.carts td:nth-child(3):before {
|
.carts td:nth-child(3):before {
|
||||||
content: "€ "
|
content: "€ ";
|
||||||
}
|
}
|
||||||
|
|
||||||
.carts td:nth-child(3) {
|
.carts td:nth-child(3) {
|
||||||
|
@ -288,7 +288,7 @@ td:nth-child(4) {
|
||||||
|
|
||||||
table tr {
|
table tr {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: .625em;
|
margin-bottom: 0.625em;
|
||||||
}
|
}
|
||||||
|
|
||||||
table td {
|
table td {
|
||||||
|
@ -309,7 +309,7 @@ td:nth-child(4) {
|
||||||
color: white;
|
color: white;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: .1em .4em;
|
padding: 0.1em 0.4em;
|
||||||
width: 65px;
|
width: 65px;
|
||||||
height: calc(100%);
|
height: calc(100%);
|
||||||
left: -1px;
|
left: -1px;
|
||||||
|
@ -335,4 +335,4 @@ td:nth-child(4) {
|
||||||
table td:last-child {
|
table td:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
278
site/utils.js
278
site/utils.js
|
@ -46,26 +46,13 @@ const stores = {
|
||||||
},
|
},
|
||||||
penny: {
|
penny: {
|
||||||
name: "Penny",
|
name: "Penny",
|
||||||
budgetBrands: [
|
budgetBrands: ["bravo", "echt bio!", "san fabio", "federike", "blik", "berida", "today", "ich bin österreich"],
|
||||||
"bravo",
|
|
||||||
"echt bio!",
|
|
||||||
"san fabio",
|
|
||||||
"federike",
|
|
||||||
"blik",
|
|
||||||
"berida",
|
|
||||||
"today",
|
|
||||||
"ich bin österreich",
|
|
||||||
],
|
|
||||||
color: "rgb(255, 180, 180)",
|
color: "rgb(255, 180, 180)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORE_KEYS = Object.keys(stores);
|
const STORE_KEYS = Object.keys(stores);
|
||||||
const BUDGET_BRANDS = [
|
const BUDGET_BRANDS = [...new Set([].concat(...Object.values(stores).map((store) => store.budgetBrands)))];
|
||||||
...new Set(
|
|
||||||
[].concat(...Object.values(stores).map((store) => store.budgetBrands))
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Returns the current date in ISO format
|
* @description Returns the current date in ISO format
|
||||||
|
@ -128,12 +115,7 @@ function decompress(compressedItems) {
|
||||||
const date = data[i++];
|
const date = data[i++];
|
||||||
const price = data[i++];
|
const price = data[i++];
|
||||||
prices.push({
|
prices.push({
|
||||||
date:
|
date: date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6, 8),
|
||||||
date.substring(0, 4) +
|
|
||||||
"-" +
|
|
||||||
date.substring(4, 6) +
|
|
||||||
"-" +
|
|
||||||
date.substring(6, 8),
|
|
||||||
price,
|
price,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -168,11 +150,7 @@ function decompress(compressedItems) {
|
||||||
url = "https://shop.unimarkt.at" + url;
|
url = "https://shop.unimarkt.at" + url;
|
||||||
break;
|
break;
|
||||||
case "reweDe":
|
case "reweDe":
|
||||||
url =
|
url = "https://shop.rewe.de/p/" + name.toLowerCase().replace(/ /g, "-") + "/" + id;
|
||||||
"https://shop.rewe.de/p/" +
|
|
||||||
name.toLowerCase().replace(/ /g, "-") +
|
|
||||||
"/" +
|
|
||||||
id;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,21 +178,13 @@ async function loadItems() {
|
||||||
new Promise(async (resolve) => {
|
new Promise(async (resolve) => {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`latest-canonical.${store}.compressed.json`);
|
||||||
`latest-canonical.${store}.compressed.json`
|
|
||||||
);
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
console.log(
|
console.log(`Loading compressed items for ${store} took ${(performance.now() - now) / 1000} secs`);
|
||||||
`Loading compressed items for ${store} took ${
|
|
||||||
(performance.now() - now) / 1000
|
|
||||||
} secs`
|
|
||||||
);
|
|
||||||
resolve(decompress(json));
|
resolve(decompress(json));
|
||||||
} catch {
|
} catch {
|
||||||
console.log(
|
console.log(
|
||||||
`Error while loading compressed items for ${store}. It took ${
|
`Error while loading compressed items for ${store}. It took ${(performance.now() - now) / 1000} secs, continueing...`
|
||||||
(performance.now() - now) / 1000
|
|
||||||
} secs, continueing...`
|
|
||||||
);
|
);
|
||||||
resolve([]);
|
resolve([]);
|
||||||
}
|
}
|
||||||
|
@ -222,27 +192,19 @@ async function loadItems() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const items = [].concat(...(await Promise.all(compressedItemsPerStore)));
|
const items = [].concat(...(await Promise.all(compressedItemsPerStore)));
|
||||||
console.log(
|
console.log("Loading compressed items in parallel took " + (performance.now() - now) / 1000 + " secs");
|
||||||
"Loading compressed items in parallel took " +
|
|
||||||
(performance.now() - now) / 1000 +
|
|
||||||
" secs"
|
|
||||||
);
|
|
||||||
|
|
||||||
now = performance.now();
|
now = performance.now();
|
||||||
alasql.fn.hasPriceChange = (priceHistory, date, endDate) => {
|
alasql.fn.hasPriceChange = (priceHistory, date, endDate) => {
|
||||||
if (!endDate) return priceHistory.some((price) => price.date == date);
|
if (!endDate) return priceHistory.some((price) => price.date == date);
|
||||||
else
|
else return priceHistory.some((price) => price.date >= date && price.date <= endDate);
|
||||||
return priceHistory.some(
|
|
||||||
(price) => price.date >= date && price.date <= endDate
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
item.search = item.name + " " + item.quantity + " " + item.unit;
|
item.search = item.name + " " + item.quantity + " " + item.unit;
|
||||||
item.search = item.search.toLowerCase().replace(",", ".");
|
item.search = item.search.toLowerCase().replace(",", ".");
|
||||||
|
|
||||||
item.numPrices = item.priceHistory.length;
|
item.numPrices = item.priceHistory.length;
|
||||||
item.priceOldest =
|
item.priceOldest = item.priceHistory[item.priceHistory.length - 1].price;
|
||||||
item.priceHistory[item.priceHistory.length - 1].price;
|
|
||||||
item.dateOldest = item.priceHistory[item.priceHistory.length - 1].date;
|
item.dateOldest = item.priceHistory[item.priceHistory.length - 1].date;
|
||||||
item.date = item.priceHistory[0].date;
|
item.date = item.priceHistory[0].date;
|
||||||
let highestPriceBefore = -1;
|
let highestPriceBefore = -1;
|
||||||
|
@ -261,9 +223,7 @@ async function loadItems() {
|
||||||
item.highestBefore = highestPriceBefore;
|
item.highestBefore = highestPriceBefore;
|
||||||
item.lowestBefore = lowestPriceBefore;
|
item.lowestBefore = lowestPriceBefore;
|
||||||
}
|
}
|
||||||
console.log(
|
console.log("Processing items took " + (performance.now() - now) / 1000 + " secs");
|
||||||
"Processing items took " + (performance.now() - now) / 1000 + " secs"
|
|
||||||
);
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,51 +302,31 @@ function itemToDOM(item) {
|
||||||
quantity = parseFloat((0.001 * quantity).toFixed(2));
|
quantity = parseFloat((0.001 * quantity).toFixed(2));
|
||||||
unit = unit == "ml" ? "l" : "kg";
|
unit = unit == "ml" ? "l" : "kg";
|
||||||
}
|
}
|
||||||
let unitDom = dom(
|
let unitDom = dom("td", (item.isWeighted ? "⚖ " : "") + `${quantity} ${unit}`);
|
||||||
"td",
|
|
||||||
(item.isWeighted ? "⚖ " : "") + `${quantity} ${unit}`
|
|
||||||
);
|
|
||||||
unitDom.setAttribute("data-label", "Menge");
|
unitDom.setAttribute("data-label", "Menge");
|
||||||
let increase = "";
|
let increase = "";
|
||||||
if (item.priceHistory.length > 1) {
|
if (item.priceHistory.length > 1) {
|
||||||
let percentageChange = Math.round(
|
let percentageChange = Math.round(((item.priceHistory[0].price - item.priceHistory[1].price) / item.priceHistory[1].price) * 100);
|
||||||
((item.priceHistory[0].price - item.priceHistory[1].price) /
|
increase = `<span class="${percentageChange > 0 ? "increase" : "decrease"}">${
|
||||||
item.priceHistory[1].price) *
|
|
||||||
100
|
|
||||||
);
|
|
||||||
increase = `<span class="${
|
|
||||||
percentageChange > 0 ? "increase" : "decrease"
|
|
||||||
}">${
|
|
||||||
percentageChange > 0 ? "+" + percentageChange : percentageChange
|
percentageChange > 0 ? "+" + percentageChange : percentageChange
|
||||||
}%</span>`;
|
}%</span>`;
|
||||||
}
|
}
|
||||||
let priceDomText = `${Number(item.price).toFixed(2)} ${increase} ${
|
let priceDomText = `${Number(item.price).toFixed(2)} ${increase} ${
|
||||||
item.priceHistory.length > 1
|
item.priceHistory.length > 1 ? "(" + (item.priceHistory.length - 1) + ")" : ""
|
||||||
? "(" + (item.priceHistory.length - 1) + ")"
|
|
||||||
: ""
|
|
||||||
}`;
|
}`;
|
||||||
let pricesText = "";
|
let pricesText = "";
|
||||||
for (let i = 0; i < item.priceHistory.length; i++) {
|
for (let i = 0; i < item.priceHistory.length; i++) {
|
||||||
const date = item.priceHistory[i].date;
|
const date = item.priceHistory[i].date;
|
||||||
const currPrice = item.priceHistory[i].price;
|
const currPrice = item.priceHistory[i].price;
|
||||||
const lastPrice = item.priceHistory[i + 1]
|
const lastPrice = item.priceHistory[i + 1] ? item.priceHistory[i + 1].price : currPrice;
|
||||||
? item.priceHistory[i + 1].price
|
const increase = Math.round(((currPrice - lastPrice) / lastPrice) * 100);
|
||||||
: currPrice;
|
|
||||||
const increase = Math.round(
|
|
||||||
((currPrice - lastPrice) / lastPrice) * 100
|
|
||||||
);
|
|
||||||
let priceColor = "black";
|
let priceColor = "black";
|
||||||
if (increase > 0) priceColor = "red";
|
if (increase > 0) priceColor = "red";
|
||||||
if (increase < 0) priceColor = "green";
|
if (increase < 0) priceColor = "green";
|
||||||
pricesText += `<span style="color: ${priceColor}">${date} ${currPrice} ${
|
pricesText += `<span style="color: ${priceColor}">${date} ${currPrice} ${increase > 0 ? "+" + increase : increase}%</span>`;
|
||||||
increase > 0 ? "+" + increase : increase
|
|
||||||
}%</span>`;
|
|
||||||
if (i != item.priceHistory.length - 1) pricesText += "<br>";
|
if (i != item.priceHistory.length - 1) pricesText += "<br>";
|
||||||
}
|
}
|
||||||
let priceDom = dom(
|
let priceDom = dom("td", `${priceDomText}<div class="priceinfo hide">${pricesText}</div>`);
|
||||||
"td",
|
|
||||||
`${priceDomText}<div class="priceinfo hide">${pricesText}</div>`
|
|
||||||
);
|
|
||||||
priceDom.setAttribute("data-label", "Preis");
|
priceDom.setAttribute("data-label", "Preis");
|
||||||
if (item.priceHistory.length > 1) {
|
if (item.priceHistory.length > 1) {
|
||||||
priceDom.style["cursor"] = "pointer";
|
priceDom.style["cursor"] = "pointer";
|
||||||
|
@ -410,16 +350,7 @@ function itemToDOM(item) {
|
||||||
|
|
||||||
let componentId = 0;
|
let componentId = 0;
|
||||||
|
|
||||||
function searchItems(
|
function searchItems(items, query, checkedStores, budgetBrands, minPrice, maxPrice, exact, bio) {
|
||||||
items,
|
|
||||||
query,
|
|
||||||
checkedStores,
|
|
||||||
budgetBrands,
|
|
||||||
minPrice,
|
|
||||||
maxPrice,
|
|
||||||
exact,
|
|
||||||
bio
|
|
||||||
) {
|
|
||||||
query = query.trim();
|
query = query.trim();
|
||||||
if (query.length < 3) return [];
|
if (query.length < 3) return [];
|
||||||
|
|
||||||
|
@ -428,9 +359,7 @@ function searchItems(
|
||||||
return alasql("select * from ? where " + query, [items]);
|
return alasql("select * from ? where " + query, [items]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = query
|
const tokens = query.split(/\s+/).map((token) => token.toLowerCase().replace(",", "."));
|
||||||
.split(/\s+/)
|
|
||||||
.map((token) => token.toLowerCase().replace(",", "."));
|
|
||||||
|
|
||||||
let hits = [];
|
let hits = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
@ -443,18 +372,11 @@ function searchItems(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (exact) {
|
if (exact) {
|
||||||
if (
|
if (index > 0 && item.search.charAt(index - 1) != " " && item.search.charAt(index - 1) != "-") {
|
||||||
index > 0 &&
|
|
||||||
item.search.charAt(index - 1) != " " &&
|
|
||||||
item.search.charAt(index - 1) != "-"
|
|
||||||
) {
|
|
||||||
allFound = false;
|
allFound = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (
|
if (index + token.length < item.search.length && item.search.charAt(index + token.length) != " ") {
|
||||||
index + token.length < item.search.length &&
|
|
||||||
item.search.charAt(index + token.length) != " "
|
|
||||||
) {
|
|
||||||
allFound = false;
|
allFound = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -462,17 +384,10 @@ function searchItems(
|
||||||
}
|
}
|
||||||
if (allFound) {
|
if (allFound) {
|
||||||
const name = item.name.toLowerCase();
|
const name = item.name.toLowerCase();
|
||||||
if (checkedStores.length && !checkedStores.includes(item.store))
|
if (checkedStores.length && !checkedStores.includes(item.store)) continue;
|
||||||
continue;
|
|
||||||
if (item.price < minPrice) continue;
|
if (item.price < minPrice) continue;
|
||||||
if (item.price > maxPrice) continue;
|
if (item.price > maxPrice) continue;
|
||||||
if (
|
if (budgetBrands && !BUDGET_BRANDS.some((budgetBrand) => name.indexOf(budgetBrand) >= 0)) continue;
|
||||||
budgetBrands &&
|
|
||||||
!BUDGET_BRANDS.some(
|
|
||||||
(budgetBrand) => name.indexOf(budgetBrand) >= 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue;
|
|
||||||
if (bio && !item.bio) continue;
|
if (bio && !item.bio) continue;
|
||||||
hits.push(item);
|
hits.push(item);
|
||||||
}
|
}
|
||||||
|
@ -480,14 +395,7 @@ function searchItems(
|
||||||
return hits;
|
return hits;
|
||||||
}
|
}
|
||||||
|
|
||||||
function newSearchComponent(
|
function newSearchComponent(parentElement, items, searched, filter, headerModifier, itemDomModifier) {
|
||||||
parentElement,
|
|
||||||
items,
|
|
||||||
searched,
|
|
||||||
filter,
|
|
||||||
headerModifier,
|
|
||||||
itemDomModifier
|
|
||||||
) {
|
|
||||||
let id = componentId++;
|
let id = componentId++;
|
||||||
parentElement.innerHTML = "";
|
parentElement.innerHTML = "";
|
||||||
parentElement.innerHTML = `
|
parentElement.innerHTML = `
|
||||||
|
@ -501,17 +409,14 @@ function newSearchComponent(
|
||||||
<a id="json-${id}" href="" class="hide">JSON</a>
|
<a id="json-${id}" href="" class="hide">JSON</a>
|
||||||
<div class="filters filters--store">
|
<div class="filters filters--store">
|
||||||
<label><input id="all-${id}" type="checkbox" checked="true"><strong>Alle</strong></label>
|
<label><input id="all-${id}" type="checkbox" checked="true"><strong>Alle</strong></label>
|
||||||
${STORE_KEYS.map(
|
${STORE_KEYS.map((store) => `<label><input id="${store}-${id}" type="checkbox" checked="true">${stores[store].name}</label>`).join(
|
||||||
(store) =>
|
" "
|
||||||
`<label><input id="${store}-${id}" type="checkbox" checked="true">${stores[store].name}</label>`
|
)}
|
||||||
).join(" ")}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<label>
|
<label>
|
||||||
<input id="budgetBrands-${id}" type="checkbox"> Nur
|
<input id="budgetBrands-${id}" type="checkbox"> Nur
|
||||||
<abbr title="${BUDGET_BRANDS.map((budgetBrand) =>
|
<abbr title="${BUDGET_BRANDS.map((budgetBrand) => budgetBrand.toUpperCase()).join(", ")}">
|
||||||
budgetBrand.toUpperCase()
|
|
||||||
).join(", ")}">
|
|
||||||
Diskont-Eigenmarken
|
Diskont-Eigenmarken
|
||||||
</abbr>
|
</abbr>
|
||||||
</label>
|
</label>
|
||||||
|
@ -536,7 +441,8 @@ function newSearchComponent(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const clientRect = entry.target.getBoundingClientRect();
|
const clientRect = entry.target.getBoundingClientRect();
|
||||||
if (entry.intersectionRatio < 0.999 && (clientRect.top + clientRect.height) < window.innerHeight) { // Fix Edge issue
|
if (entry.intersectionRatio < 0.999 && clientRect.top + clientRect.height < window.innerHeight) {
|
||||||
|
// Fix Edge issue
|
||||||
entry.target.classList.add("wrapper--pinned");
|
entry.target.classList.add("wrapper--pinned");
|
||||||
} else {
|
} else {
|
||||||
entry.target.classList.remove("wrapper--pinned");
|
entry.target.classList.remove("wrapper--pinned");
|
||||||
|
@ -558,9 +464,7 @@ function newSearchComponent(
|
||||||
const budgetBrands = parentElement.querySelector(`#budgetBrands-${id}`);
|
const budgetBrands = parentElement.querySelector(`#budgetBrands-${id}`);
|
||||||
const bio = parentElement.querySelector(`#bio-${id}`);
|
const bio = parentElement.querySelector(`#bio-${id}`);
|
||||||
const allCheckbox = parentElement.querySelector(`#all-${id}`);
|
const allCheckbox = parentElement.querySelector(`#all-${id}`);
|
||||||
const storeCheckboxes = STORE_KEYS.map((store) =>
|
const storeCheckboxes = STORE_KEYS.map((store) => parentElement.querySelector(`#${store}-${id}`));
|
||||||
parentElement.querySelector(`#${store}-${id}`)
|
|
||||||
);
|
|
||||||
const minPrice = parentElement.querySelector(`#minprice-${id}`);
|
const minPrice = parentElement.querySelector(`#minprice-${id}`);
|
||||||
const maxPrice = parentElement.querySelector(`#maxprice-${id}`);
|
const maxPrice = parentElement.querySelector(`#maxprice-${id}`);
|
||||||
const numResults = parentElement.querySelector(`#numresults-${id}`);
|
const numResults = parentElement.querySelector(`#numresults-${id}`);
|
||||||
|
@ -582,16 +486,9 @@ function newSearchComponent(
|
||||||
queryLink.classList.remove("hide");
|
queryLink.classList.remove("hide");
|
||||||
jsonLink.classList.remove("hide");
|
jsonLink.classList.remove("hide");
|
||||||
const inputs = [...table.querySelectorAll("input:checked")];
|
const inputs = [...table.querySelectorAll("input:checked")];
|
||||||
let checked = inputs.length
|
let checked = inputs.length ? inputs.map((item) => item.dataset.id) : getQueryParameter("c");
|
||||||
? inputs.map((item) => item.dataset.id)
|
|
||||||
: getQueryParameter("c");
|
|
||||||
if (typeof checked === "string") checked = [checked];
|
if (typeof checked === "string") checked = [checked];
|
||||||
queryLink.setAttribute(
|
queryLink.setAttribute("href", `/?q=${encodeURIComponent(query)}${checked?.length ? `&c=${checked.join("&c=")}` : ""}`);
|
||||||
"href",
|
|
||||||
`/?q=${encodeURIComponent(query)}${
|
|
||||||
checked?.length ? `&c=${checked.join("&c=")}` : ""
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let search = (query) => {
|
let search = (query) => {
|
||||||
|
@ -612,9 +509,7 @@ function newSearchComponent(
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Query: " + query + "\n" + e.message);
|
console.log("Query: " + query + "\n" + e.message);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log("Search took " + (performance.now() - now) / 1000.0 + " secs");
|
||||||
"Search took " + (performance.now() - now) / 1000.0 + " secs"
|
|
||||||
);
|
|
||||||
if (searched) hits = searched(hits);
|
if (searched) hits = searched(hits);
|
||||||
if (filter) hits = hits.filter(filter);
|
if (filter) hits = hits.filter(filter);
|
||||||
table.innerHTML = "";
|
table.innerHTML = "";
|
||||||
|
@ -622,10 +517,7 @@ function newSearchComponent(
|
||||||
numResults.innerHTML = "Resultate: 0";
|
numResults.innerHTML = "Resultate: 0";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (query.trim().charAt(0) != "!" || query.trim().toLowerCase().indexOf("order by") == -1) {
|
||||||
query.trim().charAt(0) != "!" ||
|
|
||||||
query.trim().toLowerCase().indexOf("order by") == -1
|
|
||||||
) {
|
|
||||||
if (sort.value == "priceasc") {
|
if (sort.value == "priceasc") {
|
||||||
hits.sort((a, b) => a.price - b.price);
|
hits.sort((a, b) => a.price - b.price);
|
||||||
} else if (sort.value == "pricedesc") {
|
} else if (sort.value == "pricedesc") {
|
||||||
|
@ -636,10 +528,7 @@ function newSearchComponent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let header = dom(
|
let header = dom("tr", `<th>Kette</th><th>Name</th><th>Menge</th><th>Preis</th>`);
|
||||||
"tr",
|
|
||||||
`<th>Kette</th><th>Name</th><th>Menge</th><th>Preis</th>`
|
|
||||||
);
|
|
||||||
if (headerModifier) header = headerModifier(header);
|
if (headerModifier) header = headerModifier(header);
|
||||||
const thead = dom("thead", ``);
|
const thead = dom("thead", ``);
|
||||||
thead.appendChild(header);
|
thead.appendChild(header);
|
||||||
|
@ -648,21 +537,15 @@ function newSearchComponent(
|
||||||
now = performance.now();
|
now = performance.now();
|
||||||
let num = 0;
|
let num = 0;
|
||||||
let limit = isMobile() ? 500 : 2000;
|
let limit = isMobile() ? 500 : 2000;
|
||||||
hits.every(hit => {
|
hits.every((hit) => {
|
||||||
let itemDom = itemToDOM(hit);
|
let itemDom = itemToDOM(hit);
|
||||||
if (itemDomModifier)
|
if (itemDomModifier) itemDom = itemDomModifier(hit, itemDom, hits, setQuery);
|
||||||
itemDom = itemDomModifier(hit, itemDom, hits, setQuery);
|
|
||||||
table.appendChild(itemDom);
|
table.appendChild(itemDom);
|
||||||
num++;
|
num++;
|
||||||
return num < limit;
|
return num < limit;
|
||||||
});
|
});
|
||||||
console.log(
|
console.log("Building DOM took: " + (performance.now() - now) / 1000.0 + " secs");
|
||||||
"Building DOM took: " + (performance.now() - now) / 1000.0 + " secs"
|
numResults.innerHTML = "Resultate: " + hits.length + (num < hits.length ? ", " + num + " angezeigt" : "");
|
||||||
);
|
|
||||||
numResults.innerHTML =
|
|
||||||
"Resultate: " +
|
|
||||||
hits.length +
|
|
||||||
(num < hits.length ? ", " + num + " angezeigt" : "");
|
|
||||||
lastHits = hits;
|
lastHits = hits;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -676,13 +559,9 @@ function newSearchComponent(
|
||||||
maxPrice.value = 100;
|
maxPrice.value = 100;
|
||||||
}
|
}
|
||||||
if (query?.charAt(0) == "!") {
|
if (query?.charAt(0) == "!") {
|
||||||
parentElement
|
parentElement.querySelectorAll(".filters").forEach((f) => (f.style.display = "none"));
|
||||||
.querySelectorAll(".filters")
|
|
||||||
.forEach((f) => (f.style.display = "none"));
|
|
||||||
} else {
|
} else {
|
||||||
parentElement
|
parentElement.querySelectorAll(".filters").forEach((f) => (f.style = undefined));
|
||||||
.querySelectorAll(".filters")
|
|
||||||
.forEach((f) => (f.style = undefined));
|
|
||||||
}
|
}
|
||||||
setQuery();
|
setQuery();
|
||||||
search(searchInput.value);
|
search(searchInput.value);
|
||||||
|
@ -690,10 +569,8 @@ function newSearchComponent(
|
||||||
});
|
});
|
||||||
budgetBrands.addEventListener("change", () => search(searchInput.value));
|
budgetBrands.addEventListener("change", () => search(searchInput.value));
|
||||||
bio.addEventListener("change", () => search(searchInput.value));
|
bio.addEventListener("change", () => search(searchInput.value));
|
||||||
allCheckbox.addEventListener("change", () => storeCheckboxes.forEach(store => store.checked = allCheckbox.checked));
|
allCheckbox.addEventListener("change", () => storeCheckboxes.forEach((store) => (store.checked = allCheckbox.checked)));
|
||||||
storeCheckboxes.map((store) =>
|
storeCheckboxes.map((store) => store.addEventListener("change", () => search(searchInput.value)));
|
||||||
store.addEventListener("change", () => search(searchInput.value))
|
|
||||||
);
|
|
||||||
sort.addEventListener("change", () => search(searchInput.value));
|
sort.addEventListener("change", () => search(searchInput.value));
|
||||||
minPrice.addEventListener("change", () => search(searchInput.value));
|
minPrice.addEventListener("change", () => search(searchInput.value));
|
||||||
maxPrice.addEventListener("change", () => search(searchInput.value));
|
maxPrice.addEventListener("change", () => search(searchInput.value));
|
||||||
|
@ -710,18 +587,14 @@ function showChart(canvasDom, items, chartType) {
|
||||||
canvasDom.style.display = "block";
|
canvasDom.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
const allDates = items.flatMap((product) =>
|
const allDates = items.flatMap((product) => product.priceHistory.map((item) => item.date));
|
||||||
product.priceHistory.map((item) => item.date)
|
|
||||||
);
|
|
||||||
const uniqueDates = [...new Set(allDates)];
|
const uniqueDates = [...new Set(allDates)];
|
||||||
uniqueDates.sort();
|
uniqueDates.sort();
|
||||||
|
|
||||||
const datasets = items.map((product) => {
|
const datasets = items.map((product) => {
|
||||||
let price = null;
|
let price = null;
|
||||||
const prices = uniqueDates.map((date) => {
|
const prices = uniqueDates.map((date) => {
|
||||||
const priceObj = product.priceHistory.find(
|
const priceObj = product.priceHistory.find((item) => item.date === date);
|
||||||
(item) => item.date === date
|
|
||||||
);
|
|
||||||
if (!price && priceObj) price = priceObj.price;
|
if (!price && priceObj) price = priceObj.price;
|
||||||
return priceObj ? priceObj.price : null;
|
return priceObj ? priceObj.price : null;
|
||||||
});
|
});
|
||||||
|
@ -776,26 +649,13 @@ function getOldestDate(items) {
|
||||||
return oldestDate;
|
return oldestDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCharts(
|
function showCharts(canvasDom, items, sum, sumStores, todayOnly, startDate, endDate) {
|
||||||
canvasDom,
|
|
||||||
items,
|
|
||||||
sum,
|
|
||||||
sumStores,
|
|
||||||
todayOnly,
|
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
) {
|
|
||||||
let itemsToShow = [];
|
let itemsToShow = [];
|
||||||
|
|
||||||
if (sum && items.length > 0) {
|
if (sum && items.length > 0) {
|
||||||
itemsToShow.push({
|
itemsToShow.push({
|
||||||
name: "Preissumme Warenkorb",
|
name: "Preissumme Warenkorb",
|
||||||
priceHistory: calculateOverallPriceChanges(
|
priceHistory: calculateOverallPriceChanges(items, todayOnly, startDate, endDate),
|
||||||
items,
|
|
||||||
todayOnly,
|
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -805,12 +665,7 @@ function showCharts(
|
||||||
if (storeItems.length > 0) {
|
if (storeItems.length > 0) {
|
||||||
itemsToShow.push({
|
itemsToShow.push({
|
||||||
name: "Preissumme " + store,
|
name: "Preissumme " + store,
|
||||||
priceHistory: calculateOverallPriceChanges(
|
priceHistory: calculateOverallPriceChanges(storeItems, todayOnly, startDate, endDate),
|
||||||
storeItems,
|
|
||||||
todayOnly,
|
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -822,10 +677,7 @@ function showCharts(
|
||||||
name: item.store + " " + item.name,
|
name: item.store + " " + item.name,
|
||||||
priceHistory: todayOnly
|
priceHistory: todayOnly
|
||||||
? [{ date: currentDate(), price: item.price }]
|
? [{ date: currentDate(), price: item.price }]
|
||||||
: item.priceHistory.filter(
|
: item.priceHistory.filter((price) => price.date >= startDate && price.date <= endDate),
|
||||||
(price) =>
|
|
||||||
price.date >= startDate && price.date <= endDate
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -842,21 +694,15 @@ function calculateOverallPriceChanges(items, todayOnly, startDate, endDate) {
|
||||||
return [{ date: currentDate(), price: sum }];
|
return [{ date: currentDate(), price: sum }];
|
||||||
}
|
}
|
||||||
|
|
||||||
const allDates = items.flatMap((product) =>
|
const allDates = items.flatMap((product) => product.priceHistory.map((item) => item.date));
|
||||||
product.priceHistory.map((item) => item.date)
|
|
||||||
);
|
|
||||||
let uniqueDates = [...new Set(allDates)];
|
let uniqueDates = [...new Set(allDates)];
|
||||||
uniqueDates.sort();
|
uniqueDates.sort();
|
||||||
uniqueDates = uniqueDates.filter(
|
uniqueDates = uniqueDates.filter((date) => date >= startDate && date <= endDate);
|
||||||
(date) => date >= startDate && date <= endDate
|
|
||||||
);
|
|
||||||
|
|
||||||
const allPrices = items.map((product) => {
|
const allPrices = items.map((product) => {
|
||||||
let price = null;
|
let price = null;
|
||||||
const prices = uniqueDates.map((date) => {
|
const prices = uniqueDates.map((date) => {
|
||||||
const priceObj = product.priceHistory.find(
|
const priceObj = product.priceHistory.find((item) => item.date === date);
|
||||||
(item) => item.date === date
|
|
||||||
);
|
|
||||||
if (!price && priceObj) price = priceObj.price;
|
if (!price && priceObj) price = priceObj.price;
|
||||||
return priceObj ? priceObj.price : null;
|
return priceObj ? priceObj.price : null;
|
||||||
});
|
});
|
||||||
|
@ -1247,9 +1093,7 @@ function cluster(items, maxTime) {
|
||||||
if (storeItems.length > 0) itemsPerStore.push(storeItems);
|
if (storeItems.length > 0) itemsPerStore.push(storeItems);
|
||||||
}
|
}
|
||||||
itemsPerStore.sort((a, b) => b.length - a.length);
|
itemsPerStore.sort((a, b) => b.length - a.length);
|
||||||
itemsPerStore.forEach((items) =>
|
itemsPerStore.forEach((items) => console.log(items[0].store + ", " + items.length));
|
||||||
console.log(items[0].store + ", " + items.length)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Take the store with the most items, then try to find the best match
|
// Take the store with the most items, then try to find the best match
|
||||||
// from each of the other stores
|
// from each of the other stores
|
||||||
|
@ -1294,11 +1138,7 @@ function cluster(items, maxTime) {
|
||||||
|
|
||||||
const time = (performance.now() - now) / 1000;
|
const time = (performance.now() - now) / 1000;
|
||||||
console.log(maxIterations + ", time " + time);
|
console.log(maxIterations + ", time " + time);
|
||||||
if (
|
if (JSON.stringify(clusters) == JSON.stringify(newClusters) || maxIterations == 1 || time > maxTime) {
|
||||||
JSON.stringify(clusters) == JSON.stringify(newClusters) ||
|
|
||||||
maxIterations == 1 ||
|
|
||||||
time > maxTime
|
|
||||||
) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1325,9 +1165,7 @@ function flattenClusters(clusters) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMobile() {
|
function isMobile() {
|
||||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
navigator.userAgent
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -3,49 +3,54 @@ const utils = require("./utils");
|
||||||
const HITS = Math.floor(30000 + Math.random() * 2000);
|
const HITS = Math.floor(30000 + Math.random() * 2000);
|
||||||
|
|
||||||
const conversions = {
|
const conversions = {
|
||||||
"Beutel": { unit: 'stk', factor: 1 },
|
Beutel: { unit: "stk", factor: 1 },
|
||||||
"Blatt": { unit: 'stk', factor: 1 },
|
Blatt: { unit: "stk", factor: 1 },
|
||||||
"Bund": { unit: 'stk', factor: 1 },
|
Bund: { unit: "stk", factor: 1 },
|
||||||
"g": { unit: 'g', factor: 1},
|
g: { unit: "g", factor: 1 },
|
||||||
"Gramm": { unit: 'g', factor: 1},
|
Gramm: { unit: "g", factor: 1 },
|
||||||
"kg": { unit: 'g', factor: 1000},
|
kg: { unit: "g", factor: 1000 },
|
||||||
"Kilogramm": { unit: 'g', factor: 1},
|
Kilogramm: { unit: "g", factor: 1 },
|
||||||
"l": { unit: 'ml', factor: 1000},
|
l: { unit: "ml", factor: 1000 },
|
||||||
"Liter": { unit: 'ml', factor: 1000},
|
Liter: { unit: "ml", factor: 1000 },
|
||||||
"Meter": { unit: 'cm', factor: 100},
|
Meter: { unit: "cm", factor: 100 },
|
||||||
"Milliliter": { unit: 'ml', factor: 1},
|
Milliliter: { unit: "ml", factor: 1 },
|
||||||
"ml": { unit: 'ml', factor: 1},
|
ml: { unit: "ml", factor: 1 },
|
||||||
"Paar": { unit: 'stk', factor: 1 },
|
Paar: { unit: "stk", factor: 1 },
|
||||||
"Packung": { unit: 'stk', factor: 1 },
|
Packung: { unit: "stk", factor: 1 },
|
||||||
"Portion": { unit: 'stk', factor: 1 },
|
Portion: { unit: "stk", factor: 1 },
|
||||||
"Rollen": { unit: 'stk', factor: 1 },
|
Rollen: { unit: "stk", factor: 1 },
|
||||||
"Stk": { unit: 'stk', factor: 1 },
|
Stk: { unit: "stk", factor: 1 },
|
||||||
"Stück": { unit: 'stk', factor: 1 },
|
Stück: { unit: "stk", factor: 1 },
|
||||||
"Teebeutel": { unit: 'stk', factor: 1 },
|
Teebeutel: { unit: "stk", factor: 1 },
|
||||||
"Waschgang": { unit: 'wg', factor: 1 },
|
Waschgang: { unit: "wg", factor: 1 },
|
||||||
"Zentimeter": { unit: 'cm', factor: 1 },
|
Zentimeter: { unit: "cm", factor: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCanonical = function(item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
let quantity = 1, unit = "kg";
|
let quantity = 1,
|
||||||
if(item.data.grammagePriceFactor == 1) {
|
unit = "kg";
|
||||||
const grammage = item.data.grammage !== "" && item.data.grammage.trim().split(' ').length>1 ? item.data.grammage : item.data.price.unit;
|
if (item.data.grammagePriceFactor == 1) {
|
||||||
if (grammage) [quantity, unit] = grammage.trim().split(' ').splice(0,2);
|
const grammage = item.data.grammage !== "" && item.data.grammage.trim().split(" ").length > 1 ? item.data.grammage : item.data.price.unit;
|
||||||
|
if (grammage) [quantity, unit] = grammage.trim().split(" ").splice(0, 2);
|
||||||
}
|
}
|
||||||
return utils.convertUnit({
|
return utils.convertUnit(
|
||||||
id: item.data.articleId,
|
{
|
||||||
name: item.data.name,
|
id: item.data.articleId,
|
||||||
price: item.data.price.final,
|
name: item.data.name,
|
||||||
priceHistory: [{ date: today, price: item.data.price.final }],
|
price: item.data.price.final,
|
||||||
isWeighted : item.data.isWeightArticle,
|
priceHistory: [{ date: today, price: item.data.price.final }],
|
||||||
unit,
|
isWeighted: item.data.isWeightArticle,
|
||||||
quantity,
|
unit,
|
||||||
bio: item.data.attributes && item.data.attributes.includes("s_bio"),
|
quantity,
|
||||||
url: `https://shop.billa.at${item.data.canonicalPath}`,
|
bio: item.data.attributes && item.data.attributes.includes("s_bio"),
|
||||||
}, conversions, 'billa');
|
url: `https://shop.billa.at${item.data.canonicalPath}`,
|
||||||
}
|
},
|
||||||
|
conversions,
|
||||||
|
"billa"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.fetchData = async function() {
|
exports.fetchData = async function () {
|
||||||
const BILLA_SEARCH = `https://shop.billa.at/api/search/full?searchTerm=*&storeId=00-10&pageSize=${HITS}`;
|
const BILLA_SEARCH = `https://shop.billa.at/api/search/full?searchTerm=*&storeId=00-10&pageSize=${HITS}`;
|
||||||
return (await axios.get(BILLA_SEARCH)).data.tiles;
|
return (await axios.get(BILLA_SEARCH)).data.tiles;
|
||||||
}
|
};
|
||||||
|
|
130
stores/dm-de.js
130
stores/dm-de.js
|
@ -2,92 +2,96 @@ const axios = require("axios");
|
||||||
const utils = require("./utils");
|
const utils = require("./utils");
|
||||||
|
|
||||||
const conversions = {
|
const conversions = {
|
||||||
'g': { unit: 'g', factor: 1 },
|
g: { unit: "g", factor: 1 },
|
||||||
'kg': { unit: 'g', factor: 1000 },
|
kg: { unit: "g", factor: 1000 },
|
||||||
'l': { unit: 'ml', factor: 1000 },
|
l: { unit: "ml", factor: 1000 },
|
||||||
'ml': { unit: 'ml', factor: 1 },
|
ml: { unit: "ml", factor: 1 },
|
||||||
'St': { unit: 'stk', factor: 1 },
|
St: { unit: "stk", factor: 1 },
|
||||||
'Wl': { unit: 'wg', factor: 1 },
|
Wl: { unit: "wg", factor: 1 },
|
||||||
'm': { unit: 'cm', factor: 100 },
|
m: { unit: "cm", factor: 100 },
|
||||||
'mm': { unit: 'cm', factor: .1 },
|
mm: { unit: "cm", factor: 0.1 },
|
||||||
'Bl': { unit: 'stk', factor: 1 },
|
Bl: { unit: "stk", factor: 1 },
|
||||||
'Btl': { unit: 'stk', factor: 1 },
|
Btl: { unit: "stk", factor: 1 },
|
||||||
'Paar': { unit: 'stk', factor: 1 },
|
Paar: { unit: "stk", factor: 1 },
|
||||||
'Portion': { unit: 'stk', factor: 1 },
|
Portion: { unit: "stk", factor: 1 },
|
||||||
'Satz': { unit: 'stk', factor: 1 },
|
Satz: { unit: "stk", factor: 1 },
|
||||||
'Tablette': { unit: 'stk', factor: 1 },
|
Tablette: { unit: "stk", factor: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCanonical = function(item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
let quantity = item.netQuantityContent || item.basePriceQuantity;
|
let quantity = item.netQuantityContent || item.basePriceQuantity;
|
||||||
let unit = item.contentUnit || item.basePriceUnit;
|
let unit = item.contentUnit || item.basePriceUnit;
|
||||||
return utils.convertUnit({
|
return utils.convertUnit(
|
||||||
id: item.gtin,
|
{
|
||||||
name: `${item.brandName} ${item.title}`,
|
id: item.gtin,
|
||||||
price: item.price.value,
|
name: `${item.brandName} ${item.title}`,
|
||||||
priceHistory: [{ date: today, price: item.price.value }],
|
price: item.price.value,
|
||||||
unit,
|
priceHistory: [{ date: today, price: item.price.value }],
|
||||||
quantity,
|
unit,
|
||||||
...(item.brandName === "dmBio" || (item.name ? (item.name.startsWith("Bio ") | item.name.startsWith("Bio-")) : false)) && {bio: true},
|
quantity,
|
||||||
url: `https://www.dm.de/product-p${item.gtin}.html`,
|
...((item.brandName === "dmBio" || (item.name ? item.name.startsWith("Bio ") | item.name.startsWith("Bio-") : false)) && { bio: true }),
|
||||||
}, conversions, 'dmDe');
|
url: `https://www.dm.de/product-p${item.gtin}.html`,
|
||||||
}
|
},
|
||||||
|
conversions,
|
||||||
|
"dmDe"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.fetchData = async function() {
|
exports.fetchData = async function () {
|
||||||
const DM_BASE_URL = `https://product-search.services.dmtech.com/de/search/crawl?pageSize=1000&`
|
const DM_BASE_URL = `https://product-search.services.dmtech.com/de/search/crawl?pageSize=1000&`;
|
||||||
const QUERIES = [
|
const QUERIES = [
|
||||||
'allCategories.id=010000&price.value.to=2', //~500 items
|
"allCategories.id=010000&price.value.to=2", //~500 items
|
||||||
'allCategories.id=010000&price.value.from=2&price.value.to=3', //~600 items
|
"allCategories.id=010000&price.value.from=2&price.value.to=3", //~600 items
|
||||||
'allCategories.id=010000&price.value.from=3&price.value.to=4', //~500 items
|
"allCategories.id=010000&price.value.from=3&price.value.to=4", //~500 items
|
||||||
'allCategories.id=010000&price.value.from=4&price.value.to=7', //~800 items
|
"allCategories.id=010000&price.value.from=4&price.value.to=7", //~800 items
|
||||||
'allCategories.id=010000&price.value.from=7&price.value.to=10', //~900 items
|
"allCategories.id=010000&price.value.from=7&price.value.to=10", //~900 items
|
||||||
'allCategories.id=010000&price.value.from=10&price.value.to=15', //~900 items
|
"allCategories.id=010000&price.value.from=10&price.value.to=15", //~900 items
|
||||||
'allCategories.id=010000&price.value.from=15', //~300 items
|
"allCategories.id=010000&price.value.from=15", //~300 items
|
||||||
'allCategories.id=020000&price.value.to=2', //~600 items
|
"allCategories.id=020000&price.value.to=2", //~600 items
|
||||||
'allCategories.id=020000&price.value.from=2&price.value.to=3', //~550 items
|
"allCategories.id=020000&price.value.from=2&price.value.to=3", //~550 items
|
||||||
'allCategories.id=020000&price.value.from=3&price.value.to=4', //~600 items
|
"allCategories.id=020000&price.value.from=3&price.value.to=4", //~600 items
|
||||||
'allCategories.id=020000&price.value.from=4&price.value.to=6', //~800 items
|
"allCategories.id=020000&price.value.from=4&price.value.to=6", //~800 items
|
||||||
'allCategories.id=020000&price.value.from=6&price.value.to=10', //~850 items
|
"allCategories.id=020000&price.value.from=6&price.value.to=10", //~850 items
|
||||||
'allCategories.id=020000&price.value.from=10&price.value.to=18', //~900 items
|
"allCategories.id=020000&price.value.from=10&price.value.to=18", //~900 items
|
||||||
'allCategories.id=020000&price.value.from=18', //~960 items (!)
|
"allCategories.id=020000&price.value.from=18", //~960 items (!)
|
||||||
'allCategories.id=030000&price.value.to=8', //~900 items
|
"allCategories.id=030000&price.value.to=8", //~900 items
|
||||||
'allCategories.id=030000&price.value.from=8', //~500 items
|
"allCategories.id=030000&price.value.from=8", //~500 items
|
||||||
'allCategories.id=040000&price.value.to=2', //~600 items
|
"allCategories.id=040000&price.value.to=2", //~600 items
|
||||||
'allCategories.id=040000&price.value.from=2&price.value.to=4', //~900 items
|
"allCategories.id=040000&price.value.from=2&price.value.to=4", //~900 items
|
||||||
'allCategories.id=040000&price.value.from=4', //~400 items
|
"allCategories.id=040000&price.value.from=4", //~400 items
|
||||||
'allCategories.id=050000&price.value.to=4', //~600 items
|
"allCategories.id=050000&price.value.to=4", //~600 items
|
||||||
'allCategories.id=050000&price.value.from=4', //~800 items
|
"allCategories.id=050000&price.value.from=4", //~800 items
|
||||||
'allCategories.id=060000&price.value.to=4', //~900 items
|
"allCategories.id=060000&price.value.to=4", //~900 items
|
||||||
'allCategories.id=060000&price.value.from=4', //~500 items
|
"allCategories.id=060000&price.value.from=4", //~500 items
|
||||||
'allCategories.id=070000', //~300 items
|
"allCategories.id=070000", //~300 items
|
||||||
]
|
];
|
||||||
|
|
||||||
let dmItems = [];
|
let dmItems = [];
|
||||||
for (let query of QUERIES) {
|
for (let query of QUERIES) {
|
||||||
var res = (await axios.get(DM_BASE_URL + query, {
|
var res = await axios.get(DM_BASE_URL + query, {
|
||||||
validateStatus: function (status) {
|
validateStatus: function (status) {
|
||||||
return (status >= 200 && status < 300) || status == 429;
|
return (status >= 200 && status < 300) || status == 429;
|
||||||
}
|
},
|
||||||
}));
|
});
|
||||||
|
|
||||||
// exponential backoff
|
// exponential backoff
|
||||||
backoff = 2000;
|
backoff = 2000;
|
||||||
while (res.status == 429) {
|
while (res.status == 429) {
|
||||||
console.info(`DM-DE API returned 429, retrying in ${backoff/1000}s.`);
|
console.info(`DM-DE API returned 429, retrying in ${backoff / 1000}s.`);
|
||||||
await new Promise(resolve => setTimeout(resolve, backoff));
|
await new Promise((resolve) => setTimeout(resolve, backoff));
|
||||||
backoff *= 2;
|
backoff *= 2;
|
||||||
res = (await axios.get(DM_BASE_URL + query, {
|
res = await axios.get(DM_BASE_URL + query, {
|
||||||
validateStatus: function (status) {
|
validateStatus: function (status) {
|
||||||
return (status >= 200 && status < 300) || status == 429;
|
return (status >= 200 && status < 300) || status == 429;
|
||||||
}
|
},
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
let items = res.data;
|
let items = res.data;
|
||||||
if (items.count > 1000) {
|
if (items.count > 1000) {
|
||||||
console.warn(`DM-DE Query returned more than 1000 items! Items may be missing. Adjust queries. Query: ${query}`);
|
console.warn(`DM-DE Query returned more than 1000 items! Items may be missing. Adjust queries. Query: ${query}`);
|
||||||
}
|
}
|
||||||
dmItems = dmItems.concat(items.products);
|
dmItems = dmItems.concat(items.products);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
return dmItems;
|
return dmItems;
|
||||||
}
|
};
|
||||||
|
|
130
stores/dm.js
130
stores/dm.js
|
@ -2,92 +2,96 @@ const axios = require("axios");
|
||||||
const utils = require("./utils");
|
const utils = require("./utils");
|
||||||
|
|
||||||
const conversions = {
|
const conversions = {
|
||||||
'g': { unit: 'g', factor: 1 },
|
g: { unit: "g", factor: 1 },
|
||||||
'kg': { unit: 'g', factor: 1000 },
|
kg: { unit: "g", factor: 1000 },
|
||||||
'l': { unit: 'ml', factor: 1000 },
|
l: { unit: "ml", factor: 1000 },
|
||||||
'ml': { unit: 'ml', factor: 1 },
|
ml: { unit: "ml", factor: 1 },
|
||||||
'St': { unit: 'stk', factor: 1 },
|
St: { unit: "stk", factor: 1 },
|
||||||
'Wl': { unit: 'wg', factor: 1 },
|
Wl: { unit: "wg", factor: 1 },
|
||||||
'm': { unit: 'cm', factor: 100 },
|
m: { unit: "cm", factor: 100 },
|
||||||
'mm': { unit: 'cm', factor: .1 },
|
mm: { unit: "cm", factor: 0.1 },
|
||||||
'Bl': { unit: 'stk', factor: 1 },
|
Bl: { unit: "stk", factor: 1 },
|
||||||
'Btl': { unit: 'stk', factor: 1 },
|
Btl: { unit: "stk", factor: 1 },
|
||||||
'Paar': { unit: 'stk', factor: 1 },
|
Paar: { unit: "stk", factor: 1 },
|
||||||
'Portion': { unit: 'stk', factor: 1 },
|
Portion: { unit: "stk", factor: 1 },
|
||||||
'Satz': { unit: 'stk', factor: 1 },
|
Satz: { unit: "stk", factor: 1 },
|
||||||
'Tablette': { unit: 'stk', factor: 1 },
|
Tablette: { unit: "stk", factor: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCanonical = function(item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
let quantity = item.netQuantityContent || item.basePriceQuantity;
|
let quantity = item.netQuantityContent || item.basePriceQuantity;
|
||||||
let unit = item.contentUnit || item.basePriceUnit;
|
let unit = item.contentUnit || item.basePriceUnit;
|
||||||
return utils.convertUnit({
|
return utils.convertUnit(
|
||||||
id: item.gtin,
|
{
|
||||||
name: `${item.brandName} ${item.title}`,
|
id: item.gtin,
|
||||||
price: item.price.value,
|
name: `${item.brandName} ${item.title}`,
|
||||||
priceHistory: [{ date: today, price: item.price.value }],
|
price: item.price.value,
|
||||||
unit,
|
priceHistory: [{ date: today, price: item.price.value }],
|
||||||
quantity,
|
unit,
|
||||||
...(item.brandName === "dmBio" || (item.name ? (item.name.startsWith("Bio ") | item.name.startsWith("Bio-")) : false)) && {bio: true},
|
quantity,
|
||||||
url: `https://www.dm.at/product-p${item.gtin}.html`,
|
...((item.brandName === "dmBio" || (item.name ? item.name.startsWith("Bio ") | item.name.startsWith("Bio-") : false)) && { bio: true }),
|
||||||
}, conversions, 'dm');
|
url: `https://www.dm.at/product-p${item.gtin}.html`,
|
||||||
}
|
},
|
||||||
|
conversions,
|
||||||
|
"dm"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.fetchData = async function() {
|
exports.fetchData = async function () {
|
||||||
const DM_BASE_URL = `https://product-search.services.dmtech.com/at/search/crawl?pageSize=1000&`
|
const DM_BASE_URL = `https://product-search.services.dmtech.com/at/search/crawl?pageSize=1000&`;
|
||||||
const QUERIES = [
|
const QUERIES = [
|
||||||
'allCategories.id=010000&price.value.to=2', //~500 items
|
"allCategories.id=010000&price.value.to=2", //~500 items
|
||||||
'allCategories.id=010000&price.value.from=2&price.value.to=3', //~600 items
|
"allCategories.id=010000&price.value.from=2&price.value.to=3", //~600 items
|
||||||
'allCategories.id=010000&price.value.from=3&price.value.to=4', //~500 items
|
"allCategories.id=010000&price.value.from=3&price.value.to=4", //~500 items
|
||||||
'allCategories.id=010000&price.value.from=4&price.value.to=7', //~800 items
|
"allCategories.id=010000&price.value.from=4&price.value.to=7", //~800 items
|
||||||
'allCategories.id=010000&price.value.from=7&price.value.to=10', //~900 items
|
"allCategories.id=010000&price.value.from=7&price.value.to=10", //~900 items
|
||||||
'allCategories.id=010000&price.value.from=10&price.value.to=15', //~900 items
|
"allCategories.id=010000&price.value.from=10&price.value.to=15", //~900 items
|
||||||
'allCategories.id=010000&price.value.from=15', //~300 items
|
"allCategories.id=010000&price.value.from=15", //~300 items
|
||||||
'allCategories.id=020000&price.value.to=2', //~600 items
|
"allCategories.id=020000&price.value.to=2", //~600 items
|
||||||
'allCategories.id=020000&price.value.from=2&price.value.to=3', //~550 items
|
"allCategories.id=020000&price.value.from=2&price.value.to=3", //~550 items
|
||||||
'allCategories.id=020000&price.value.from=3&price.value.to=4', //~600 items
|
"allCategories.id=020000&price.value.from=3&price.value.to=4", //~600 items
|
||||||
'allCategories.id=020000&price.value.from=4&price.value.to=6', //~800 items
|
"allCategories.id=020000&price.value.from=4&price.value.to=6", //~800 items
|
||||||
'allCategories.id=020000&price.value.from=6&price.value.to=10', //~850 items
|
"allCategories.id=020000&price.value.from=6&price.value.to=10", //~850 items
|
||||||
'allCategories.id=020000&price.value.from=10&price.value.to=18', //~900 items
|
"allCategories.id=020000&price.value.from=10&price.value.to=18", //~900 items
|
||||||
'allCategories.id=020000&price.value.from=18', //~960 items (!)
|
"allCategories.id=020000&price.value.from=18", //~960 items (!)
|
||||||
'allCategories.id=030000&price.value.to=8', //~900 items
|
"allCategories.id=030000&price.value.to=8", //~900 items
|
||||||
'allCategories.id=030000&price.value.from=8', //~500 items
|
"allCategories.id=030000&price.value.from=8", //~500 items
|
||||||
'allCategories.id=040000&price.value.to=2', //~600 items
|
"allCategories.id=040000&price.value.to=2", //~600 items
|
||||||
'allCategories.id=040000&price.value.from=2&price.value.to=4', //~900 items
|
"allCategories.id=040000&price.value.from=2&price.value.to=4", //~900 items
|
||||||
'allCategories.id=040000&price.value.from=4', //~400 items
|
"allCategories.id=040000&price.value.from=4", //~400 items
|
||||||
'allCategories.id=050000&price.value.to=4', //~600 items
|
"allCategories.id=050000&price.value.to=4", //~600 items
|
||||||
'allCategories.id=050000&price.value.from=4', //~800 items
|
"allCategories.id=050000&price.value.from=4", //~800 items
|
||||||
'allCategories.id=060000&price.value.to=4', //~900 items
|
"allCategories.id=060000&price.value.to=4", //~900 items
|
||||||
'allCategories.id=060000&price.value.from=4', //~500 items
|
"allCategories.id=060000&price.value.from=4", //~500 items
|
||||||
'allCategories.id=070000', //~300 items
|
"allCategories.id=070000", //~300 items
|
||||||
]
|
];
|
||||||
|
|
||||||
let dmItems = [];
|
let dmItems = [];
|
||||||
for (let query of QUERIES) {
|
for (let query of QUERIES) {
|
||||||
var res = (await axios.get(DM_BASE_URL + query, {
|
var res = await axios.get(DM_BASE_URL + query, {
|
||||||
validateStatus: function (status) {
|
validateStatus: function (status) {
|
||||||
return (status >= 200 && status < 300) || status == 429;
|
return (status >= 200 && status < 300) || status == 429;
|
||||||
}
|
},
|
||||||
}));
|
});
|
||||||
|
|
||||||
// exponential backoff
|
// exponential backoff
|
||||||
backoff = 2000;
|
backoff = 2000;
|
||||||
while (res.status == 429) {
|
while (res.status == 429) {
|
||||||
console.info(`DM API returned 429, retrying in ${backoff/1000}s.`);
|
console.info(`DM API returned 429, retrying in ${backoff / 1000}s.`);
|
||||||
await new Promise(resolve => setTimeout(resolve, backoff));
|
await new Promise((resolve) => setTimeout(resolve, backoff));
|
||||||
backoff *= 2;
|
backoff *= 2;
|
||||||
res = (await axios.get(DM_BASE_URL + query, {
|
res = await axios.get(DM_BASE_URL + query, {
|
||||||
validateStatus: function (status) {
|
validateStatus: function (status) {
|
||||||
return (status >= 200 && status < 300) || status == 429;
|
return (status >= 200 && status < 300) || status == 429;
|
||||||
}
|
},
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
let items = res.data;
|
let items = res.data;
|
||||||
if (items.count > 1000) {
|
if (items.count > 1000) {
|
||||||
console.warn(`Query returned more than 1000 items! Items may be missing. Adjust queries. Query: ${query}`);
|
console.warn(`Query returned more than 1000 items! Items may be missing. Adjust queries. Query: ${query}`);
|
||||||
}
|
}
|
||||||
dmItems = dmItems.concat(items.products);
|
dmItems = dmItems.concat(items.products);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
return dmItems;
|
return dmItems;
|
||||||
}
|
};
|
||||||
|
|
|
@ -2,53 +2,68 @@ const axios = require("axios");
|
||||||
const utils = require("./utils");
|
const utils = require("./utils");
|
||||||
|
|
||||||
const conversions = {
|
const conversions = {
|
||||||
"": {unit: "stk", factor: 1},
|
"": { unit: "stk", factor: 1 },
|
||||||
"blatt": {unit: "stk", factor: 1},
|
blatt: { unit: "stk", factor: 1 },
|
||||||
"g": {unit: "g", factor: 1},
|
g: { unit: "g", factor: 1 },
|
||||||
"gg": {unit: "g", factor: 1},
|
gg: { unit: "g", factor: 1 },
|
||||||
"gramm": {unit: "g", factor: 1},
|
gramm: { unit: "g", factor: 1 },
|
||||||
"kg": {unit: "g", factor: 1000},
|
kg: { unit: "g", factor: 1000 },
|
||||||
"cl": {unit: "ml", factor: 100},
|
cl: { unit: "ml", factor: 100 },
|
||||||
"l": {unit: "ml", factor: 1000},
|
l: { unit: "ml", factor: 1000 },
|
||||||
"ml": {unit: "ml", factor: 1},
|
ml: { unit: "ml", factor: 1 },
|
||||||
"paar": {unit: "stk", factor: 1},
|
paar: { unit: "stk", factor: 1 },
|
||||||
"stk.": {unit: "stk", factor: 1},
|
"stk.": { unit: "stk", factor: 1 },
|
||||||
"stück": {unit: "stk", factor: 1},
|
stück: { unit: "stk", factor: 1 },
|
||||||
"er": {unit: "stk", factor: 1},
|
er: { unit: "stk", factor: 1 },
|
||||||
"teebeutel": {unit: "stk", factor: 1},
|
teebeutel: { unit: "stk", factor: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCanonical = function(item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
// try to read quantity and unit from product name
|
// try to read quantity and unit from product name
|
||||||
const name = item.ProductName;
|
const name = item.ProductName;
|
||||||
let [quantity, unit] = utils.parseUnitAndQuantityAtEnd(name);
|
let [quantity, unit] = utils.parseUnitAndQuantityAtEnd(name);
|
||||||
if(conversions[unit] === undefined) {
|
if (conversions[unit] === undefined) {
|
||||||
// fallback: use given quantity and unit (including packaging)
|
// fallback: use given quantity and unit (including packaging)
|
||||||
quantity = item.Unit
|
quantity = item.Unit;
|
||||||
unit= item.UnitType
|
unit = item.UnitType;
|
||||||
}
|
}
|
||||||
return utils.convertUnit({
|
return utils.convertUnit(
|
||||||
id: item.ProductID,
|
{
|
||||||
name,
|
id: item.ProductID,
|
||||||
price: item.Price,
|
name,
|
||||||
priceHistory: [{ date: today, price: item.Price }],
|
price: item.Price,
|
||||||
isWeighted: item.IsBulk,
|
priceHistory: [{ date: today, price: item.Price }],
|
||||||
unit,
|
isWeighted: item.IsBulk,
|
||||||
quantity,
|
unit,
|
||||||
bio: item.IsBio,
|
quantity,
|
||||||
url: `https://www.roksh.at/hofer/produkte/${item.CategorySEOName}/${item.SEOName}`
|
bio: item.IsBio,
|
||||||
}, conversions, 'hofer');
|
url: `https://www.roksh.at/hofer/produkte/${item.CategorySEOName}/${item.SEOName}`,
|
||||||
}
|
},
|
||||||
|
conversions,
|
||||||
|
"hofer"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.fetchData = async function() {
|
exports.fetchData = async function () {
|
||||||
const HOFER_BASE_URL = `https://shopservice.roksh.at`
|
const HOFER_BASE_URL = `https://shopservice.roksh.at`;
|
||||||
const CATEGORIES = HOFER_BASE_URL + `/category/GetFullCategoryList/`
|
const CATEGORIES = HOFER_BASE_URL + `/category/GetFullCategoryList/`;
|
||||||
const CONFIG = { headers: { authorization: null } }
|
const CONFIG = { headers: { authorization: null } };
|
||||||
const ITEMS = HOFER_BASE_URL + `/productlist/CategoryProductList`
|
const ITEMS = HOFER_BASE_URL + `/productlist/CategoryProductList`;
|
||||||
|
|
||||||
// fetch access token
|
// fetch access token
|
||||||
const token_data = { "OwnWebshopProviderCode": "", "SetUserSelectedShopsOnFirstSiteLoad": true, "RedirectToDashboardNeeded": false, "ShopsSelectedForRoot": "hofer", "BrandProviderSelectedForRoot": null, "UserSelectedShops": [] }
|
const token_data = {
|
||||||
const token = (await axios.post("https://shopservice.roksh.at/session/configure", token_data, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } })).headers['jwt-auth'];
|
OwnWebshopProviderCode: "",
|
||||||
|
SetUserSelectedShopsOnFirstSiteLoad: true,
|
||||||
|
RedirectToDashboardNeeded: false,
|
||||||
|
ShopsSelectedForRoot: "hofer",
|
||||||
|
BrandProviderSelectedForRoot: null,
|
||||||
|
UserSelectedShops: [],
|
||||||
|
};
|
||||||
|
const token = (
|
||||||
|
await axios.post("https://shopservice.roksh.at/session/configure", token_data, {
|
||||||
|
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
).headers["jwt-auth"];
|
||||||
CONFIG.headers.authorization = "Bearer " + token;
|
CONFIG.headers.authorization = "Bearer " + token;
|
||||||
|
|
||||||
// concat all subcategories (categories.[i].ChildList)
|
// concat all subcategories (categories.[i].ChildList)
|
||||||
|
@ -57,14 +72,17 @@ exports.fetchData = async function() {
|
||||||
|
|
||||||
let hoferItems = [];
|
let hoferItems = [];
|
||||||
for (let subCategory of subCategories) {
|
for (let subCategory of subCategories) {
|
||||||
let categoryData = (await axios.get(`${ITEMS}?progId=${subCategory.ProgID}&firstLoadProductListResultNum=4&listResultProductNum=24`, CONFIG)).data;
|
let categoryData = (await axios.get(`${ITEMS}?progId=${subCategory.ProgID}&firstLoadProductListResultNum=4&listResultProductNum=24`, CONFIG))
|
||||||
|
.data;
|
||||||
const numPages = categoryData.ProductListResults[0].ListContext.TotalPages;
|
const numPages = categoryData.ProductListResults[0].ListContext.TotalPages;
|
||||||
|
|
||||||
for (let iPage = 1; iPage <= numPages; iPage++) {
|
for (let iPage = 1; iPage <= numPages; iPage++) {
|
||||||
let items = (await axios.post(`${HOFER_BASE_URL}/productlist/GetProductList`, { CategoryProgId: subCategory.ProgID, Page: iPage }, CONFIG)).data;
|
let items = (
|
||||||
|
await axios.post(`${HOFER_BASE_URL}/productlist/GetProductList`, { CategoryProgId: subCategory.ProgID, Page: iPage }, CONFIG)
|
||||||
|
).data;
|
||||||
hoferItems = hoferItems.concat(items.ProductList);
|
hoferItems = hoferItems.concat(items.ProductList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return hoferItems;
|
return hoferItems;
|
||||||
}
|
};
|
||||||
|
|
|
@ -7,4 +7,4 @@ exports.mpreis = require("./mpreis");
|
||||||
exports.spar = require("./spar");
|
exports.spar = require("./spar");
|
||||||
exports.unimarkt = require("./unimarkt");
|
exports.unimarkt = require("./unimarkt");
|
||||||
exports.reweDe = require("./rewe-de");
|
exports.reweDe = require("./rewe-de");
|
||||||
exports.penny = require("./penny");
|
exports.penny = require("./penny");
|
||||||
|
|
|
@ -4,58 +4,59 @@ const utils = require("./utils");
|
||||||
const HITS = Math.floor(30000 + Math.random() * 2000);
|
const HITS = Math.floor(30000 + Math.random() * 2000);
|
||||||
|
|
||||||
const conversions = {
|
const conversions = {
|
||||||
"": {unit: "stk", factor: 1},
|
"": { unit: "stk", factor: 1 },
|
||||||
"dosen": {unit: "stk", factor: 1},
|
dosen: { unit: "stk", factor: 1 },
|
||||||
"blatt": {unit: "stk", factor: 1},
|
blatt: { unit: "stk", factor: 1 },
|
||||||
"flaschen": {unit: "stk", factor: 1},
|
flaschen: { unit: "stk", factor: 1 },
|
||||||
"l": {unit: "ml", factor: 1000},
|
l: { unit: "ml", factor: 1000 },
|
||||||
"liter": {unit: "ml", factor: 1000},
|
liter: { unit: "ml", factor: 1000 },
|
||||||
"ml": {unit: "ml", factor: 1},
|
ml: { unit: "ml", factor: 1 },
|
||||||
"g": {unit: "g", factor: 1},
|
g: { unit: "g", factor: 1 },
|
||||||
"kg": {unit: "g", factor: 1000},
|
kg: { unit: "g", factor: 1000 },
|
||||||
"stk.": {unit: "stk", factor: 1},
|
"stk.": { unit: "stk", factor: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCanonical = function(item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
let quantity = 1;
|
let quantity = 1;
|
||||||
let unit = '';
|
let unit = "";
|
||||||
let text = (item.price.basePrice?.text ?? "").trim().split('(')[0].replaceAll(',', '.').toLowerCase();
|
let text = (item.price.basePrice?.text ?? "").trim().split("(")[0].replaceAll(",", ".").toLowerCase();
|
||||||
let isWeighted = false;
|
let isWeighted = false;
|
||||||
|
|
||||||
if(text === 'per kg') {
|
if (text === "per kg") {
|
||||||
isWeighted = true;
|
isWeighted = true;
|
||||||
unit = 'kg';
|
unit = "kg";
|
||||||
}
|
} else {
|
||||||
else {
|
if (text.startsWith("bei") && text.search("je ") != -1) text = text.substr(text.search("je "));
|
||||||
if(text.startsWith('bei') && text.search('je ') != -1)
|
|
||||||
text = text.substr(text.search('je '))
|
|
||||||
|
|
||||||
for (let s of ['ab ', 'je ', 'ca. ', 'z.b.: ', 'z.b. '])
|
for (let s of ["ab ", "je ", "ca. ", "z.b.: ", "z.b. "]) text = text.replace(s, "").trim();
|
||||||
text = text.replace(s, '').trim()
|
|
||||||
|
|
||||||
const regex = /^([0-9.x ]+)(.*)$/;
|
const regex = /^([0-9.x ]+)(.*)$/;
|
||||||
const matches = text.match(regex);
|
const matches = text.match(regex);
|
||||||
if(matches) {
|
if (matches) {
|
||||||
matches[1].split('x').forEach( (q)=> {
|
matches[1].split("x").forEach((q) => {
|
||||||
quantity = quantity * parseFloat(q.split('/')[0])
|
quantity = quantity * parseFloat(q.split("/")[0]);
|
||||||
})
|
});
|
||||||
unit = matches[2].split('/')[0].trim().split(' ')[0];
|
unit = matches[2].split("/")[0].trim().split(" ")[0];
|
||||||
}
|
}
|
||||||
unit = unit.split('-')[0];
|
unit = unit.split("-")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.convertUnit({
|
return utils.convertUnit(
|
||||||
id: item.productId,
|
{
|
||||||
name: `${item.keyfacts?.supplementalDescription?.concat(" ") ?? ""}${item.fullTitle}`,
|
id: item.productId,
|
||||||
price: item.price.price,
|
name: `${item.keyfacts?.supplementalDescription?.concat(" ") ?? ""}${item.fullTitle}`,
|
||||||
priceHistory: [{ date: today, price: item.price.price }],
|
price: item.price.price,
|
||||||
unit,
|
priceHistory: [{ date: today, price: item.price.price }],
|
||||||
quantity,
|
unit,
|
||||||
url: `https://www.lidl.at${item.canonicalUrl}`,
|
quantity,
|
||||||
}, conversions, 'lidl');
|
url: `https://www.lidl.at${item.canonicalUrl}`,
|
||||||
}
|
},
|
||||||
|
conversions,
|
||||||
|
"lidl"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.fetchData = async function() {
|
exports.fetchData = async function () {
|
||||||
const LIDL_SEARCH = `https://www.lidl.at/p/api/gridboxes/AT/de/?max=${HITS}`;
|
const LIDL_SEARCH = `https://www.lidl.at/p/api/gridboxes/AT/de/?max=${HITS}`;
|
||||||
return (await axios.get(LIDL_SEARCH)).data.filter(item => !!item.price.price);
|
return (await axios.get(LIDL_SEARCH)).data.filter((item) => !!item.price.price);
|
||||||
}
|
};
|
||||||
|
|
|
@ -2,57 +2,61 @@ const axios = require("axios");
|
||||||
const utils = require("./utils");
|
const utils = require("./utils");
|
||||||
|
|
||||||
const conversions = {
|
const conversions = {
|
||||||
'cm': { unit: 'cm', factor: 1 },
|
cm: { unit: "cm", factor: 1 },
|
||||||
'dag': { unit: 'g', factor: 10 },
|
dag: { unit: "g", factor: 10 },
|
||||||
'dl': { unit: 'ml', factor: 10 },
|
dl: { unit: "ml", factor: 10 },
|
||||||
'grm': { unit: 'g', factor: 1 },
|
grm: { unit: "g", factor: 1 },
|
||||||
'kgm': { unit: 'g', factor: 1000 },
|
kgm: { unit: "g", factor: 1000 },
|
||||||
'ltr': { unit: 'ml', factor: 1000 },
|
ltr: { unit: "ml", factor: 1000 },
|
||||||
'mlt': { unit: 'ml', factor: 1 },
|
mlt: { unit: "ml", factor: 1 },
|
||||||
'mtr': { unit: 'm', factor: 1 },
|
mtr: { unit: "m", factor: 1 },
|
||||||
'stk': { unit: 'stk', factor: 1 },
|
stk: { unit: "stk", factor: 1 },
|
||||||
'stk.': { unit: 'stk', factor: 1 },
|
"stk.": { unit: "stk", factor: 1 },
|
||||||
'g': { unit: 'g', factor: 1 },
|
g: { unit: "g", factor: 1 },
|
||||||
'anw': { unit: 'stk', factor: 1 },
|
anw: { unit: "stk", factor: 1 },
|
||||||
'l': { unit: 'ml', factor: 1000 },
|
l: { unit: "ml", factor: 1000 },
|
||||||
'm': { unit: 'cm', factor: 100 },
|
m: { unit: "cm", factor: 100 },
|
||||||
'ml': { unit: 'ml', factor: 1 },
|
ml: { unit: "ml", factor: 1 },
|
||||||
'kg': { unit: 'g', factor: 1000 },
|
kg: { unit: "g", factor: 1000 },
|
||||||
'paar': { unit: 'stk', factor: 1 },
|
paar: { unit: "stk", factor: 1 },
|
||||||
'stück': { unit: 'stk', factor: 1 },
|
stück: { unit: "stk", factor: 1 },
|
||||||
'bl.': { unit: 'stk', factor: 1 },
|
"bl.": { unit: "stk", factor: 1 },
|
||||||
'pkg': { unit: 'stk', factor: 1 },
|
pkg: { unit: "stk", factor: 1 },
|
||||||
'gr': { unit: 'g', factor: 1 },
|
gr: { unit: "g", factor: 1 },
|
||||||
'er': { unit: 'stk', factor: 1 },
|
er: { unit: "stk", factor: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCanonical = function(item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
let quantity = item.prices[0].presentationPrice.measurementUnit.quantity
|
let quantity = item.prices[0].presentationPrice.measurementUnit.quantity;
|
||||||
let unit = item.prices[0].presentationPrice.measurementUnit.unitCode.toLowerCase()
|
let unit = item.prices[0].presentationPrice.measurementUnit.unitCode.toLowerCase();
|
||||||
if(['xro', 'h87', 'hlt'].indexOf(unit)!=-1) {
|
if (["xro", "h87", "hlt"].indexOf(unit) != -1) {
|
||||||
const q = utils.parseUnitAndQuantityAtEnd(item.mixins.productCustomAttributes.packagingUnit)
|
const q = utils.parseUnitAndQuantityAtEnd(item.mixins.productCustomAttributes.packagingUnit);
|
||||||
quantity = q[0] ?? quantity;
|
quantity = q[0] ?? quantity;
|
||||||
unit = q[1];
|
unit = q[1];
|
||||||
}
|
}
|
||||||
if (!(unit in conversions)) {
|
if (!(unit in conversions)) {
|
||||||
unit = 'stk';
|
unit = "stk";
|
||||||
}
|
}
|
||||||
const isWeighted = (item.mixins.productCustomAttributes?.packagingDescription ?? "").startsWith("Gewichtsware");
|
const isWeighted = (item.mixins.productCustomAttributes?.packagingDescription ?? "").startsWith("Gewichtsware");
|
||||||
return utils.convertUnit({
|
return utils.convertUnit(
|
||||||
id: item.code,
|
{
|
||||||
name: item.name[0],
|
id: item.code,
|
||||||
isWeighted,
|
name: item.name[0],
|
||||||
price: isWeighted ? item.prices[0].effectiveAmount : item.prices[0].presentationPrice.effectiveAmount,
|
isWeighted,
|
||||||
priceHistory: [{ date: today, price: item.prices[0].presentationPrice.effectiveAmount }],
|
price: isWeighted ? item.prices[0].effectiveAmount : item.prices[0].presentationPrice.effectiveAmount,
|
||||||
unit,
|
priceHistory: [{ date: today, price: item.prices[0].presentationPrice.effectiveAmount }],
|
||||||
quantity,
|
unit,
|
||||||
bio: item.mixins.mpreisAttributes.properties?.includes('BIO'),
|
quantity,
|
||||||
url: `https://www.mpreis.at/shop/p/${item.code}`,
|
bio: item.mixins.mpreisAttributes.properties?.includes("BIO"),
|
||||||
}, conversions, 'mpreis');
|
url: `https://www.mpreis.at/shop/p/${item.code}`,
|
||||||
}
|
},
|
||||||
|
conversions,
|
||||||
|
"mpreis"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.fetchData = async function() {
|
exports.fetchData = async function () {
|
||||||
const MPREIS_URL = `https://ax2ixv4hll-dsn.algolia.net/1/indexes/prod_mpreis_8450/browse?X-Algolia-API-Key=NmJlMTI0NjY1NGU4MDUwYTRlMmYzYWFjOWFlY2U4MGFkNGZjMDY2NmNjNjQzNWY3OWJlNDY4OTY0ZjEwOTEwYWZpbHRlcnM9cHVibGlzaGVk&X-Algolia-Application-Id=AX2IXV4HLL&X-Algolia-Agent=Vue.js`
|
const MPREIS_URL = `https://ax2ixv4hll-dsn.algolia.net/1/indexes/prod_mpreis_8450/browse?X-Algolia-API-Key=NmJlMTI0NjY1NGU4MDUwYTRlMmYzYWFjOWFlY2U4MGFkNGZjMDY2NmNjNjQzNWY3OWJlNDY4OTY0ZjEwOTEwYWZpbHRlcnM9cHVibGlzaGVk&X-Algolia-Application-Id=AX2IXV4HLL&X-Algolia-Agent=Vue.js`;
|
||||||
let mpreisItems = [];
|
let mpreisItems = [];
|
||||||
let res = (await axios.get(MPREIS_URL)).data;
|
let res = (await axios.get(MPREIS_URL)).data;
|
||||||
mpreisItems = mpreisItems.concat(res.hits);
|
mpreisItems = mpreisItems.concat(res.hits);
|
||||||
|
@ -63,4 +67,4 @@ exports.fetchData = async function() {
|
||||||
cursor = res.cursor;
|
cursor = res.cursor;
|
||||||
}
|
}
|
||||||
return mpreisItems;
|
return mpreisItems;
|
||||||
}
|
};
|
||||||
|
|
|
@ -3,35 +3,39 @@ const utils = require("./utils");
|
||||||
const MAXITEMS = 10000;
|
const MAXITEMS = 10000;
|
||||||
|
|
||||||
const conversions = {
|
const conversions = {
|
||||||
"bd": { unit: 'stk', factor: 1 },
|
bd: { unit: "stk", factor: 1 },
|
||||||
"g": { unit: 'g', factor: 1 },
|
g: { unit: "g", factor: 1 },
|
||||||
"gr": { unit: 'g', factor: 1 },
|
gr: { unit: "g", factor: 1 },
|
||||||
"kg": { unit: 'g', factor: 1000 },
|
kg: { unit: "g", factor: 1000 },
|
||||||
"lt": { unit: 'ml', factor: 1000 },
|
lt: { unit: "ml", factor: 1000 },
|
||||||
"ml": { unit: 'ml', factor: 1 },
|
ml: { unit: "ml", factor: 1 },
|
||||||
"pk": { unit: 'stk', factor: 1 },
|
pk: { unit: "stk", factor: 1 },
|
||||||
"pa": { unit: 'stk', factor: 1 },
|
pa: { unit: "stk", factor: 1 },
|
||||||
"rl": { unit: 'stk', factor: 1 },
|
rl: { unit: "stk", factor: 1 },
|
||||||
"st": { unit: 'stk', factor: 1 },
|
st: { unit: "stk", factor: 1 },
|
||||||
"tb": { unit: 'stk', factor: 1 },
|
tb: { unit: "stk", factor: 1 },
|
||||||
"wg": { unit: 'wg', factor: 1 },
|
wg: { unit: "wg", factor: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCanonical = function (item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
let quantity = item.amount;
|
let quantity = item.amount;
|
||||||
let unit = item.volumeLabelKey;
|
let unit = item.volumeLabelKey;
|
||||||
return utils.convertUnit({
|
return utils.convertUnit(
|
||||||
id: item.productId,
|
{
|
||||||
name: item.name,
|
id: item.productId,
|
||||||
price: item.price.regular.value / 100,
|
name: item.name,
|
||||||
priceHistory: [{ date: today, price: item.price.regular.value / 100 }],
|
price: item.price.regular.value / 100,
|
||||||
isWeighted: item.isWeightArticle,
|
priceHistory: [{ date: today, price: item.price.regular.value / 100 }],
|
||||||
unit,
|
isWeighted: item.isWeightArticle,
|
||||||
quantity,
|
unit,
|
||||||
bio: item.name.toLowerCase().includes("bio") && !item.name.toLowerCase().includes("fabio"),
|
quantity,
|
||||||
url: `https://www.penny.at/produkte/${item.slug}`,
|
bio: item.name.toLowerCase().includes("bio") && !item.name.toLowerCase().includes("fabio"),
|
||||||
}, conversions, 'penny');
|
url: `https://www.penny.at/produkte/${item.slug}`,
|
||||||
}
|
},
|
||||||
|
conversions,
|
||||||
|
"penny"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.fetchData = async function () {
|
exports.fetchData = async function () {
|
||||||
hits = 100;
|
hits = 100;
|
||||||
|
@ -41,9 +45,9 @@ exports.fetchData = async function () {
|
||||||
while (!done) {
|
while (!done) {
|
||||||
const PENNY_SEARCH = `https://www.penny.at/api/products?page=${page}&pageSize=${hits}`;
|
const PENNY_SEARCH = `https://www.penny.at/api/products?page=${page}&pageSize=${hits}`;
|
||||||
data = (await axios.get(PENNY_SEARCH)).data;
|
data = (await axios.get(PENNY_SEARCH)).data;
|
||||||
done = (data.count < hits || page * hits > MAXITEMS);
|
done = data.count < hits || page * hits > MAXITEMS;
|
||||||
page++;
|
page++;
|
||||||
result = result.concat(data.results);
|
result = result.concat(data.results);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,42 +1,47 @@
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const util = require('util');
|
const util = require("util");
|
||||||
const exec = util.promisify(require('child_process').exec);
|
const exec = util.promisify(require("child_process").exec);
|
||||||
const utils = require('./utils');
|
const utils = require("./utils");
|
||||||
|
|
||||||
const conversions = {
|
const conversions = {
|
||||||
"Beutel": { unit: 'stk', factor: 1 },
|
Beutel: { unit: "stk", factor: 1 },
|
||||||
"Blatt": { unit: 'stk', factor: 1 },
|
Blatt: { unit: "stk", factor: 1 },
|
||||||
"Bund": { unit: 'stk', factor: 1 },
|
Bund: { unit: "stk", factor: 1 },
|
||||||
"g": { unit: 'g', factor: 1},
|
g: { unit: "g", factor: 1 },
|
||||||
"Gramm": { unit: 'g', factor: 1},
|
Gramm: { unit: "g", factor: 1 },
|
||||||
"kg": { unit: 'g', factor: 1000},
|
kg: { unit: "g", factor: 1000 },
|
||||||
"Kilogramm": { unit: 'g', factor: 1},
|
Kilogramm: { unit: "g", factor: 1 },
|
||||||
"l": { unit: 'ml', factor: 1000},
|
l: { unit: "ml", factor: 1000 },
|
||||||
"Liter": { unit: 'ml', factor: 1000},
|
Liter: { unit: "ml", factor: 1000 },
|
||||||
"cm": { unit: 'cm', factor: 1},
|
cm: { unit: "cm", factor: 1 },
|
||||||
"m": { unit: 'cm', factor: 100},
|
m: { unit: "cm", factor: 100 },
|
||||||
"Meter": { unit: 'cm', factor: 100},
|
Meter: { unit: "cm", factor: 100 },
|
||||||
"Milliliter": { unit: 'ml', factor: 1},
|
Milliliter: { unit: "ml", factor: 1 },
|
||||||
"ml": { unit: 'ml', factor: 1},
|
ml: { unit: "ml", factor: 1 },
|
||||||
"Paar": { unit: 'stk', factor: 1 },
|
Paar: { unit: "stk", factor: 1 },
|
||||||
"Packung": { unit: 'stk', factor: 1 },
|
Packung: { unit: "stk", factor: 1 },
|
||||||
"Portion": { unit: 'stk', factor: 1 },
|
Portion: { unit: "stk", factor: 1 },
|
||||||
"Rollen": { unit: 'stk', factor: 1 },
|
Rollen: { unit: "stk", factor: 1 },
|
||||||
"Stk": { unit: 'stk', factor: 1 },
|
Stk: { unit: "stk", factor: 1 },
|
||||||
"Stück": { unit: 'stk', factor: 1 },
|
Stück: { unit: "stk", factor: 1 },
|
||||||
"stück": { unit: 'stk', factor: 1 },
|
stück: { unit: "stk", factor: 1 },
|
||||||
"Teebeutel": { unit: 'stk', factor: 1 },
|
Teebeutel: { unit: "stk", factor: 1 },
|
||||||
"Waschgang": { unit: 'wg', factor: 1 },
|
Waschgang: { unit: "wg", factor: 1 },
|
||||||
"Zentimeter": { unit: 'cm', factor: 1 },
|
Zentimeter: { unit: "cm", factor: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCanonical = function (item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
let quantity = 1, unit = "kg";
|
let quantity = 1,
|
||||||
|
unit = "kg";
|
||||||
if (item.grammage && item.grammage.length > 0) {
|
if (item.grammage && item.grammage.length > 0) {
|
||||||
let grammage = item.grammage.trim().replace(/\([^)]*\)/g, '').replace(",", ".").trim();
|
let grammage = item.grammage
|
||||||
|
.trim()
|
||||||
|
.replace(/\([^)]*\)/g, "")
|
||||||
|
.replace(",", ".")
|
||||||
|
.trim();
|
||||||
let multiplier = 1;
|
let multiplier = 1;
|
||||||
if (grammage.indexOf("x") != -1) {
|
if (grammage.indexOf("x") != -1) {
|
||||||
let tokens = grammage.split("x")
|
let tokens = grammage.split("x");
|
||||||
multiplier = Number.parseFloat(tokens[0]);
|
multiplier = Number.parseFloat(tokens[0]);
|
||||||
grammage = tokens[1];
|
grammage = tokens[1];
|
||||||
}
|
}
|
||||||
|
@ -51,22 +56,27 @@ exports.getCanonical = function (item, today) {
|
||||||
}
|
}
|
||||||
quantity *= multiplier;
|
quantity *= multiplier;
|
||||||
} else {
|
} else {
|
||||||
quantity = 1; unit = "Stk";
|
quantity = 1;
|
||||||
|
unit = "Stk";
|
||||||
}
|
}
|
||||||
|
|
||||||
let price = Number.parseFloat(item.currentPrice.split(" ")[0].replace(",", "."));
|
let price = Number.parseFloat(item.currentPrice.split(" ")[0].replace(",", "."));
|
||||||
return utils.convertUnit({
|
return utils.convertUnit(
|
||||||
id: item.id,
|
{
|
||||||
name: item.name,
|
id: item.id,
|
||||||
price,
|
name: item.name,
|
||||||
priceHistory: [{ date: today, price }],
|
price,
|
||||||
isWeighted: false,
|
priceHistory: [{ date: today, price }],
|
||||||
unit,
|
isWeighted: false,
|
||||||
quantity,
|
unit,
|
||||||
bio: false,
|
quantity,
|
||||||
url: "",
|
bio: false,
|
||||||
}, conversions, "reweDe");
|
url: "",
|
||||||
}
|
},
|
||||||
|
conversions,
|
||||||
|
"reweDe"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.fetchData = async function () {
|
exports.fetchData = async function () {
|
||||||
// For some unholy reason, Axios returns 403 when accessing the endpoint
|
// For some unholy reason, Axios returns 403 when accessing the endpoint
|
||||||
|
@ -83,19 +93,31 @@ exports.fetchData = async function () {
|
||||||
return (await axiosNoDefaults.get('https://mobile-api.rewe.de/api/v3/product-search?searchTerm=*&page=1&sorting=RELEVANCE_DESC&objectsPerPage=250&marketCode=440405&serviceTypes=PICKUP', { headers, httpsAgent: agent })).data;*/
|
return (await axiosNoDefaults.get('https://mobile-api.rewe.de/api/v3/product-search?searchTerm=*&page=1&sorting=RELEVANCE_DESC&objectsPerPage=250&marketCode=440405&serviceTypes=PICKUP', { headers, httpsAgent: agent })).data;*/
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await exec("curl --version")
|
await exec("curl --version");
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.log("ERROR: Can't fetch REWE-DE data, no curl installed.");
|
console.log("ERROR: Can't fetch REWE-DE data, no curl installed.");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let pageId = 1;
|
let pageId = 1;
|
||||||
let result = (await exec(`curl -s "https://mobile-api.rewe.de/api/v3/product-search\?searchTerm\=\*\&page\=${pageId++}\&sorting\=RELEVANCE_DESC\&objectsPerPage\=250\&marketCode\=440405\&serviceTypes\=PICKUP" -H "Rd-Service-Types: PICKUP" -H "Rd-Market-Id: 440405"`)).stdout;
|
let result = (
|
||||||
|
await exec(
|
||||||
|
`curl -s "https://mobile-api.rewe.de/api/v3/product-search\?searchTerm\=\*\&page\=${pageId++}\&sorting\=RELEVANCE_DESC\&objectsPerPage\=250\&marketCode\=440405\&serviceTypes\=PICKUP" -H "Rd-Service-Types: PICKUP" -H "Rd-Market-Id: 440405"`
|
||||||
|
)
|
||||||
|
).stdout;
|
||||||
const firstPage = JSON.parse(result);
|
const firstPage = JSON.parse(result);
|
||||||
const totalPages = firstPage.totalPages;
|
const totalPages = firstPage.totalPages;
|
||||||
const items = [...firstPage.products];
|
const items = [...firstPage.products];
|
||||||
for (let i = 2; i <= totalPages; i++) {
|
for (let i = 2; i <= totalPages; i++) {
|
||||||
items.push(...JSON.parse((await exec(`curl -s "https://mobile-api.rewe.de/api/v3/product-search\?searchTerm\=\*\&page\=${pageId++}\&sorting\=RELEVANCE_DESC\&objectsPerPage\=250\&marketCode\=440405\&serviceTypes\=PICKUP" -H "Rd-Service-Types: PICKUP" -H "Rd-Market-Id: 440405"`)).stdout).products);
|
items.push(
|
||||||
|
...JSON.parse(
|
||||||
|
(
|
||||||
|
await exec(
|
||||||
|
`curl -s "https://mobile-api.rewe.de/api/v3/product-search\?searchTerm\=\*\&page\=${pageId++}\&sorting\=RELEVANCE_DESC\&objectsPerPage\=250\&marketCode\=440405\&serviceTypes\=PICKUP" -H "Rd-Service-Types: PICKUP" -H "Rd-Market-Id: 440405"`
|
||||||
|
)
|
||||||
|
).stdout
|
||||||
|
).products
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
};
|
||||||
|
|
|
@ -3,64 +3,67 @@ const utils = require("./utils");
|
||||||
const HITS = Math.floor(30000 + Math.random() * 2000);
|
const HITS = Math.floor(30000 + Math.random() * 2000);
|
||||||
|
|
||||||
const conversions = {
|
const conversions = {
|
||||||
'g': { unit: 'g', factor: 1 },
|
g: { unit: "g", factor: 1 },
|
||||||
'kg': { unit: 'g', factor: 1000 },
|
kg: { unit: "g", factor: 1000 },
|
||||||
'l': { unit: 'ml', factor: 1000 },
|
l: { unit: "ml", factor: 1000 },
|
||||||
'ml': { unit: 'ml', factor: 1 },
|
ml: { unit: "ml", factor: 1 },
|
||||||
'stk': { unit: 'stk', factor: 1 },
|
stk: { unit: "stk", factor: 1 },
|
||||||
'stück': { unit: 'stk', factor: 1 },
|
stück: { unit: "stk", factor: 1 },
|
||||||
'100ml': { unit: 'ml', factor: 100 },
|
"100ml": { unit: "ml", factor: 100 },
|
||||||
'wg': { unit: 'wg', factor: 1 },
|
wg: { unit: "wg", factor: 1 },
|
||||||
'100g': { unit: 'g', factor: 100 },
|
"100g": { unit: "g", factor: 100 },
|
||||||
'm': { unit: 'cm', factor: 100 },
|
m: { unit: "cm", factor: 100 },
|
||||||
'cm': { unit: 'cm', factor: 100 },
|
cm: { unit: "cm", factor: 100 },
|
||||||
'ml': { unit: 'ml', factor: 1 },
|
ml: { unit: "ml", factor: 1 },
|
||||||
'meter': { unit: 'cm', factor: 100 },
|
meter: { unit: "cm", factor: 100 },
|
||||||
'mm': { unit: 'cm', factor: .1 },
|
mm: { unit: "cm", factor: 0.1 },
|
||||||
'stk.': { unit: 'cm', factor: .1 },
|
"stk.": { unit: "cm", factor: 0.1 },
|
||||||
'cl': { unit: 'ml', factor: 10 },
|
cl: { unit: "ml", factor: 10 },
|
||||||
'blatt': { unit: 'stk', factor: 1 },
|
blatt: { unit: "stk", factor: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCanonical = function(item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
let price, unit, quantity;
|
let price, unit, quantity;
|
||||||
const description = item.masterValues["short-description-3"] ?? item.masterValues["short-description-2"];
|
const description = item.masterValues["short-description-3"] ?? item.masterValues["short-description-2"];
|
||||||
if (item.masterValues["quantity-selector"]) {
|
if (item.masterValues["quantity-selector"]) {
|
||||||
const [str_price, str_unit] = item.masterValues["price-per-unit"].split('/');
|
const [str_price, str_unit] = item.masterValues["price-per-unit"].split("/");
|
||||||
price = parseFloat(str_price.replace("€", ""));
|
price = parseFloat(str_price.replace("€", ""));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
price = item.masterValues.price;
|
price = item.masterValues.price;
|
||||||
}
|
}
|
||||||
if(description) {
|
if (description) {
|
||||||
const s = description.replace(" EINWEG", "").replace(" MEHRWEG", "").trim();
|
const s = description.replace(" EINWEG", "").replace(" MEHRWEG", "").trim();
|
||||||
const q = utils.parseUnitAndQuantityAtEnd(s);
|
const q = utils.parseUnitAndQuantityAtEnd(s);
|
||||||
quantity = q[0]
|
quantity = q[0];
|
||||||
unit = q[1]
|
unit = q[1];
|
||||||
}
|
}
|
||||||
if(conversions[unit]===undefined) {
|
if (conversions[unit] === undefined) {
|
||||||
// use price per unit to calculate quantity (less accurate)
|
// use price per unit to calculate quantity (less accurate)
|
||||||
let [unitPrice, unit_] = item.masterValues['price-per-unit'].split('/');
|
let [unitPrice, unit_] = item.masterValues["price-per-unit"].split("/");
|
||||||
unitPrice = parseFloat(unitPrice.replace("€", ""));
|
unitPrice = parseFloat(unitPrice.replace("€", ""));
|
||||||
quantity = parseFloat((price / unitPrice).toFixed(3));
|
quantity = parseFloat((price / unitPrice).toFixed(3));
|
||||||
unit = unit_.toLowerCase();
|
unit = unit_.toLowerCase();
|
||||||
}
|
}
|
||||||
return utils.convertUnit({
|
return utils.convertUnit(
|
||||||
id: item.masterValues["code-internal"],
|
{
|
||||||
sparId: item.masterValues["product-number"],
|
id: item.masterValues["code-internal"],
|
||||||
name: item.masterValues.title + " " + item.masterValues["short-description"],
|
sparId: item.masterValues["product-number"],
|
||||||
price,
|
name: item.masterValues.title + " " + item.masterValues["short-description"],
|
||||||
priceHistory: [{ date: today, price }],
|
price,
|
||||||
unit,
|
priceHistory: [{ date: today, price }],
|
||||||
quantity,
|
unit,
|
||||||
isWeighted: item.masterValues['item-type'] === 'WeightProduct',
|
quantity,
|
||||||
bio: item.masterValues.biolevel === "Bio",
|
isWeighted: item.masterValues["item-type"] === "WeightProduct",
|
||||||
url: `https://www.interspar.at/shop/lebensmittel${item.masterValues.url}`,
|
bio: item.masterValues.biolevel === "Bio",
|
||||||
}, conversions, 'spar');
|
url: `https://www.interspar.at/shop/lebensmittel${item.masterValues.url}`,
|
||||||
}
|
},
|
||||||
|
conversions,
|
||||||
|
"spar"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.fetchData = async function() {
|
exports.fetchData = async function () {
|
||||||
const SPAR_SEARCH = `https://search-spar.spar-ics.com/fact-finder/rest/v4/search/products_lmos_at?query=*&q=*&page=1&hitsPerPage=${HITS}`;
|
const SPAR_SEARCH = `https://search-spar.spar-ics.com/fact-finder/rest/v4/search/products_lmos_at?query=*&q=*&page=1&hitsPerPage=${HITS}`;
|
||||||
const rawItems = (await axios.get(SPAR_SEARCH)).data.hits;
|
const rawItems = (await axios.get(SPAR_SEARCH)).data.hits;
|
||||||
return rawItems?.hits || rawItems;
|
return rawItems?.hits || rawItems;
|
||||||
}
|
};
|
||||||
|
|
|
@ -2,52 +2,50 @@ const axios = require("axios");
|
||||||
const HTMLParser = require("node-html-parser");
|
const HTMLParser = require("node-html-parser");
|
||||||
|
|
||||||
exports.getCanonical = function (item, today) {
|
exports.getCanonical = function (item, today) {
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
price: item.price,
|
price: item.price,
|
||||||
priceHistory: [{ date: today, price: item.price }],
|
priceHistory: [{ date: today, price: item.price }],
|
||||||
unit: item.unit,
|
unit: item.unit,
|
||||||
bio: item.name.toLowerCase().includes('bio'),
|
bio: item.name.toLowerCase().includes("bio"),
|
||||||
url: `https://shop.unimarkt.at${item.canonicalUrl}`,
|
url: `https://shop.unimarkt.at${item.canonicalUrl}`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.fetchData = async function () {
|
exports.fetchData = async function () {
|
||||||
const UNIMARKT_BASE_URL = `https://shop.unimarkt.at/`;
|
const UNIMARKT_BASE_URL = `https://shop.unimarkt.at/`;
|
||||||
const UNIMARKT_MAIN_CATEGORIES = [
|
const UNIMARKT_MAIN_CATEGORIES = [
|
||||||
"obst-gemuese",
|
"obst-gemuese",
|
||||||
"kuehlprodukte",
|
"kuehlprodukte",
|
||||||
"fleisch-wurst",
|
"fleisch-wurst",
|
||||||
"brot-gebaeck",
|
"brot-gebaeck",
|
||||||
"getraenke",
|
"getraenke",
|
||||||
"lebensmittel",
|
"lebensmittel",
|
||||||
"suesses-snacks",
|
"suesses-snacks",
|
||||||
];
|
];
|
||||||
|
|
||||||
let unimarktItems = [];
|
let unimarktItems = [];
|
||||||
for (let category of UNIMARKT_MAIN_CATEGORIES) {
|
for (let category of UNIMARKT_MAIN_CATEGORIES) {
|
||||||
var res = await axios.get(UNIMARKT_BASE_URL + category, {
|
var res = await axios.get(UNIMARKT_BASE_URL + category, {
|
||||||
validateStatus: function (status) {
|
validateStatus: function (status) {
|
||||||
return (status >= 200 && status < 300);
|
return status >= 200 && status < 300;
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.data) {
|
|
||||||
var root = HTMLParser.parse(res.data);
|
|
||||||
|
|
||||||
root
|
|
||||||
.querySelectorAll(".articleListItem .produktContainer")
|
|
||||||
.forEach((product) => {
|
|
||||||
unimarktItems.push({
|
|
||||||
id: product._attrs["data-articleid"],
|
|
||||||
name: product.querySelector(".name").text,
|
|
||||||
price: parseFloat(product._attrs["data-price"]),
|
|
||||||
unit: product.querySelector(".grammatur").text,
|
|
||||||
canonicalUrl: product.querySelector(".image > a")._attrs["href"],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res && res.data) {
|
||||||
|
var root = HTMLParser.parse(res.data);
|
||||||
|
|
||||||
|
root.querySelectorAll(".articleListItem .produktContainer").forEach((product) => {
|
||||||
|
unimarktItems.push({
|
||||||
|
id: product._attrs["data-articleid"],
|
||||||
|
name: product.querySelector(".name").text,
|
||||||
|
price: parseFloat(product._attrs["data-price"]),
|
||||||
|
unit: product.querySelector(".grammatur").text,
|
||||||
|
canonicalUrl: product.querySelector(".image > a")._attrs["href"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return unimarktItems;
|
||||||
return unimarktItems;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,39 +1,38 @@
|
||||||
exports.convertUnit = function (item, units, store) {
|
exports.convertUnit = function (item, units, store) {
|
||||||
|
if (!(item.unit in units)) {
|
||||||
if(!(item.unit in units)) {
|
|
||||||
console.error(`Unknown unit in ${store}: '${item.unit}' in item ${item.name}`);
|
console.error(`Unknown unit in ${store}: '${item.unit}' in item ${item.name}`);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof(item.quantity) == 'string')
|
if (typeof item.quantity == "string") item.quantity = parseFloat(item.quantity.replace(",", "."));
|
||||||
item.quantity = parseFloat(item.quantity.replace(',', '.'));
|
|
||||||
|
|
||||||
const conv = units[item.unit];
|
const conv = units[item.unit];
|
||||||
item.quantity = conv.factor * item.quantity;
|
item.quantity = conv.factor * item.quantity;
|
||||||
item.unit = conv.unit;
|
item.unit = conv.unit;
|
||||||
|
|
||||||
if(item.isWeighted && (item.unit =='g' || item.unit == 'ml')) {
|
if (item.isWeighted && (item.unit == "g" || item.unit == "ml")) {
|
||||||
item.price = 100*item.price/item.quantity;
|
item.price = (100 * item.price) / item.quantity;
|
||||||
item.quantity = 100;
|
item.quantity = 100;
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.parseUnitAndQuantityAtEnd = function (name) {
|
exports.parseUnitAndQuantityAtEnd = function (name) {
|
||||||
let unit, quantity = 1;
|
let unit,
|
||||||
const nameTokens = name.trim().replaceAll('(','').replaceAll(')','').replaceAll(',', '.').split(' ');
|
quantity = 1;
|
||||||
const lastToken = nameTokens[nameTokens.length-1];
|
const nameTokens = name.trim().replaceAll("(", "").replaceAll(")", "").replaceAll(",", ".").split(" ");
|
||||||
const secondLastToken = nameTokens.length >= 2 ? nameTokens[nameTokens.length-2] : null;
|
const lastToken = nameTokens[nameTokens.length - 1];
|
||||||
|
const secondLastToken = nameTokens.length >= 2 ? nameTokens[nameTokens.length - 2] : null;
|
||||||
|
|
||||||
const token = parseFloat(lastToken) ? lastToken : secondLastToken + lastToken;
|
const token = parseFloat(lastToken) ? lastToken : secondLastToken + lastToken;
|
||||||
const regex = /^([0-9.x]+)(.*)$/;
|
const regex = /^([0-9.x]+)(.*)$/;
|
||||||
const matches = token.match(regex);
|
const matches = token.match(regex);
|
||||||
if(matches) {
|
if (matches) {
|
||||||
matches[1].split('x').forEach( (q)=> {
|
matches[1].split("x").forEach((q) => {
|
||||||
quantity = quantity * parseFloat(q)
|
quantity = quantity * parseFloat(q);
|
||||||
})
|
});
|
||||||
unit = matches[2];
|
unit = matches[2];
|
||||||
return [quantity, unit.toLowerCase()];
|
return [quantity, unit.toLowerCase()];
|
||||||
}
|
}
|
||||||
return [undefined, undefined];
|
return [undefined, undefined];
|
||||||
}
|
};
|
||||||
|
|
54
stuff.js
54
stuff.js
|
@ -4,20 +4,19 @@ const STORE_KEYS = Object.keys(stores);
|
||||||
|
|
||||||
function grammageAnalysis() {
|
function grammageAnalysis() {
|
||||||
const items = JSON.parse(fs.readFileSync("docker/data/latest-canonical.json"));
|
const items = JSON.parse(fs.readFileSync("docker/data/latest-canonical.json"));
|
||||||
items.sort(item => item.priceHistory.length);
|
items.sort((item) => item.priceHistory.length);
|
||||||
|
|
||||||
for (item of items) {
|
for (item of items) {
|
||||||
if (item.priceHistory.length > 2)
|
if (item.priceHistory.length > 2) console.log(JSON.stringify(item, null, 2));
|
||||||
console.log(JSON.stringify(item, null, 2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const units = {};
|
const units = {};
|
||||||
const unitsSmall = {}
|
const unitsSmall = {};
|
||||||
|
|
||||||
for (item of items) {
|
for (item of items) {
|
||||||
const tokens = item.unit ? item.unit.split(/\s+/) : [];
|
const tokens = item.unit ? item.unit.split(/\s+/) : [];
|
||||||
if (tokens.length == 0) continue;
|
if (tokens.length == 0) continue;
|
||||||
if (tokens[0].charAt(0) >= '0' && tokens[0].charAt(0) <= '9') {
|
if (tokens[0].charAt(0) >= "0" && tokens[0].charAt(0) <= "9") {
|
||||||
tokens.splice(0, 1);
|
tokens.splice(0, 1);
|
||||||
}
|
}
|
||||||
units[tokens.join(" ")] = item;
|
units[tokens.join(" ")] = item;
|
||||||
|
@ -29,7 +28,7 @@ function grammageAnalysis() {
|
||||||
console.log(Object.keys(unitsSmall).length);
|
console.log(Object.keys(unitsSmall).length);
|
||||||
|
|
||||||
const hofer = JSON.parse(fs.readFileSync("docker/data/hofer-2023-05-19.json"));
|
const hofer = JSON.parse(fs.readFileSync("docker/data/hofer-2023-05-19.json"));
|
||||||
const unitTypes = {}
|
const unitTypes = {};
|
||||||
for (item of hofer) {
|
for (item of hofer) {
|
||||||
unitTypes[item.UnitType] = true;
|
unitTypes[item.UnitType] = true;
|
||||||
}
|
}
|
||||||
|
@ -75,7 +74,7 @@ function grammageAnalysis() {
|
||||||
for (item of billa) {
|
for (item of billa) {
|
||||||
let unit;
|
let unit;
|
||||||
if (item.masterValues["quantity-selector"]) {
|
if (item.masterValues["quantity-selector"]) {
|
||||||
const [str_price, str_unit] = item.masterValues["price-per-unit"].split('/');
|
const [str_price, str_unit] = item.masterValues["price-per-unit"].split("/");
|
||||||
unit = str_unit.trim();
|
unit = str_unit.trim();
|
||||||
} else {
|
} else {
|
||||||
unit = item.masterValues["short-description-3"];
|
unit = item.masterValues["short-description-3"];
|
||||||
|
@ -83,7 +82,9 @@ function grammageAnalysis() {
|
||||||
|
|
||||||
if (!unit) {
|
if (!unit) {
|
||||||
noGrammage.push(item);
|
noGrammage.push(item);
|
||||||
noGrammageUnits[item.masterValues["sales-unit"]] = noGrammageUnits[item.masterValues["sales-unit"]] ? noGrammageUnits[item.masterValues["sales-unit"]] + 1 : 1;
|
noGrammageUnits[item.masterValues["sales-unit"]] = noGrammageUnits[item.masterValues["sales-unit"]]
|
||||||
|
? noGrammageUnits[item.masterValues["sales-unit"]] + 1
|
||||||
|
: 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let tokens = unit.split(" ");
|
let tokens = unit.split(" ");
|
||||||
|
@ -116,8 +117,8 @@ function momentumCartConversion() {
|
||||||
const lines = fs.readFileSync("momentum-cart.csv").toString().split(/\r?\n/);
|
const lines = fs.readFileSync("momentum-cart.csv").toString().split(/\r?\n/);
|
||||||
const cart = {
|
const cart = {
|
||||||
name: "Momentum Eigenmarken Vergleich",
|
name: "Momentum Eigenmarken Vergleich",
|
||||||
items: []
|
items: [],
|
||||||
}
|
};
|
||||||
for (line of lines) {
|
for (line of lines) {
|
||||||
const [sparId, billaId] = line.split(/\s+/);
|
const [sparId, billaId] = line.split(/\s+/);
|
||||||
const sparItem = lookup[sparId];
|
const sparItem = lookup[sparId];
|
||||||
|
@ -139,7 +140,7 @@ function momentumCartConversion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixSparHistoricalData(dataDir) {
|
function fixSparHistoricalData(dataDir) {
|
||||||
const files = fs.readdirSync(dataDir).filter(file => file.indexOf("canonical") == -1 && file.indexOf(`spar-`) == 0);
|
const files = fs.readdirSync(dataDir).filter((file) => file.indexOf("canonical") == -1 && file.indexOf(`spar-`) == 0);
|
||||||
console.log(files);
|
console.log(files);
|
||||||
|
|
||||||
for (file of files) {
|
for (file of files) {
|
||||||
|
@ -151,15 +152,14 @@ function fixSparHistoricalData(dataDir) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nReadlines = require('n-readlines');
|
const nReadlines = require("n-readlines");
|
||||||
|
|
||||||
function convertDossierData(dataDir, file) {
|
function convertDossierData(dataDir, file) {
|
||||||
console.log(`Converting ${file}`);
|
console.log(`Converting ${file}`);
|
||||||
const lookup = {};
|
const lookup = {};
|
||||||
for (item of JSON.parse(fs.readFileSync(`${dataDir}/latest-canonical.json`))) {
|
for (item of JSON.parse(fs.readFileSync(`${dataDir}/latest-canonical.json`))) {
|
||||||
lookup[item.store + item.id] = item;
|
lookup[item.store + item.id] = item;
|
||||||
if (item.sparId)
|
if (item.sparId) lookup[item.store + "-" + item.sparId] = item;
|
||||||
lookup[item.store + "-" + item.sparId] = item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = new nReadlines(file);
|
const lines = new nReadlines(file);
|
||||||
|
@ -167,10 +167,10 @@ function convertDossierData(dataDir, file) {
|
||||||
const itemsPerDate = {};
|
const itemsPerDate = {};
|
||||||
let line = null;
|
let line = null;
|
||||||
const store = file.indexOf("spar") == 0 ? "spar" : "billa";
|
const store = file.indexOf("spar") == 0 ? "spar" : "billa";
|
||||||
lines.next()
|
lines.next();
|
||||||
let itemsTotal = 0;
|
let itemsTotal = 0;
|
||||||
let notFound = 0;
|
let notFound = 0;
|
||||||
while (line = lines.next()) {
|
while ((line = lines.next())) {
|
||||||
itemsTotal++;
|
itemsTotal++;
|
||||||
const tokens = line.toString("utf-8").split(";");
|
const tokens = line.toString("utf-8").split(";");
|
||||||
const dateTokens = tokens[0].split(".");
|
const dateTokens = tokens[0].split(".");
|
||||||
|
@ -181,8 +181,7 @@ function convertDossierData(dataDir, file) {
|
||||||
const price = Number.parseFloat(tokens[7].replace("€", "").trim().replace(",", "."));
|
const price = Number.parseFloat(tokens[7].replace("€", "").trim().replace(",", "."));
|
||||||
const id = tokens[4].replace("ARTIKELNUMMER: ", "").replace("Art. Nr.: ", "");
|
const id = tokens[4].replace("ARTIKELNUMMER: ", "").replace("Art. Nr.: ", "");
|
||||||
let item = lookup[store + id];
|
let item = lookup[store + id];
|
||||||
if (!item)
|
if (!item) item = lookup[store + "-" + id];
|
||||||
item = lookup[store + "-" + id]
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
// console.log("Couldn't find item " + name);
|
// console.log("Couldn't find item " + name);
|
||||||
notFound++;
|
notFound++;
|
||||||
|
@ -199,8 +198,8 @@ function convertDossierData(dataDir, file) {
|
||||||
title: producer,
|
title: producer,
|
||||||
"short-description": name,
|
"short-description": name,
|
||||||
"short-description-3": unit,
|
"short-description-3": unit,
|
||||||
bioLevel: ""
|
bioLevel: "",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
items.push({
|
items.push({
|
||||||
|
@ -208,12 +207,12 @@ function convertDossierData(dataDir, file) {
|
||||||
articleId: id,
|
articleId: id,
|
||||||
name: name,
|
name: name,
|
||||||
price: {
|
price: {
|
||||||
final: price
|
final: price,
|
||||||
},
|
},
|
||||||
grammagePriceFactor: 1,
|
grammagePriceFactor: 1,
|
||||||
grammage: unit,
|
grammage: unit,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("total: " + itemsTotal);
|
console.log("total: " + itemsTotal);
|
||||||
|
@ -231,8 +230,8 @@ function clownCompress(dataDir) {
|
||||||
const compressed = {
|
const compressed = {
|
||||||
stores: STORE_KEYS,
|
stores: STORE_KEYS,
|
||||||
n: items.length,
|
n: items.length,
|
||||||
data: []
|
data: [],
|
||||||
}
|
};
|
||||||
const data = compressed.data;
|
const data = compressed.data;
|
||||||
for (item of items) {
|
for (item of items) {
|
||||||
data.push(STORE_KEYS.indexOf(item.store));
|
data.push(STORE_KEYS.indexOf(item.store));
|
||||||
|
@ -274,7 +273,6 @@ function clownCompress(dataDir) {
|
||||||
fs.writeFileSync(`${dataDir}/clown.json`, JSON.stringify(compressed));
|
fs.writeFileSync(`${dataDir}/clown.json`, JSON.stringify(compressed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const clustering = require("./site/utils");
|
const clustering = require("./site/utils");
|
||||||
|
|
||||||
/*let items = JSON.parse(fs.readFileSync("palmolive.json"));
|
/*let items = JSON.parse(fs.readFileSync("palmolive.json"));
|
||||||
|
@ -302,5 +300,5 @@ for (cluster of clusters) {
|
||||||
(async () => {
|
(async () => {
|
||||||
// let items = await stores.reweDe.fetchData();
|
// let items = await stores.reweDe.fetchData();
|
||||||
let items = JSON.parse(fs.readFileSync("tmp/reweDe-2023-05-31.json"));
|
let items = JSON.parse(fs.readFileSync("tmp/reweDe-2023-05-31.json"));
|
||||||
for (item of items) stores.reweDe.getCanonical(item)
|
for (item of items) stores.reweDe.getCanonical(item);
|
||||||
})();
|
})();
|
||||||
|
|
Loading…
Reference in New Issue