Initial commit.

This commit is contained in:
Mario Zechner 2023-05-15 13:53:34 +02:00
parent bb8baf32d2
commit 9afe998789
20 changed files with 4779 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
docker/data
.DS_Store

37
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,37 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "client",
"url": "http://localhost:3001",
"webRoot": "${workspaceFolder}/site"
},
{
"type": "node",
"request": "attach",
"name": "server",
"port": 9230,
"address": "localhost",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/heisse-preise",
"protocol": "inspector",
"restart": true,
"continueOnAttach": true
},
],
"compounds": [
{
"name": "client-server",
"configurations": [
"client",
"server"
],
"stopAll": true
}
]
}

2
README.md Normal file
View File

@ -0,0 +1,2 @@
https://search-spar.spar-ics.com/fact-finder/rest/v4/search/products_lmos_at?query=*&q=*&page=1&hitsPerPage=90000
https://shop.billa.at/api/search/full?searchTerm=*&storeId=00-10&pageSize=10000

4
docker/Dockerfile.site Normal file
View File

@ -0,0 +1,4 @@
FROM node:16
WORKDIR /heisse-preise/
ENTRYPOINT ["./docker/main.sh"]

62
docker/control.sh Executable file
View File

@ -0,0 +1,62 @@
#!/bin/bash
set -e
printHelp () {
echo "Usage: control.sh <command>"
echo "Available commands:"
echo
echo " start Pulls changes, builds docker image(s), and starts"
echo " the services (Nginx, Node.js)."
echo " startdev Pulls changes, builds docker image(s), and starts"
echo " the services (Nginx, Node.js)."
echo
echo " reloadnginx Reloads the nginx configuration"
echo
echo " stop Stops the services."
echo
echo " logs Tail -f services' logs."
echo
echo " shell Opens a shell into the Node.js container."
echo
echo " shellnginx Opens a shell into the Nginx container."
echo
echo " dbbackup Takes a SQL dumb of the database and stores it in backup.sql"
}
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
pushd $dir > /dev/null
case "$1" in
start)
git pull
docker-compose -p heisse-preise -f docker-compose.base.yml -f docker-compose.prod.yml build
docker-compose -p heisse-preise -f docker-compose.base.yml -f docker-compose.prod.yml up -d
;;
startdev)
docker-compose -p heisse-preise -f docker-compose.base.yml -f docker-compose.dev.yml build
docker-compose -p heisse-preise -f docker-compose.base.yml -f docker-compose.dev.yml up
;;
reloadnginx)
docker exec -it hp_nginx nginx -t
docker exec -it hp_nginx nginx -s reload
;;
stop)
docker-compose -p heisse-preise -f docker-compose.base.yml down -t 1
;;
shell)
docker exec -it hp_site bash
;;
shellnginx)
docker exec -it hp_nginx bash
;;
logs)
docker-compose -p heisse-preise -f docker-compose.base.yml logs -f
;;
*)
echo "Invalid command $1"
printHelp
;;
esac
popd > /dev/null

View File

@ -0,0 +1,45 @@
version: "3"
services:
web:
image: nginx:1.21.6
container_name: hp_nginx
restart: always
links:
- site
volumes:
- ./server.conf:/etc/nginx/conf.d/default.conf
- ./nginx.conf:/etc/nginx/nginx.conf
- ./data/logs:/logs
- ..:/heisse-preise
networks:
- hp_network
site:
build:
dockerfile: Dockerfile.site
context: .
container_name: hp_site
restart: always
# links:
# - elasticsearch
environment:
- HP_ADMIN_TOKEN=${HP_ADMIN_TOKEN}
volumes:
- ..:/heisse-preise
- ./data/:/heisse-preise/data
networks:
- hp_network
# elasticsearch:
# image: docker.elastic.co/elasticsearch/elasticsearch:8.7.0
# container_name: hp_elasticsearch
# restart: always
# environment:
# - discovery.type=single-node
# ports:
# - 9200:9200
# - 9300:9300
# networks:
# - hp_network
networks:
hp_network:
driver: bridge

View File

@ -0,0 +1,11 @@
version: "3"
services:
web:
ports:
- 3001:80
site:
ports:
- 9230:9230
environment:
- DEV=true

View File

@ -0,0 +1,13 @@
version: "3"
services:
web:
environment:
VIRTUAL_HOST: heisse-preise.io,www.heisse-preise.io
LETSENCRYPT_HOST: heisse-preise.io,www.heisse-preise.io
LETSENCRYPT_EMAIL: "badlogicgames@gmail.com"
networks:
- reverse_proxy
networks:
reverse_proxy:
external:
name: nginx-proxy

11
docker/main.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
set -e
npm install
if [ -z "$DEV" ]; then
echo "RUNNING STUFF 2"
node index.js
else
echo "RUNNING STUFF"
npm run dev
fi

35
docker/nginx.conf Normal file
View File

@ -0,0 +1,35 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main 'no ip - [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
map $request_method $limit {
default "";
POST $binary_remote_addr;
}
limit_req_zone $binary_remote_addr zone=one_per_second:20m rate=1r/s;
limit_req_zone $binary_remote_addr zone=one_per_minute:20m rate=1r/m;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}

52
docker/server.conf Normal file
View File

@ -0,0 +1,52 @@
server {
listen 80;
index index.html;
server_name www.heisse-preise.io heisse-preise.at;
error_log /logs/error.log;
access_log /logs/access.log;
root /heisse-preise/site;
# Let the nginx-proxy give us the
# real ip, see https://github.com/jwilder/nginx-proxy/issues/130
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 0.0.0.0/0;
sendfile on;
sendfile_max_chunk 1m;
tcp_nopush on;
tcp_nodelay on;
gzip on;
gzip_comp_level 4;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain application/x-javascript application/javascript text/xml text/css application/xml;
location / {
}
location /api {
#limit_req zone=one_per_second;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://site:3000;
}
# proxying websocket requests
location /api/ws {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_pass http://site:3000/api/ws;
}
error_page 400 401 402 403 404 405 406 407 408 409 410 411 412 414 415 416 417 418 421 422 423 424 425 426 428 429 431 451 500 501 502 503 504 505 506 507 508 510 511 /404;
location = /404 {
try_files /404.html $uri;
}
}

68
index.js Normal file
View File

@ -0,0 +1,68 @@
const fs = require("fs")
const axios = require("axios")
const HITS = 30000;
const SPAR_SEARCH = `https://search-spar.spar-ics.com/fact-finder/rest/v4/search/products_lmos_at?query=*&q=*&page=1&hitsPerPage=${HITS}`;
const BILLA_SEARCH = `https://shop.billa.at/api/search/full?searchTerm=*&storeId=00-10&pageSize=${HITS}`;
function currentDate() {
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
async function updateData() {
console.log("Fetching data");
const sparItems = (await axios.get(SPAR_SEARCH)).data.hits;
fs.writeFileSync(`data/spar-${currentDate()}.json`, JSON.stringify(sparItems, null, 2));
const sparItemsCanonical = [];
for (let i = 0; i < sparItems.length; i++) {
const item = sparItems[i];
sparItemsCanonical.push({
store: "spar",
name: item.masterValues.title + " " + item.masterValues["short-description"],
price: item.masterValues.price,
unit: item.masterValues["short-description-3"]
});
}
fs.writeFileSync(`data/spar-${currentDate()}-canonical.json`, JSON.stringify(sparItemsCanonical, null, 2));
const billaItems = (await axios.get(BILLA_SEARCH)).data.tiles;
fs.writeFileSync(`data/billa-${currentDate()}.json`, JSON.stringify(billaItems, null, 2));
const billaItemsCanonical = [];
for (let i = 0; i < billaItems.length; i++) {
const item = billaItems[i];
billaItemsCanonical.push({
store: "billa",
name: item.data.name,
price: item.data.price.final,
unit: item.data.grammage
});
}
fs.writeFileSync(`data/billa-${currentDate()}-canonical.json`, JSON.stringify(billaItemsCanonical, null, 2));
fs.writeFileSync(`data/latest-canonical.json`, JSON.stringify([...billaItemsCanonical, ...sparItemsCanonical], null, 2));
console.log("Updated data");
}
let items = JSON.parse(fs.readFileSync("data/latest-canonical.json"));
// updateData()
const express = require('express')
const compression = require('compression');
const app = express()
const port = 3000
app.use(compression());
app.get('/api/index', (req, res) => {
res.send(items)
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

1067
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "heissepreise",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "DEV=true nodemon --watch index.js --inspect-brk=0.0.0.0:9230 index.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/badlogic/heissepreise.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/badlogic/heissepreise/issues"
},
"homepage": "https://github.com/badlogic/heissepreise#readme",
"dependencies": {
"axios": "^1.4.0",
"compression": "^1.7.4",
"express": "^4.18.2",
"node-cron": "^3.0.2",
"nodemon": "^2.0.22"
}
}

2507
site/elasticlunr.js Normal file

File diff suppressed because it is too large Load Diff

33
site/index.html Normal file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Heisse Preise</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="elasticlunr.js"></script>
<script src="lunr.stemmer.support.js"></script>
<script src="lunr.de.js"></script>
</head>
<body>
<div class="main">
<h2>Heisse Preise</h2>
<a href="api/index">Rohdaten</a>
<input id="search" class="search" type="text" placeholder="Produkte suchen...">
<div class="filters">
<input id="billa" type="checkbox" checked="true"><label>Billa</label>
<input id="spar" type="checkbox" checked="true"><label>Spar</label>
<input id="eigenmarken" type="checkbox"><label>Nur CLEVER & S-BUDGET</label>
</div>
<div class="results">
<table id="result">
</table>
</div>
</div>
<script src="main.js"></script>
</body>
</html>

380
site/lunr.de.js Normal file
View File

@ -0,0 +1,380 @@
/*!
* Lunr languages, `German` language
* https://github.com/MihaiValentin/lunr-languages
*
* Copyright 2014, Mihai Valentin
* http://www.mozilla.org/MPL/
*/
/*!
* based on
* Snowball JavaScript Library v0.3
* http://code.google.com/p/urim/
* http://snowball.tartarus.org/
*
* Copyright 2010, Oleg Mazko
* http://www.mozilla.org/MPL/
*/
/**
* export the module via AMD, CommonJS or as a browser global
* Export code from https://github.com/umdjs/umd/blob/master/returnExports.js
*/
;
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(factory)
} else if (typeof exports === 'object') {
/**
* Node. Does not work with strict CommonJS, but
* only CommonJS-like environments that support module.exports,
* like Node.
*/
module.exports = factory()
} else {
// Browser globals (root is window)
factory()(root.lunr);
}
}(this, function() {
/**
* Just return a value to define the module export.
* This example returns an object, but the module
* can return a function as the exported value.
*/
return function(lunr) {
/* throw error if lunr is not yet included */
if ('undefined' === typeof lunr) {
throw new Error('Lunr is not present. Please include / require Lunr before this script.');
}
/* throw error if lunr stemmer support is not yet included */
if ('undefined' === typeof lunr.stemmerSupport) {
throw new Error('Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.');
}
/* register specific locale function */
lunr.de = function() {
this.pipeline.reset();
this.pipeline.add(
lunr.de.trimmer,
lunr.de.stopWordFilter,
lunr.de.stemmer
);
};
/* lunr trimmer function */
lunr.de.wordCharacters = "A-Za-z\xAA\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02B8\u02E0-\u02E4\u1D00-\u1D25\u1D2C-\u1D5C\u1D62-\u1D65\u1D6B-\u1D77\u1D79-\u1DBE\u1E00-\u1EFF\u2071\u207F\u2090-\u209C\u212A\u212B\u2132\u214E\u2160-\u2188\u2C60-\u2C7F\uA722-\uA787\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA7FF\uAB30-\uAB5A\uAB5C-\uAB64\uFB00-\uFB06\uFF21-\uFF3A\uFF41-\uFF5A";
lunr.de.trimmer = lunr.trimmerSupport.generateTrimmer(lunr.de.wordCharacters);
lunr.Pipeline.registerFunction(lunr.de.trimmer, 'trimmer-de');
/* lunr stemmer function */
lunr.de.stemmer = (function() {
/* create the wrapped stemmer object */
var Among = lunr.stemmerSupport.Among,
SnowballProgram = lunr.stemmerSupport.SnowballProgram,
st = new function GermanStemmer() {
var a_0 = [new Among("", -1, 6), new Among("U", 0, 2),
new Among("Y", 0, 1), new Among("\u00E4", 0, 3),
new Among("\u00F6", 0, 4), new Among("\u00FC", 0, 5)
],
a_1 = [
new Among("e", -1, 2), new Among("em", -1, 1),
new Among("en", -1, 2), new Among("ern", -1, 1),
new Among("er", -1, 1), new Among("s", -1, 3),
new Among("es", 5, 2)
],
a_2 = [new Among("en", -1, 1),
new Among("er", -1, 1), new Among("st", -1, 2),
new Among("est", 2, 1)
],
a_3 = [new Among("ig", -1, 1),
new Among("lich", -1, 1)
],
a_4 = [new Among("end", -1, 1),
new Among("ig", -1, 2), new Among("ung", -1, 1),
new Among("lich", -1, 3), new Among("isch", -1, 2),
new Among("ik", -1, 2), new Among("heit", -1, 3),
new Among("keit", -1, 4)
],
g_v = [17, 65, 16, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 8, 0, 32, 8
],
g_s_ending = [117, 30, 5],
g_st_ending = [
117, 30, 4
],
I_x, I_p2, I_p1, sbp = new SnowballProgram();
this.setCurrent = function(word) {
sbp.setCurrent(word);
};
this.getCurrent = function() {
return sbp.getCurrent();
};
function habr1(c1, c2, v_1) {
if (sbp.eq_s(1, c1)) {
sbp.ket = sbp.cursor;
if (sbp.in_grouping(g_v, 97, 252)) {
sbp.slice_from(c2);
sbp.cursor = v_1;
return true;
}
}
return false;
}
function r_prelude() {
var v_1 = sbp.cursor,
v_2, v_3, v_4, v_5;
while (true) {
v_2 = sbp.cursor;
sbp.bra = v_2;
if (sbp.eq_s(1, "\u00DF")) {
sbp.ket = sbp.cursor;
sbp.slice_from("ss");
} else {
if (v_2 >= sbp.limit)
break;
sbp.cursor = v_2 + 1;
}
}
sbp.cursor = v_1;
while (true) {
v_3 = sbp.cursor;
while (true) {
v_4 = sbp.cursor;
if (sbp.in_grouping(g_v, 97, 252)) {
v_5 = sbp.cursor;
sbp.bra = v_5;
if (habr1("u", "U", v_4))
break;
sbp.cursor = v_5;
if (habr1("y", "Y", v_4))
break;
}
if (v_4 >= sbp.limit) {
sbp.cursor = v_3;
return;
}
sbp.cursor = v_4 + 1;
}
}
}
function habr2() {
while (!sbp.in_grouping(g_v, 97, 252)) {
if (sbp.cursor >= sbp.limit)
return true;
sbp.cursor++;
}
while (!sbp.out_grouping(g_v, 97, 252)) {
if (sbp.cursor >= sbp.limit)
return true;
sbp.cursor++;
}
return false;
}
function r_mark_regions() {
I_p1 = sbp.limit;
I_p2 = I_p1;
var c = sbp.cursor + 3;
if (0 <= c && c <= sbp.limit) {
I_x = c;
if (!habr2()) {
I_p1 = sbp.cursor;
if (I_p1 < I_x)
I_p1 = I_x;
if (!habr2())
I_p2 = sbp.cursor;
}
}
}
function r_postlude() {
var among_var, v_1;
while (true) {
v_1 = sbp.cursor;
sbp.bra = v_1;
among_var = sbp.find_among(a_0, 6);
if (!among_var)
return;
sbp.ket = sbp.cursor;
switch (among_var) {
case 1:
sbp.slice_from("y");
break;
case 2:
case 5:
sbp.slice_from("u");
break;
case 3:
sbp.slice_from("a");
break;
case 4:
sbp.slice_from("o");
break;
case 6:
if (sbp.cursor >= sbp.limit)
return;
sbp.cursor++;
break;
}
}
}
function r_R1() {
return I_p1 <= sbp.cursor;
}
function r_R2() {
return I_p2 <= sbp.cursor;
}
function r_standard_suffix() {
var among_var, v_1 = sbp.limit - sbp.cursor,
v_2, v_3, v_4;
sbp.ket = sbp.cursor;
among_var = sbp.find_among_b(a_1, 7);
if (among_var) {
sbp.bra = sbp.cursor;
if (r_R1()) {
switch (among_var) {
case 1:
sbp.slice_del();
break;
case 2:
sbp.slice_del();
sbp.ket = sbp.cursor;
if (sbp.eq_s_b(1, "s")) {
sbp.bra = sbp.cursor;
if (sbp.eq_s_b(3, "nis"))
sbp.slice_del();
}
break;
case 3:
if (sbp.in_grouping_b(g_s_ending, 98, 116))
sbp.slice_del();
break;
}
}
}
sbp.cursor = sbp.limit - v_1;
sbp.ket = sbp.cursor;
among_var = sbp.find_among_b(a_2, 4);
if (among_var) {
sbp.bra = sbp.cursor;
if (r_R1()) {
switch (among_var) {
case 1:
sbp.slice_del();
break;
case 2:
if (sbp.in_grouping_b(g_st_ending, 98, 116)) {
var c = sbp.cursor - 3;
if (sbp.limit_backward <= c && c <= sbp.limit) {
sbp.cursor = c;
sbp.slice_del();
}
}
break;
}
}
}
sbp.cursor = sbp.limit - v_1;
sbp.ket = sbp.cursor;
among_var = sbp.find_among_b(a_4, 8);
if (among_var) {
sbp.bra = sbp.cursor;
if (r_R2()) {
switch (among_var) {
case 1:
sbp.slice_del();
sbp.ket = sbp.cursor;
if (sbp.eq_s_b(2, "ig")) {
sbp.bra = sbp.cursor;
v_2 = sbp.limit - sbp.cursor;
if (!sbp.eq_s_b(1, "e")) {
sbp.cursor = sbp.limit - v_2;
if (r_R2())
sbp.slice_del();
}
}
break;
case 2:
v_3 = sbp.limit - sbp.cursor;
if (!sbp.eq_s_b(1, "e")) {
sbp.cursor = sbp.limit - v_3;
sbp.slice_del();
}
break;
case 3:
sbp.slice_del();
sbp.ket = sbp.cursor;
v_4 = sbp.limit - sbp.cursor;
if (!sbp.eq_s_b(2, "er")) {
sbp.cursor = sbp.limit - v_4;
if (!sbp.eq_s_b(2, "en"))
break;
}
sbp.bra = sbp.cursor;
if (r_R1())
sbp.slice_del();
break;
case 4:
sbp.slice_del();
sbp.ket = sbp.cursor;
among_var = sbp.find_among_b(a_3, 2);
if (among_var) {
sbp.bra = sbp.cursor;
if (r_R2() && among_var == 1)
sbp.slice_del();
}
break;
}
}
}
}
this.stem = function() {
var v_1 = sbp.cursor;
r_prelude();
sbp.cursor = v_1;
r_mark_regions();
sbp.limit_backward = v_1;
sbp.cursor = sbp.limit;
r_standard_suffix();
sbp.cursor = sbp.limit_backward;
r_postlude();
return true;
}
};
/* and return a function that stems a word for the current locale */
return function(word) {
st.setCurrent(word);
st.stem();
return st.getCurrent();
}
})();
lunr.Pipeline.registerFunction(lunr.de.stemmer, 'stemmer-de');
/* stop word filter function */
lunr.de.stopWordFilter = function(token) {
if (lunr.de.stopWordFilter.stopWords.indexOf(token) === -1) {
return token;
}
};
lunr.de.stopWordFilter.stopWords = new lunr.SortedSet();
lunr.de.stopWordFilter.stopWords.length = 232;
// The space at the beginning is crucial: It marks the empty string
// as a stop word. lunr.js crashes during search when documents
// processed by the pipeline still contain the empty string.
lunr.de.stopWordFilter.stopWords.elements = ' aber alle allem allen aller alles als also am an ander andere anderem anderen anderer anderes anderm andern anderr anders auch auf aus bei bin bis bist da damit dann das dasselbe dazu daß dein deine deinem deinen deiner deines dem demselben den denn denselben der derer derselbe derselben des desselben dessen dich die dies diese dieselbe dieselben diesem diesen dieser dieses dir doch dort du durch ein eine einem einen einer eines einig einige einigem einigen einiger einiges einmal er es etwas euch euer eure eurem euren eurer eures für gegen gewesen hab habe haben hat hatte hatten hier hin hinter ich ihm ihn ihnen ihr ihre ihrem ihren ihrer ihres im in indem ins ist jede jedem jeden jeder jedes jene jenem jenen jener jenes jetzt kann kein keine keinem keinen keiner keines können könnte machen man manche manchem manchen mancher manches mein meine meinem meinen meiner meines mich mir mit muss musste nach nicht nichts noch nun nur ob oder ohne sehr sein seine seinem seinen seiner seines selbst sich sie sind so solche solchem solchen solcher solches soll sollte sondern sonst um und uns unse unsem unsen unser unses unter viel vom von vor war waren warst was weg weil weiter welche welchem welchen welcher welches wenn werde werden wie wieder will wir wird wirst wo wollen wollte während würde würden zu zum zur zwar zwischen über'.split(' ');
lunr.Pipeline.registerFunction(lunr.de.stopWordFilter, 'stopWordFilter-de');
};
}))

View File

@ -0,0 +1,295 @@
/*!
* Snowball JavaScript Library v0.3
* http://code.google.com/p/urim/
* http://snowball.tartarus.org/
*
* Copyright 2010, Oleg Mazko
* http://www.mozilla.org/MPL/
*/
/**
* export the module via AMD, CommonJS or as a browser global
* Export code from https://github.com/umdjs/umd/blob/master/returnExports.js
*/
;(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(factory)
} else if (typeof exports === 'object') {
/**
* Node. Does not work with strict CommonJS, but
* only CommonJS-like environments that support module.exports,
* like Node.
*/
module.exports = factory()
} else {
// Browser globals (root is window)
factory()(root.lunr);
}
}(this, function () {
/**
* Just return a value to define the module export.
* This example returns an object, but the module
* can return a function as the exported value.
*/
return function(lunr) {
/* provides utilities for the included stemmers */
lunr.stemmerSupport = {
Among: function(s, substring_i, result, method) {
this.toCharArray = function(s) {
var sLength = s.length, charArr = new Array(sLength);
for (var i = 0; i < sLength; i++)
charArr[i] = s.charCodeAt(i);
return charArr;
};
if ((!s && s != "") || (!substring_i && (substring_i != 0)) || !result)
throw ("Bad Among initialisation: s:" + s + ", substring_i: "
+ substring_i + ", result: " + result);
this.s_size = s.length;
this.s = this.toCharArray(s);
this.substring_i = substring_i;
this.result = result;
this.method = method;
},
SnowballProgram: function() {
var current;
return {
bra : 0,
ket : 0,
limit : 0,
cursor : 0,
limit_backward : 0,
setCurrent : function(word) {
current = word;
this.cursor = 0;
this.limit = word.length;
this.limit_backward = 0;
this.bra = this.cursor;
this.ket = this.limit;
},
getCurrent : function() {
var result = current;
current = null;
return result;
},
in_grouping : function(s, min, max) {
if (this.cursor < this.limit) {
var ch = current.charCodeAt(this.cursor);
if (ch <= max && ch >= min) {
ch -= min;
if (s[ch >> 3] & (0X1 << (ch & 0X7))) {
this.cursor++;
return true;
}
}
}
return false;
},
in_grouping_b : function(s, min, max) {
if (this.cursor > this.limit_backward) {
var ch = current.charCodeAt(this.cursor - 1);
if (ch <= max && ch >= min) {
ch -= min;
if (s[ch >> 3] & (0X1 << (ch & 0X7))) {
this.cursor--;
return true;
}
}
}
return false;
},
out_grouping : function(s, min, max) {
if (this.cursor < this.limit) {
var ch = current.charCodeAt(this.cursor);
if (ch > max || ch < min) {
this.cursor++;
return true;
}
ch -= min;
if (!(s[ch >> 3] & (0X1 << (ch & 0X7)))) {
this.cursor++;
return true;
}
}
return false;
},
out_grouping_b : function(s, min, max) {
if (this.cursor > this.limit_backward) {
var ch = current.charCodeAt(this.cursor - 1);
if (ch > max || ch < min) {
this.cursor--;
return true;
}
ch -= min;
if (!(s[ch >> 3] & (0X1 << (ch & 0X7)))) {
this.cursor--;
return true;
}
}
return false;
},
eq_s : function(s_size, s) {
if (this.limit - this.cursor < s_size)
return false;
for (var i = 0; i < s_size; i++)
if (current.charCodeAt(this.cursor + i) != s.charCodeAt(i))
return false;
this.cursor += s_size;
return true;
},
eq_s_b : function(s_size, s) {
if (this.cursor - this.limit_backward < s_size)
return false;
for (var i = 0; i < s_size; i++)
if (current.charCodeAt(this.cursor - s_size + i) != s
.charCodeAt(i))
return false;
this.cursor -= s_size;
return true;
},
find_among : function(v, v_size) {
var i = 0, j = v_size, c = this.cursor, l = this.limit, common_i = 0, common_j = 0, first_key_inspected = false;
while (true) {
var k = i + ((j - i) >> 1), diff = 0, common = common_i < common_j
? common_i
: common_j, w = v[k];
for (var i2 = common; i2 < w.s_size; i2++) {
if (c + common == l) {
diff = -1;
break;
}
diff = current.charCodeAt(c + common) - w.s[i2];
if (diff)
break;
common++;
}
if (diff < 0) {
j = k;
common_j = common;
} else {
i = k;
common_i = common;
}
if (j - i <= 1) {
if (i > 0 || j == i || first_key_inspected)
break;
first_key_inspected = true;
}
}
while (true) {
var w = v[i];
if (common_i >= w.s_size) {
this.cursor = c + w.s_size;
if (!w.method)
return w.result;
var res = w.method();
this.cursor = c + w.s_size;
if (res)
return w.result;
}
i = w.substring_i;
if (i < 0)
return 0;
}
},
find_among_b : function(v, v_size) {
var i = 0, j = v_size, c = this.cursor, lb = this.limit_backward, common_i = 0, common_j = 0, first_key_inspected = false;
while (true) {
var k = i + ((j - i) >> 1), diff = 0, common = common_i < common_j
? common_i
: common_j, w = v[k];
for (var i2 = w.s_size - 1 - common; i2 >= 0; i2--) {
if (c - common == lb) {
diff = -1;
break;
}
diff = current.charCodeAt(c - 1 - common) - w.s[i2];
if (diff)
break;
common++;
}
if (diff < 0) {
j = k;
common_j = common;
} else {
i = k;
common_i = common;
}
if (j - i <= 1) {
if (i > 0 || j == i || first_key_inspected)
break;
first_key_inspected = true;
}
}
while (true) {
var w = v[i];
if (common_i >= w.s_size) {
this.cursor = c - w.s_size;
if (!w.method)
return w.result;
var res = w.method();
this.cursor = c - w.s_size;
if (res)
return w.result;
}
i = w.substring_i;
if (i < 0)
return 0;
}
},
replace_s : function(c_bra, c_ket, s) {
var adjustment = s.length - (c_ket - c_bra), left = current
.substring(0, c_bra), right = current.substring(c_ket);
current = left + s + right;
this.limit += adjustment;
if (this.cursor >= c_ket)
this.cursor += adjustment;
else if (this.cursor > c_bra)
this.cursor = c_bra;
return adjustment;
},
slice_check : function() {
if (this.bra < 0 || this.bra > this.ket || this.ket > this.limit
|| this.limit > current.length)
throw ("faulty slice operation");
},
slice_from : function(s) {
this.slice_check();
this.replace_s(this.bra, this.ket, s);
},
slice_del : function() {
this.slice_from("");
},
insert : function(c_bra, c_ket, s) {
var adjustment = this.replace_s(c_bra, c_ket, s);
if (c_bra <= this.bra)
this.bra += adjustment;
if (c_bra <= this.ket)
this.ket += adjustment;
},
slice_to : function() {
this.slice_check();
return current.substring(this.bra, this.ket);
},
eq_v_b : function(s) {
return this.eq_s_b(s.length, s);
}
};
}
};
lunr.trimmerSupport = {
generateTrimmer: function(wordCharacters) {
var startRegex = new RegExp("^[^" + wordCharacters + "]+")
var endRegex = new RegExp("[^" + wordCharacters + "]+$")
return function(token) {
return token
.replace(startRegex, '')
.replace(endRegex, '');
};
}
}
}
}));

94
site/main.js Normal file
View File

@ -0,0 +1,94 @@
let index = null;
let items = null;
async function load() {
let response = await fetch("api/index")
items = await response.json();
index = elasticlunr(function () {
// this.use(elasticlunr.de);
this.addField("search");
this.setRef("id");
});
let i = 0;
for (item of items) {
item.id = i++;
item.search = item.name + " " + item.unit;
item.search = item.search.toLowerCase();
index.addDoc(item);
}
console.log(items.length);
setupUI();
}
function dom(el, html) {
let element = document.createElement(el);
element.innerHTML = html;
return element;
}
function searchItems(query) {
const tokens = query.split(/\s+/).map(token => token.toLowerCase());
const hits = [];
for (item of items) {
let allFound = true;
for (token of tokens) {
if (item.search.indexOf(token) < 0) {
allFound = false;
break;
}
}
if (allFound) hits.push({ doc: item });
}
return hits;
}
function search(query) {
// const hits = index.search(query);
const hits = searchItems(query);
const table = document.querySelector("#result");
const eigenmarken = document.querySelector("#eigenmarken").checked;
const billa = document.querySelector("#billa").checked;
const spar = document.querySelector("#spar").checked;
table.innerHTML = "";
if (hits.ielength == 0) return;
hits.sort((a, b) => {
return a.doc.price - b.doc.price;
})
table.appendChild(dom("tr", `
<th>Kette</th><th>Name</th><th>Menge</th><th>Preis</th>
`));
for (hit of hits) {
const name = hit.doc.name.toLowerCase();
if (hit.doc.store == "billa" && !billa) continue;
if (hit.doc.store == "spar" && !spar) continue;
if (eigenmarken && !(name.indexOf("clever") == 0 || name.indexOf("s-budget") == 0))
continue;
table.appendChild(dom("tr", `
<td>${hit.doc.store}</td>
<td>${hit.doc.name}</td>
<td>${hit.doc.unit}</td>
<td>${hit.doc.price}</td>
`));
}
}
function setupUI() {
const searchInput = document.querySelector("#search");
searchInput.addEventListener("input", (event) => {
search(searchInput.value);
});
document.querySelector("#eigenmarken").addEventListener("change", () => search(searchInput.value));
document.querySelector("#billa").addEventListener("change", () => search(searchInput.value));
document.querySelector("#spar").addEventListener("change", () => search(searchInput.value));
}
load();

34
site/style.css Normal file
View File

@ -0,0 +1,34 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
.main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
max-width: 800px;
margin: 0 auto;
}
.search {
margin: 20px 0;
width: 80%;
font-size: 1.5em;
}
.filters {
margin-bottom: 1em;
}
.results {
display: flex;
flex-direction: row;
width: 100%;
}
table {
width: 100%;
}