mirror of
https://github.com/badlogic/heissepreise.git
synced 2024-06-19 23:18:45 +02:00
commit
14f4b40b3c
75
bundle.js
Executable file → Normal file
75
bundle.js
Executable file → Normal file
|
@ -4,6 +4,7 @@ const chokidar = require("chokidar");
|
||||||
const esbuild = require("esbuild");
|
const esbuild = require("esbuild");
|
||||||
const { exec } = require("child_process");
|
const { exec } = require("child_process");
|
||||||
const { promisify } = require("util");
|
const { promisify } = require("util");
|
||||||
|
const i18n = require("./i18n");
|
||||||
|
|
||||||
function deleteDirectory(directory) {
|
function deleteDirectory(directory) {
|
||||||
if (fs.existsSync(directory)) {
|
if (fs.existsSync(directory)) {
|
||||||
|
@ -19,38 +20,71 @@ function deleteDirectory(directory) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceFileContents(string, fileDir) {
|
/**
|
||||||
const pattern = /%%([^%]+)%%|\/\/\s*include\s*"([^"]+)"/g;
|
* Process file content by resolving includes and translation placeholders.
|
||||||
|
*
|
||||||
|
* @param {string} fileContent original file content
|
||||||
|
* @param {string} fileDir path of the directory of the file
|
||||||
|
* @param {string} locale language code which should be used to resolve translation placeholders
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function replaceFileContents(fileContent, fileDir, locale) {
|
||||||
|
const pattern = /%%([^%]+)%%|__(.+?)__/g;
|
||||||
|
|
||||||
return string.replace(pattern, (_, filename1, filename2) => {
|
return fileContent.replace(pattern, (_, filename, translationKey) => {
|
||||||
const filename = filename1 || filename2;
|
if (filename != undefined) {
|
||||||
const filenamePath = path.join(fileDir, filename);
|
const filenamePath = path.join(fileDir, filename);
|
||||||
try {
|
try {
|
||||||
const data = fs.readFileSync(filenamePath, "utf8");
|
const data = fs.readFileSync(filenamePath, "utf8");
|
||||||
const replacedData = replaceFileContents(data, path.dirname(filenamePath));
|
const replacedData = replaceFileContents(data, path.dirname(filenamePath), locale);
|
||||||
return replacedData;
|
return replacedData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error reading file "${filenamePath}":`, error);
|
console.error(`Error reading file "${filenamePath}":`, error);
|
||||||
return "";
|
return "";
|
||||||
|
}
|
||||||
|
} else if (translationKey != undefined) {
|
||||||
|
return i18n.translateWithLocale(locale, translationKey);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy inputFile to outputFile, possibly modifying it in the process.
|
||||||
|
*
|
||||||
|
* @param {string} inputFile path
|
||||||
|
* @param {string} outputFile path
|
||||||
|
* @param {function(string, boolean, string):boolean} filter takes path, whether it is a directory and data (or null if directory), returns true if the file should be left out
|
||||||
|
*/
|
||||||
function processFile(inputFile, outputFile, filter) {
|
function processFile(inputFile, outputFile, filter) {
|
||||||
const fileDir = path.dirname(inputFile);
|
let extension = path.extname(inputFile);
|
||||||
if (inputFile.includes(".mp3")) {
|
if (extension == ".html") {
|
||||||
|
const data = fs.readFileSync(inputFile, "utf8");
|
||||||
|
if (filter(inputFile, false, data)) return;
|
||||||
|
for (const locale of i18n.locales) {
|
||||||
|
const replacedData = replaceFileContents(data, path.dirname(inputFile), locale);
|
||||||
|
if (locale == i18n.defaultLocale) {
|
||||||
|
fs.writeFileSync(outputFile, replacedData);
|
||||||
|
console.log(`${inputFile} -> ${outputFile}`);
|
||||||
|
}
|
||||||
|
let pathWithLanguageCode = outputFile.substring(0, outputFile.length - extension.length) + "." + locale + extension;
|
||||||
|
fs.writeFileSync(pathWithLanguageCode, replacedData);
|
||||||
|
console.log(`${inputFile} -> ${pathWithLanguageCode}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const data = fs.readFileSync(inputFile);
|
const data = fs.readFileSync(inputFile);
|
||||||
if (filter(inputFile, false, data)) return;
|
if (filter(inputFile, false, data)) return;
|
||||||
fs.writeFileSync(outputFile, data);
|
fs.writeFileSync(outputFile, data);
|
||||||
} else {
|
console.log(`${inputFile} -> ${outputFile}`);
|
||||||
const data = fs.readFileSync(inputFile, "utf8");
|
|
||||||
if (filter(inputFile, false, data)) return;
|
|
||||||
const replacedData = replaceFileContents(data, fileDir);
|
|
||||||
fs.writeFileSync(outputFile, replacedData);
|
|
||||||
}
|
}
|
||||||
console.log(`${inputFile} -> ${outputFile}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} inputDir path to the input directory, traversed recursively, outputDir and files/directories starting with _ are automatically skipped
|
||||||
|
* @param {string} outputDir path to the output directory
|
||||||
|
* @param {boolean} deleteOutput whether the contents of output directory should be deleted first
|
||||||
|
* @param {function(string, boolean, string):boolean} filter takes path, whether it is a directory and data (or null if directory), returns true if the file should be left out
|
||||||
|
*/
|
||||||
function generateSite(inputDir, outputDir, deleteOutput, filter) {
|
function generateSite(inputDir, outputDir, deleteOutput, filter) {
|
||||||
if (deleteOutput) {
|
if (deleteOutput) {
|
||||||
deleteDirectory(outputDir);
|
deleteDirectory(outputDir);
|
||||||
|
@ -135,6 +169,7 @@ async function bundle(inputDir, outputDir, watch) {
|
||||||
bundleHTML(inputDir, outputDir, false, watch, (filePath, isDir, data) => {
|
bundleHTML(inputDir, outputDir, false, watch, (filePath, isDir, data) => {
|
||||||
if (isDir) return false;
|
if (isDir) return false;
|
||||||
if (filePath.endsWith("style.css")) return true;
|
if (filePath.endsWith("style.css")) return true;
|
||||||
|
if (filePath.includes("/locales/")) return true;
|
||||||
if (filePath.endsWith(".js") && !filePath.includes("socket.io.js")) return true;
|
if (filePath.endsWith(".js") && !filePath.includes("socket.io.js")) return true;
|
||||||
if (data.includes(`require("`)) return true;
|
if (data.includes(`require("`)) return true;
|
||||||
return false;
|
return false;
|
||||||
|
|
61
i18n.js
Normal file
61
i18n.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// Internationalization support
|
||||||
|
// This file is used both serverside and clientside.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported languages and their translations.
|
||||||
|
* Each file contains an object with key-value pairs.
|
||||||
|
*
|
||||||
|
* Argument substitution is supported through named arguments: translate("Hello {{name}}", {name: "World"})
|
||||||
|
*
|
||||||
|
* Plurals and arrays are currently not supported.
|
||||||
|
*
|
||||||
|
* If the key is missing in the current localization, it falls back to default localization
|
||||||
|
* and then to treating the key itself as the localization.
|
||||||
|
*
|
||||||
|
* @type {Object.<string, Object.<string, string>>}
|
||||||
|
*/
|
||||||
|
const translations = {
|
||||||
|
// sorted alphabetically
|
||||||
|
cs: require("./locales/cs.json"),
|
||||||
|
de: require("./locales/de.json"),
|
||||||
|
en: require("./locales/en.json"),
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
const locales = Object.keys(translations);
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const defaultLocale = "de";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!string} locale name of the language to use for translation, MUST be one of the supported languages
|
||||||
|
* @param {!string} key to translate
|
||||||
|
* @param {!Object.<string, string>} [args] arguments to substitute into the translated key
|
||||||
|
* @returns {string} translated string
|
||||||
|
*/
|
||||||
|
function translateWithLocale(locale, key, args) {
|
||||||
|
let translation = translations[locale][key];
|
||||||
|
if (translation === undefined) {
|
||||||
|
console.error("Untranslated key in ", locale, ": ", key);
|
||||||
|
if (locale != defaultLocale) {
|
||||||
|
translation = translations[defaultLocale][key] || key;
|
||||||
|
} else {
|
||||||
|
translation = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args === "object") {
|
||||||
|
// Do argument substitution
|
||||||
|
for (const arg in args) {
|
||||||
|
translation = translation.replaceAll("{{" + arg + "}}", args[arg]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return translation;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.defaultLocale = defaultLocale;
|
||||||
|
exports.locales = locales;
|
||||||
|
exports.translateWithLocale = translateWithLocale;
|
100
locales/cs.json
Normal file
100
locales/cs.json
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
{
|
||||||
|
"Heisse Preise": "Zlevněno?",
|
||||||
|
"page_description": "Nekomerční open source projekt umožňující spotřebitelům najít nejlevnější verzi produktu v obchodech.",
|
||||||
|
"Einstellungen": "Nastavení",
|
||||||
|
"Impressum": "Impressum",
|
||||||
|
"Logs": "Logy",
|
||||||
|
"Historische Daten von": "Historická data od",
|
||||||
|
"Alle Angaben ohne Gewähr, Irrtümer vorbehalten.": "Veškeré informace jsou poskytovány bez záruky, chyby vyhrazeny.",
|
||||||
|
"Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.": "Názvy značek a ochranné známky jsou majetkem příslušných vlastníků.",
|
||||||
|
"Suche": "Vyhledávání",
|
||||||
|
"Preisänderungen": "Změny cen",
|
||||||
|
"Warenkörbe": "Nákupní vozíky",
|
||||||
|
|
||||||
|
"Noch keine Produkte im Warenkorb.": "Nákupní košík je prázdný.",
|
||||||
|
"Produkte suchen und mit '+' zum Warenkorb hinzufügen.": "Vyhledejte produkty a přidejte je do nákupního košíku pomocí '+'.",
|
||||||
|
"Filtern...": "Filtr...",
|
||||||
|
"(min. 3 Zeichen)": "(alespoň 3 znaky)",
|
||||||
|
"Produkt hinzufügen...": "Přidat produkt...",
|
||||||
|
|
||||||
|
"Neuer Warenkorb": "Nový nákupní košík",
|
||||||
|
"Exportieren": "Export",
|
||||||
|
"Importieren": "Import",
|
||||||
|
|
||||||
|
"Medieninhaber": "Majitel",
|
||||||
|
"Kontakt": "Kontakt",
|
||||||
|
"Adresse": "Adresa",
|
||||||
|
"Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.": "Tato nekomerční stránka umožňuje spotřebitelům porovnávat ceny produktů v obchodě s potravinami.",
|
||||||
|
|
||||||
|
"Video Anleitung": "Video instrukce (německy)",
|
||||||
|
"Text Anleitung": "Textové instrukce (německy)",
|
||||||
|
"Medienberichte": "Napsali o nás",
|
||||||
|
"Produktsuche": "Vyhledávání produktů",
|
||||||
|
|
||||||
|
"Radio & Fernsehen": "Rádio & Televize",
|
||||||
|
"Print & Online": "Tisk & Online",
|
||||||
|
|
||||||
|
"CartsList_Name": "Název",
|
||||||
|
"CartsList_Produkte": "Produkt",
|
||||||
|
"CartsList_Preis": "Cena",
|
||||||
|
|
||||||
|
"CartsList_Preisänderungen": "Změny cen",
|
||||||
|
"CartsList_Teilen": "Sdílet",
|
||||||
|
"CartsList_JSON": "JSON",
|
||||||
|
"CartsList_Löschen": "Smazat",
|
||||||
|
|
||||||
|
"ItemsChart_Keine Daten ausgewählt": "Nejsou vybrána žádná data",
|
||||||
|
"ItemsChart_Preissumme Gesamt": "Celková cena",
|
||||||
|
"ItemsChart_Preissumme Ketten": "Cena řetězce",
|
||||||
|
"ItemsChart_Nur heutige Preise": "Cena dnes",
|
||||||
|
"ItemsChart_Änderung in % seit": "Změna v % od",
|
||||||
|
"ItemsChart_Änderung in % seit {{date}}": "Změna v % od {{date}}",
|
||||||
|
"ItemsChart_Preissumme {{s}}": "Cena {{s}}",
|
||||||
|
|
||||||
|
"ItemsFilter_Produkte suchen...": "Vyhledávání produktů...",
|
||||||
|
"ItemsFilter_Filter anzeigen/ausblenden": "Zobrazit/skrýt filtry",
|
||||||
|
"ItemsFilter_Alle": "Vše",
|
||||||
|
"ItemsFilter_Datum": "Datum",
|
||||||
|
"ItemsFilter_Billiger seit letzter Änderung": "Levnější od poslední změny",
|
||||||
|
"ItemsFilter_Nur Diskont-Eigenmarken": "Pouze privátní značky",
|
||||||
|
"ItemsFilter_Nur Bio": "Pouze bio",
|
||||||
|
"ItemsFilter_Exaktes Wort": "Přesná slova",
|
||||||
|
"ItemsFilter_Preis €": "Cena €",
|
||||||
|
"ItemsFilter_Teurer": "Dražší",
|
||||||
|
"ItemsFilter_Billiger": "Levnější",
|
||||||
|
|
||||||
|
"ItemsList_Resultate": "Výsledky",
|
||||||
|
"ItemsList_Diagramm": "Graf",
|
||||||
|
"ItemsList_Verkaufspreis": "Prodejní cena",
|
||||||
|
"ItemsList_Mengenpreis": "Měrná cena",
|
||||||
|
"ItemsList_Sortieren": "Seřadit podle",
|
||||||
|
"ItemsList_Preis aufsteigend": "Cena vzestupně",
|
||||||
|
"ItemsList_Preis absteigend": "Cena sestupně",
|
||||||
|
"ItemsList_Menge aufsteigend": "Množství vzsetupně",
|
||||||
|
"ItemsList_Menge absteigend": "Množství sestupně",
|
||||||
|
"ItemsList_Kette & Name": "Řetězec & název",
|
||||||
|
"ItemsList_Namensähnlichkeit": "Podobnost jména",
|
||||||
|
"ItemsList_Kette": "Řetězec",
|
||||||
|
"ItemsList_Name": "Název",
|
||||||
|
"ItemsList_Preis": "Cena",
|
||||||
|
|
||||||
|
"Cart_Teilen": "Detail",
|
||||||
|
"Cart_Speichern": "Uložit",
|
||||||
|
"Cart_Warenkorb {{name}}": "Nákupní košík {{name}}",
|
||||||
|
"Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben": "Nákupní košík '{{name}}' již existuje. Zadejte prosím jiný název",
|
||||||
|
"Cart_Warenkorb '{{name}}' existiert nicht.": "Nákupní košík '{{name}}' neexistuje.",
|
||||||
|
"Cart_Artikel": "Položka",
|
||||||
|
|
||||||
|
"Carts_Name für Warenkorb eingeben:": "Zadejte název nákupního košíku:",
|
||||||
|
"Carts_Warenkorb mit Namen '{{name}}' existiert bereits": "Nákupní košík se jménem '{{name}}' již existuje",
|
||||||
|
"Carts_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben": "Nákupní košík '{{name}}' již existuje. Zadejte prosím jiný název pod který se nákupní košík importuje",
|
||||||
|
|
||||||
|
"Settings_Vorselektierte Ketten": "Předvybrané řetězce",
|
||||||
|
"Settings_Start-Datum für Diagramme": "Počáteční datum pro grafy",
|
||||||
|
"Settings_Diagramm Typ": "Typ grafu",
|
||||||
|
"Settings_Stufen": "Schodový",
|
||||||
|
"Settings_Linien": "Čárový",
|
||||||
|
"Settings_Nur verfügbare Produkte anzeigen": "Zobrazit pouze dostupné produkty",
|
||||||
|
"Settings_Diagramm immer anzeigen (wenn verfügbar)": "Vždy zobrazovat graf (pokud je k dispozici)",
|
||||||
|
"Settings_Suche immer anzeigen (wenn verfügbar)": "Vždy zobrazit vyhledávání (je-li k dispozici)"
|
||||||
|
}
|
100
locales/de.json
Normal file
100
locales/de.json
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
{
|
||||||
|
"Heisse Preise": "Heisse Preise",
|
||||||
|
"page_description": "Nicht-kommerzielles Open-Source-Projekt um KonsumentInnen es zu ermöglichen, die günstigste Variante eines Produktes im Handel ausfindig zu machen.",
|
||||||
|
"Einstellungen": "Einstellungen",
|
||||||
|
"Impressum": "Impressum",
|
||||||
|
"Logs": "Logs",
|
||||||
|
"Historische Daten von": "Historische Daten von",
|
||||||
|
"Alle Angaben ohne Gewähr, Irrtümer vorbehalten.": "Alle Angaben ohne Gewähr, Irrtümer vorbehalten.",
|
||||||
|
"Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.": "Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.",
|
||||||
|
"Suche": "Suche",
|
||||||
|
"Preisänderungen": "Preisänderungen",
|
||||||
|
"Warenkörbe": "Warenkörbe",
|
||||||
|
|
||||||
|
"Noch keine Produkte im Warenkorb.": "Noch keine Produkte im Warenkorb.",
|
||||||
|
"Produkte suchen und mit '+' zum Warenkorb hinzufügen.": "Produkte suchen und mit '+' zum Warenkorb hinzufügen.",
|
||||||
|
"Filtern...": "Filtern...",
|
||||||
|
"(min. 3 Zeichen)": "(min. 3 Zeichen)",
|
||||||
|
"Produkt hinzufügen...": "Produkt hinzufügen...",
|
||||||
|
|
||||||
|
"Neuer Warenkorb": "Neuer Warenkorb",
|
||||||
|
"Exportieren": "Exportieren",
|
||||||
|
"Importieren": "Importieren",
|
||||||
|
|
||||||
|
"Medieninhaber": "Medieninhaber",
|
||||||
|
"Kontakt": "Kontakt",
|
||||||
|
"Adresse": "Adresse",
|
||||||
|
"Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.": "Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.",
|
||||||
|
|
||||||
|
"Video Anleitung": "Video Anleitung",
|
||||||
|
"Text Anleitung": "Text Anleitung",
|
||||||
|
"Medienberichte": "Medienberichte",
|
||||||
|
"Produktsuche": "Produktsuche",
|
||||||
|
|
||||||
|
"Radio & Fernsehen": "Radio & Fernsehen",
|
||||||
|
"Print & Online": "Print & Online",
|
||||||
|
|
||||||
|
"CartsList_Name": "Name",
|
||||||
|
"CartsList_Produkte": "Produkte",
|
||||||
|
"CartsList_Preis": "Preis",
|
||||||
|
|
||||||
|
"CartsList_Preisänderungen": "Preisänderungen",
|
||||||
|
"CartsList_Teilen": "Teilen",
|
||||||
|
"CartsList_JSON": "JSON",
|
||||||
|
"CartsList_Löschen": "Löschen",
|
||||||
|
|
||||||
|
"ItemsChart_Keine Daten ausgewählt": "Keine Daten ausgewählt",
|
||||||
|
"ItemsChart_Preissumme Gesamt": "Preissumme Gesamt",
|
||||||
|
"ItemsChart_Preissumme Ketten": "Preissumme Ketten",
|
||||||
|
"ItemsChart_Nur heutige Preise": "Nur heutige Preise",
|
||||||
|
"ItemsChart_Änderung in % seit": "Änderung in % seit",
|
||||||
|
"ItemsChart_Änderung in % seit {{date}}": "Änderung in % seit {{date}}",
|
||||||
|
"ItemsChart_Preissumme {{s}}": "Preissumme {{s}}",
|
||||||
|
|
||||||
|
"ItemsFilter_Produkte suchen...": "Produkte suchen...",
|
||||||
|
"ItemsFilter_Filter anzeigen/ausblenden": "Filter anzeigen/ausblenden",
|
||||||
|
"ItemsFilter_Alle": "Alle",
|
||||||
|
"ItemsFilter_Datum": "Datum",
|
||||||
|
"ItemsFilter_Billiger seit letzter Änderung": "Billiger seit letzter Änderung",
|
||||||
|
"ItemsFilter_Nur Diskont-Eigenmarken": "Nur Diskont-Eigenmarken",
|
||||||
|
"ItemsFilter_Nur Bio": "Nur Bio",
|
||||||
|
"ItemsFilter_Exaktes Wort": "Exaktes Wort",
|
||||||
|
"ItemsFilter_Preis €": "Preis €",
|
||||||
|
"ItemsFilter_Teurer": "Teurer",
|
||||||
|
"ItemsFilter_Billiger": "Billiger",
|
||||||
|
|
||||||
|
"ItemsList_Resultate": "Resultate",
|
||||||
|
"ItemsList_Diagramm": "Diagramm",
|
||||||
|
"ItemsList_Verkaufspreis": "Verkaufspreis",
|
||||||
|
"ItemsList_Mengenpreis": "Mengenpreis",
|
||||||
|
"ItemsList_Sortieren": "Sortieren",
|
||||||
|
"ItemsList_Preis aufsteigend": "Preis aufsteigend",
|
||||||
|
"ItemsList_Preis absteigend": "Preis absteigend",
|
||||||
|
"ItemsList_Menge aufsteigend": "Menge aufsteigend",
|
||||||
|
"ItemsList_Menge absteigend": "Menge absteigend",
|
||||||
|
"ItemsList_Kette & Name": "Kette & Name",
|
||||||
|
"ItemsList_Namensähnlichkeit": "Namensähnlichkeit",
|
||||||
|
"ItemsList_Kette": "Kette",
|
||||||
|
"ItemsList_Name": "Name",
|
||||||
|
"ItemsList_Preis": "Preis",
|
||||||
|
|
||||||
|
"Cart_Teilen": "Teilen",
|
||||||
|
"Cart_Speichern": "Speichern",
|
||||||
|
"Cart_Warenkorb {{name}}": "Warenkorb {{name}}",
|
||||||
|
"Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben": "Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben",
|
||||||
|
"Cart_Warenkorb '{{name}}' existiert nicht.": "Warenkorb '{{name}}' existiert nicht.",
|
||||||
|
"Cart_Artikel": "Artikel",
|
||||||
|
|
||||||
|
"Carts_Name für Warenkorb eingeben:": "Name für Warenkorb eingeben:",
|
||||||
|
"Carts_Warenkorb mit Namen '{{name}}' existiert bereits": "Warenkorb mit Namen '{{name}}' existiert bereits",
|
||||||
|
"Carts_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben": "Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben",
|
||||||
|
|
||||||
|
"Settings_Vorselektierte Ketten": "Vorselektierte Ketten",
|
||||||
|
"Settings_Start-Datum für Diagramme": "Start-Datum für Diagramme",
|
||||||
|
"Settings_Diagramm Typ": "Diagramm Typ",
|
||||||
|
"Settings_Stufen": "Stufen",
|
||||||
|
"Settings_Linien": "Linien",
|
||||||
|
"Settings_Nur verfügbare Produkte anzeigen": "Nur verfügbare Produkte anzeigen",
|
||||||
|
"Settings_Diagramm immer anzeigen (wenn verfügbar)": "Diagramm immer anzeigen (wenn verfügbar)",
|
||||||
|
"Settings_Suche immer anzeigen (wenn verfügbar)": "Suche immer anzeigen (wenn verfügbar)"
|
||||||
|
}
|
100
locales/en.json
Normal file
100
locales/en.json
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
{
|
||||||
|
"Heisse Preise": "Hot Prices",
|
||||||
|
"page_description": "Non-commercial open source project to enable consumers to find the cheapest version of a product in stores.",
|
||||||
|
"Einstellungen": "Settings",
|
||||||
|
"Impressum": "Imprint",
|
||||||
|
"Logs": "Logs",
|
||||||
|
"Historische Daten von": "Historic data from",
|
||||||
|
"Alle Angaben ohne Gewähr, Irrtümer vorbehalten.": "All information provided without guarantee, errors excepted.",
|
||||||
|
"Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.": "Brand names and trademarks are the property of their respective owners.",
|
||||||
|
"Suche": "Search",
|
||||||
|
"Preisänderungen": "Price changes",
|
||||||
|
"Warenkörbe": "Shopping carts",
|
||||||
|
|
||||||
|
"Noch keine Produkte im Warenkorb.": "No products in your shopping cart yet.",
|
||||||
|
"Produkte suchen und mit '+' zum Warenkorb hinzufügen.": "Search for products and add them to the shopping cart with '+'.",
|
||||||
|
"Filtern...": "Filter...",
|
||||||
|
"(min. 3 Zeichen)": "(at least 3 characters)",
|
||||||
|
"Produkt hinzufügen...": "Add product...",
|
||||||
|
|
||||||
|
"Neuer Warenkorb": "New shopping cart",
|
||||||
|
"Exportieren": "Export",
|
||||||
|
"Importieren": "Import",
|
||||||
|
|
||||||
|
"Medieninhaber": "Owner",
|
||||||
|
"Kontakt": "Contact",
|
||||||
|
"Adresse": "Address",
|
||||||
|
"Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.": "This non-commercial site allows consumers to compare prices of products in the grocery store.",
|
||||||
|
|
||||||
|
"Video Anleitung": "Video instructions (in German)",
|
||||||
|
"Text Anleitung": "Text instructions (in German)",
|
||||||
|
"Medienberichte": "Media reports",
|
||||||
|
"Produktsuche": "Product search",
|
||||||
|
|
||||||
|
"Radio & Fernsehen": "Radio & Television",
|
||||||
|
"Print & Online": "Print & Online",
|
||||||
|
|
||||||
|
"CartsList_Name": "Name",
|
||||||
|
"CartsList_Produkte": "Product",
|
||||||
|
"CartsList_Preis": "Price",
|
||||||
|
|
||||||
|
"CartsList_Preisänderungen": "Price changes",
|
||||||
|
"CartsList_Teilen": "Share",
|
||||||
|
"CartsList_JSON": "JSON",
|
||||||
|
"CartsList_Löschen": "Delete",
|
||||||
|
|
||||||
|
"ItemsChart_Keine Daten ausgewählt": "No data selected",
|
||||||
|
"ItemsChart_Preissumme Gesamt": "Total price",
|
||||||
|
"ItemsChart_Preissumme Ketten": "Store price",
|
||||||
|
"ItemsChart_Nur heutige Preise": "Price today",
|
||||||
|
"ItemsChart_Änderung in % seit": "Change in % since",
|
||||||
|
"ItemsChart_Änderung in % seit {{date}}": "Change in % since {{date}}",
|
||||||
|
"ItemsChart_Preissumme {{s}}": "Price {{s}}",
|
||||||
|
|
||||||
|
"ItemsFilter_Produkte suchen...": "Product search...",
|
||||||
|
"ItemsFilter_Filter anzeigen/ausblenden": "Show/hide filters",
|
||||||
|
"ItemsFilter_Alle": "All",
|
||||||
|
"ItemsFilter_Datum": "Date",
|
||||||
|
"ItemsFilter_Billiger seit letzter Änderung": "Cheaper since last change",
|
||||||
|
"ItemsFilter_Nur Diskont-Eigenmarken": "Private brands only",
|
||||||
|
"ItemsFilter_Nur Bio": "Only bio",
|
||||||
|
"ItemsFilter_Exaktes Wort": "Exact word",
|
||||||
|
"ItemsFilter_Preis €": "Price €",
|
||||||
|
"ItemsFilter_Teurer": "More expensive",
|
||||||
|
"ItemsFilter_Billiger": "Cheaper",
|
||||||
|
|
||||||
|
"ItemsList_Resultate": "Results",
|
||||||
|
"ItemsList_Diagramm": "Chart",
|
||||||
|
"ItemsList_Verkaufspreis": "Unit price",
|
||||||
|
"ItemsList_Mengenpreis": "Bulk price",
|
||||||
|
"ItemsList_Sortieren": "Sort by",
|
||||||
|
"ItemsList_Preis aufsteigend": "Price ascending",
|
||||||
|
"ItemsList_Preis absteigend": "Price descending",
|
||||||
|
"ItemsList_Menge aufsteigend": "Quantity ascending",
|
||||||
|
"ItemsList_Menge absteigend": "Quantity descending",
|
||||||
|
"ItemsList_Kette & Name": "Store chain & name",
|
||||||
|
"ItemsList_Namensähnlichkeit": "Name similarity",
|
||||||
|
"ItemsList_Kette": "Store chain",
|
||||||
|
"ItemsList_Name": "Name",
|
||||||
|
"ItemsList_Preis": "Price",
|
||||||
|
|
||||||
|
"Cart_Teilen": "Detail",
|
||||||
|
"Cart_Speichern": "Save",
|
||||||
|
"Cart_Warenkorb {{name}}": "Shopping cart {{name}}",
|
||||||
|
"Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben": "Shopping cart '{{name}}' already exists. Please enter a different name for the shopping cart to be saved",
|
||||||
|
"Cart_Warenkorb '{{name}}' existiert nicht.": "Shopping cart '{{name}}' does not exist.",
|
||||||
|
"Cart_Artikel": "Item",
|
||||||
|
|
||||||
|
"Carts_Name für Warenkorb eingeben:": "Enter name for shopping cart:",
|
||||||
|
"Carts_Warenkorb mit Namen '{{name}}' existiert bereits": "Shopping cart with name '{{name}}' already exists",
|
||||||
|
"Carts_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben": "Shopping cart '{{name}}' already exists. Please enter a different name for the shopping cart to be imported",
|
||||||
|
|
||||||
|
"Settings_Vorselektierte Ketten": "Pre-selected store chains",
|
||||||
|
"Settings_Start-Datum für Diagramme": "Start date for charts",
|
||||||
|
"Settings_Diagramm Typ": "Chart type",
|
||||||
|
"Settings_Stufen": "Stepped",
|
||||||
|
"Settings_Linien": "Lines",
|
||||||
|
"Settings_Nur verfügbare Produkte anzeigen": "Show only available products",
|
||||||
|
"Settings_Diagramm immer anzeigen (wenn verfügbar)": "Always show chart (when available)",
|
||||||
|
"Settings_Suche immer anzeigen (wenn verfügbar)": "Always show search (when available)"
|
||||||
|
}
|
48
server.js
48
server.js
|
@ -7,6 +7,7 @@ const csv = require("./site/js/misc");
|
||||||
const chokidar = require("chokidar");
|
const chokidar = require("chokidar");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const compression = require("compression");
|
const compression = require("compression");
|
||||||
|
const i18n = require("./i18n");
|
||||||
|
|
||||||
function copyItemsToSite(dataDir) {
|
function copyItemsToSite(dataDir) {
|
||||||
const items = analysis.readJSON(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`);
|
const items = analysis.readJSON(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`);
|
||||||
|
@ -45,6 +46,7 @@ function parseArguments() {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
let port = process.env.PORT !== undefined && process.env.PORT != "" ? parseInt(process.env.PORT) : 3000;
|
let port = process.env.PORT !== undefined && process.env.PORT != "" ? parseInt(process.env.PORT) : 3000;
|
||||||
let liveReload = process.env.NODE_ENV === "development" || false;
|
let liveReload = process.env.NODE_ENV === "development" || false;
|
||||||
|
let skipDataUpdate = false;
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
if (args[i] === "-p" || args[i] === "--port") {
|
if (args[i] === "-p" || args[i] === "--port") {
|
||||||
port = parseInt(args[i + 1]);
|
port = parseInt(args[i + 1]);
|
||||||
|
@ -54,6 +56,8 @@ function parseArguments() {
|
||||||
throw new Error("Live reload is only supported in development mode");
|
throw new Error("Live reload is only supported in development mode");
|
||||||
}
|
}
|
||||||
liveReload = true;
|
liveReload = true;
|
||||||
|
} else if (args[i] === "--skip-data-update") {
|
||||||
|
skipDataUpdate = true;
|
||||||
} else if (args[i] === "-h" || args[i] === "--help") {
|
} else if (args[i] === "-h" || args[i] === "--help") {
|
||||||
console.log("Usage: node server.js [-p|--port PORT] [-l|--live-reload]");
|
console.log("Usage: node server.js [-p|--port PORT] [-l|--live-reload]");
|
||||||
console.log();
|
console.log();
|
||||||
|
@ -64,7 +68,7 @@ function parseArguments() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { port, liveReload };
|
return { port, liveReload, skipDataUpdate };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLogging() {
|
function setupLogging() {
|
||||||
|
@ -81,7 +85,7 @@ function setupLogging() {
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const dataDir = "data";
|
const dataDir = "data";
|
||||||
const { port, liveReload } = parseArguments();
|
const { port, liveReload, skipDataUpdate } = parseArguments();
|
||||||
|
|
||||||
if (!fs.existsSync(dataDir)) {
|
if (!fs.existsSync(dataDir)) {
|
||||||
fs.mkdirSync(dataDir);
|
fs.mkdirSync(dataDir);
|
||||||
|
@ -102,25 +106,45 @@ function setupLogging() {
|
||||||
setupLogging();
|
setupLogging();
|
||||||
bundle.bundle("site", outputDir, liveReload);
|
bundle.bundle("site", outputDir, liveReload);
|
||||||
|
|
||||||
analysis.migrateCompression(dataDir, ".json", ".json.br");
|
if (!skipDataUpdate) {
|
||||||
analysis.migrateCompression(dataDir, ".json.gz", ".json.br");
|
analysis.migrateCompression(dataDir, ".json", ".json.br");
|
||||||
|
analysis.migrateCompression(dataDir, ".json.gz", ".json.br");
|
||||||
|
|
||||||
if (fs.existsSync(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`)) {
|
if (fs.existsSync(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`)) {
|
||||||
copyItemsToSite(dataDir);
|
copyItemsToSite(dataDir);
|
||||||
analysis.updateData(dataDir, (_newItems) => {
|
analysis.updateData(dataDir, (_newItems) => {
|
||||||
|
copyItemsToSite(dataDir);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await analysis.updateData(dataDir);
|
||||||
|
copyItemsToSite(dataDir);
|
||||||
|
}
|
||||||
|
scheduleFunction(5, 0, 0, async () => {
|
||||||
|
items = await analysis.updateData(dataDir);
|
||||||
copyItemsToSite(dataDir);
|
copyItemsToSite(dataDir);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await analysis.updateData(dataDir);
|
|
||||||
copyItemsToSite(dataDir);
|
copyItemsToSite(dataDir);
|
||||||
}
|
}
|
||||||
scheduleFunction(5, 0, 0, async () => {
|
|
||||||
items = await analysis.updateData(dataDir);
|
|
||||||
copyItemsToSite(dataDir);
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
app.use(function (req, res, next) {
|
||||||
|
if (req.method == "GET") {
|
||||||
|
if (req.path == "/") {
|
||||||
|
req.url = "/index.html";
|
||||||
|
}
|
||||||
|
if (req.path.endsWith(".html")) {
|
||||||
|
// Only html files are translated
|
||||||
|
let pickedLanguage = req.acceptsLanguages(i18n.locales);
|
||||||
|
if (pickedLanguage) {
|
||||||
|
let translatedPath = req.path.substring(0, req.path.length - "html".length) + pickedLanguage + ".html";
|
||||||
|
req.url = translatedPath;
|
||||||
|
} // otherwise use default, untranslated file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
app.use(express.static("site/output"));
|
app.use(express.static("site/output"));
|
||||||
const server = http.createServer(app).listen(port, () => {
|
const server = http.createServer(app).listen(port, () => {
|
||||||
console.log(`App listening on port ${port}`);
|
console.log(`App listening on port ${port}`);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<footer>
|
<footer>
|
||||||
<div class="flex align-center justify-center gap-2 pt-4">
|
<div class="flex align-center justify-center gap-2 pt-4">
|
||||||
<a class="font-medium" href="settings.html">Einstellungen</a>
|
<a class="font-medium" href="settings.html">__Einstellungen__</a>
|
||||||
<a class="font-medium" href="imprint.html">Impressum</a>
|
<a class="font-medium" href="imprint.html">__Impressum__</a>
|
||||||
<a class="font-medium" href="data/log.txt">Logs</a>
|
<a class="font-medium" href="data/log.txt">__Logs__</a>
|
||||||
<a href="https://twitter.com/badlogicgames" style="width: 24px;" aria-label="twitter.com/badlogicgames"><svg style="padding-top: 2px;" viewBox="328 355 335 276" xmlns="http://www.w3.org/2000/svg">
|
<a href="https://twitter.com/badlogicgames" style="width: 24px;" aria-label="twitter.com/badlogicgames"><svg style="padding-top: 2px;" viewBox="328 355 335 276" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="
|
<path d="
|
||||||
M 630, 425
|
M 630, 425
|
||||||
|
@ -25,11 +25,11 @@
|
||||||
<a href="https://github.com/badlogic/heissepreise" aria-label="github.com/badlogic/heissepreise"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg></a>
|
<a href="https://github.com/badlogic/heissepreise" aria-label="github.com/badlogic/heissepreise"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex align-center justify-center gap-2 pt-4 pb-2">
|
<div class="flex align-center justify-center gap-2 pt-4 pb-2">
|
||||||
<span>Historische Daten von <a href="http://h.43z.one" class="font-medium">@h43z</a> & <a href="https://www.dossier.at/dossiers/supermaerkte/quellen/anatomie-eines-supermarkts-die-methodik/" class="font-medium">Dossier</a></span>
|
<span>__Historische Daten von__ <a href="http://h.43z.one" class="font-medium">@h43z</a> & <a href="https://www.dossier.at/dossiers/supermaerkte/quellen/anatomie-eines-supermarkts-die-methodik/" class="font-medium">Dossier</a></span>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-center mb-6">
|
<small class="text-center mb-6">
|
||||||
<p>Alle Angaben ohne Gewähr, Irrtümer vorbehalten. <br />
|
<p>__Alle Angaben ohne Gewähr, Irrtümer vorbehalten.__<br />
|
||||||
Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.
|
__Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.__
|
||||||
</p>
|
</p>
|
||||||
</small>
|
</small>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<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" />
|
||||||
<meta name="description" content="Nicht-kommerzielles Open-Source-Projekt um KonsumentInnen es zu ermöglichen, die günstigste Variante eines Produktes im Handel ausfindig zu machen." />
|
<meta name="description" content="__page_description__" />
|
||||||
<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>"
|
||||||
|
@ -15,4 +15,4 @@
|
||||||
|
|
||||||
<body class="w-full min-h-full flex flex-col">
|
<body class="w-full min-h-full flex flex-col">
|
||||||
<div class="max-w-6xl mx-auto lg:py-4 flex flex-col flex-1 w-full">
|
<div class="max-w-6xl mx-auto lg:py-4 flex flex-col flex-1 w-full">
|
||||||
<h2 class="bg-primary lg:rounded-t-xl text-center text-white p-3 uppercase text-2xl font-bold">🔥 Heisse Preise 🔥</h2>
|
<h2 class="bg-primary lg:rounded-t-xl text-center text-white p-3 uppercase text-2xl font-bold">🔥 __Heisse Preise__ 🔥</h2>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="flex justify-center gap-4 py-2 font-bold text-primary border border-primary/50 lg:rounded-b-xl">
|
<div class="flex justify-center gap-4 py-2 font-bold text-primary border border-primary/50 lg:rounded-b-xl">
|
||||||
<a href="index.html">Suche</a>
|
<a href="index.html">__Suche__</a>
|
||||||
<a href="changes.html">Preisänderungen</a>
|
<a href="changes.html">__Preisänderungen__</a>
|
||||||
<a href="carts.html">Warenkörbe</a>
|
<a href="carts.html">__Warenkörbe__</a>
|
||||||
</div>
|
</div>
|
46
site/browser_i18n.js
Normal file
46
site/browser_i18n.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
const i18n = require("../i18n");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected locale.
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
|
var currentLocale = i18n.defaultLocale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the globally used locale.
|
||||||
|
* Expects a 2 character language code string, one from locales.
|
||||||
|
* @param {string} locale
|
||||||
|
*/
|
||||||
|
function setLocale(locale) {
|
||||||
|
if (i18n.locales.includes(locale)) {
|
||||||
|
console.log("Locale changed to " + locale);
|
||||||
|
currentLocale = locale;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.error("Attempted to setLocale to unsupported language: ", locale);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates the key using the current global locale.
|
||||||
|
*
|
||||||
|
* @param {!string} key to translate
|
||||||
|
* @param {!Object.<string, string>} [args] arguments to substitute into the translated key
|
||||||
|
* @returns {string} translated string
|
||||||
|
*/
|
||||||
|
function translate(key, args) {
|
||||||
|
return i18n.translateWithLocale(currentLocale, key, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the most preferred supported language
|
||||||
|
for (const langCode of navigator.languages) {
|
||||||
|
// We don't do regional codes, so take just the language code
|
||||||
|
let lang = langCode.length >= 2 ? langCode.substring(0, 2) : null;
|
||||||
|
if (lang == null) continue;
|
||||||
|
if (i18n.locales.includes(lang)) {
|
||||||
|
setLocale(lang);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.__ = translate;
|
|
@ -4,13 +4,20 @@
|
||||||
<cart-header x-id="cartHeader"></cart-header>
|
<cart-header x-id="cartHeader"></cart-header>
|
||||||
|
|
||||||
<div x-id="noItems" class="hidden block text-center my-3">
|
<div x-id="noItems" class="hidden block text-center my-3">
|
||||||
Noch keine Produkte im Warenkorb.<br />
|
__Noch keine Produkte im Warenkorb.__<br />
|
||||||
Produkte suchen und mit '+' zum Warenkorb hinzufügen.
|
__Produkte suchen und mit '+' zum Warenkorb hinzufügen.__
|
||||||
</div>
|
</div>
|
||||||
<items-filter x-id="cartFilter" stores nochartclear class="hidden" placeholder="Filtern... (min. 3 Zeichen)"></items-filter>
|
<items-filter x-id="cartFilter" stores nochartclear class="hidden" placeholder="__Filtern...__ __(min. 3 Zeichen)__"></items-filter>
|
||||||
<items-list x-id="linkedCartList" chart json nosort class="hidden"></items-list>
|
<items-list x-id="linkedCartList" chart json nosort class="hidden"></items-list>
|
||||||
<items-list x-id="cartList" chart json nosort remove updown class="hidden"></items-list>
|
<items-list x-id="cartList" chart json nosort remove updown class="hidden"></items-list>
|
||||||
<items-filter x-id="productsFilter" class="hidden" stores misc nochartclear placeholder="Produkt hinzufügen... (min. 3 Zeichen)"></items-filter>
|
<items-filter
|
||||||
|
x-id="productsFilter"
|
||||||
|
class="hidden"
|
||||||
|
stores
|
||||||
|
misc
|
||||||
|
nochartclear
|
||||||
|
placeholder="__Produkt hinzufügen...__ (__min. 3 Zeichen__)"
|
||||||
|
></items-filter>
|
||||||
<items-list x-id="productsList" class="hidden" add></items-list>
|
<items-list x-id="productsList" class="hidden" add></items-list>
|
||||||
%%_templates/_loader.html%%
|
%%_templates/_loader.html%%
|
||||||
</div>
|
</div>
|
||||||
|
|
19
site/cart.js
19
site/cart.js
|
@ -6,6 +6,7 @@ const { STORE_KEYS, stores } = require("./model/stores");
|
||||||
require("./views");
|
require("./views");
|
||||||
const { ProgressBar } = require("./views/progress-bar");
|
const { ProgressBar } = require("./views/progress-bar");
|
||||||
const progressBar = new ProgressBar(STORE_KEYS.length);
|
const progressBar = new ProgressBar(STORE_KEYS.length);
|
||||||
|
const { __ } = require("./browser_i18n");
|
||||||
|
|
||||||
let carts = null;
|
let carts = null;
|
||||||
|
|
||||||
|
@ -35,8 +36,12 @@ class CartHeader extends View {
|
||||||
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">
|
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">
|
||||||
<span x-id="name"></span>
|
<span x-id="name"></span>
|
||||||
</h1>
|
</h1>
|
||||||
<a x-id="share" class="hidden cursor-pointer font-bold text-sm text-primary hover:underline block text-center mt-3">Teilen</a>
|
<a x-id="share" class="hidden cursor-pointer font-bold text-sm text-primary hover:underline block text-center mt-3">${__(
|
||||||
<input x-id="save" class="hidden cursor-pointer font-bold text-sm text-primary block mx-auto mt-3" type="button" value="Speichern">
|
"Cart_Teilen"
|
||||||
|
)}</a>
|
||||||
|
<input x-id="save" class="hidden cursor-pointer font-bold text-sm text-primary block mx-auto mt-3" type="button" value="${__(
|
||||||
|
"Cart_Speichern"
|
||||||
|
)}">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const elements = this.elements;
|
const elements = this.elements;
|
||||||
|
@ -47,7 +52,9 @@ class CartHeader extends View {
|
||||||
let newName = cart.name;
|
let newName = cart.name;
|
||||||
while (true) {
|
while (true) {
|
||||||
newName = prompt(
|
newName = prompt(
|
||||||
"Warenkorb '" + cart.name + " existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben",
|
__("Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben", {
|
||||||
|
name: cart.name,
|
||||||
|
}),
|
||||||
cart.name + today()
|
cart.name + today()
|
||||||
);
|
);
|
||||||
if (!newName || newName.trim().length == 0) return;
|
if (!newName || newName.trim().length == 0) return;
|
||||||
|
@ -69,7 +76,7 @@ class CartHeader extends View {
|
||||||
render() {
|
render() {
|
||||||
const cart = this.model.cart;
|
const cart = this.model.cart;
|
||||||
const elements = this.elements;
|
const elements = this.elements;
|
||||||
elements.name.innerText = `Warenkorb '${cart.name}'`;
|
elements.name.innerText = __("Cart_Warenkorb {{name}}", { name: cart.name });
|
||||||
if (this.model.linked) {
|
if (this.model.linked) {
|
||||||
elements.save.classList.remove("hidden");
|
elements.save.classList.remove("hidden");
|
||||||
} else {
|
} else {
|
||||||
|
@ -112,7 +119,7 @@ function loadCart() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cart == null) {
|
if (cart == null) {
|
||||||
alert("Warenkorb '" + cartName + "' existiert nicht.");
|
alert(__("Cart_Warenkorb '{{name}}' existiert nicht.", { name: cartName }));
|
||||||
location.href = "carts.html";
|
location.href = "carts.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +140,7 @@ function loadCart() {
|
||||||
STORE_KEYS.forEach((store) => {
|
STORE_KEYS.forEach((store) => {
|
||||||
cartFilter.elements[store].checked = true;
|
cartFilter.elements[store].checked = true;
|
||||||
});
|
});
|
||||||
cartList.elements.numItemsLabel.innerHTML = "<strong>Artikel:</strong>";
|
cartList.elements.numItemsLabel.innerHTML = `<strong>${__("Cart_Artikel")}:</strong>`;
|
||||||
cartList.elements.enableChart.checked = models.items.length < 2000;
|
cartList.elements.enableChart.checked = models.items.length < 2000;
|
||||||
cartList.elements.chart.elements.sumStores.checked = models.items.length < 2000;
|
cartList.elements.chart.elements.sumStores.checked = models.items.length < 2000;
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
<div class="w-full relative px-4 flex-1">
|
<div class="w-full relative px-4 flex-1">
|
||||||
<div class="max-w-3xl mx-auto">
|
<div class="max-w-3xl mx-auto">
|
||||||
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">Warenkörbe</h1>
|
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">__Warenkörbe__</h1>
|
||||||
<div class="px-4 py-2 my-4 text-sm border rounded-xl md:mt-8 md:rounded-b-none md:mb-0 bg-gray-100 flex gap-4">
|
<div class="px-4 py-2 my-4 text-sm border rounded-xl md:mt-8 md:rounded-b-none md:mb-0 bg-gray-100 flex gap-4">
|
||||||
<input type="button" id="new" value="Neuer Warenkorb" class="text-primary font-medium hover:underline cursor-pointer" />
|
<input type="button" id="new" value="__Neuer Warenkorb__" class="text-primary font-medium hover:underline cursor-pointer" />
|
||||||
<input type="button" id="export" value="Exportieren" class="text-primary font-medium hover:underline cursor-pointer" />
|
<input type="button" id="export" value="__Exportieren__" class="text-primary font-medium hover:underline cursor-pointer" />
|
||||||
<input type="button" id="import" value="Importieren" class="text-primary font-medium hover:underline cursor-pointer" />
|
<input type="button" id="import" value="__Importieren__" class="text-primary font-medium hover:underline cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
<carts-list id="carts" class="carts-list w-full"></carts-list>
|
<carts-list id="carts" class="carts-list w-full"></carts-list>
|
||||||
%%_templates/_loader.html%%
|
%%_templates/_loader.html%%
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
const { downloadJSON, today } = require("./js/misc");
|
const { downloadJSON, today } = require("./js/misc");
|
||||||
const model = require("./model");
|
const model = require("./model");
|
||||||
require("./views");
|
require("./views");
|
||||||
|
const { __ } = require("./browser_i18n");
|
||||||
|
|
||||||
const { STORE_KEYS } = require("./model/stores");
|
const { STORE_KEYS } = require("./model/stores");
|
||||||
const { ProgressBar } = require("./views/progress-bar");
|
const { ProgressBar } = require("./views/progress-bar");
|
||||||
const progressBar = new ProgressBar(STORE_KEYS.length);
|
const progressBar = new ProgressBar(STORE_KEYS.length);
|
||||||
|
|
||||||
function newCart() {
|
function newCart() {
|
||||||
let name = prompt("Name für Warenkorb eingeben:");
|
let name = prompt(__("Carts_Name für Warenkorb eingeben:"));
|
||||||
if (!name || name.trim().length == 0) return;
|
if (!name || name.trim().length == 0) return;
|
||||||
name = name.trim();
|
name = name.trim();
|
||||||
if (model.carts.carts.some((cart) => cart.name === name)) {
|
if (model.carts.carts.some((cart) => cart.name === name)) {
|
||||||
alert("Warenkorb mit Namen '" + name + "' existiert bereits");
|
alert(__("Carts_Warenkorb mit Namen '{{name}}' existiert bereits", { name: name }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
model.carts.add(name);
|
model.carts.add(name);
|
||||||
|
@ -36,7 +37,9 @@ function importCart(importedCart) {
|
||||||
let newName = importedCart.name;
|
let newName = importedCart.name;
|
||||||
while (true) {
|
while (true) {
|
||||||
newName = prompt(
|
newName = prompt(
|
||||||
"Warenkorb '" + importedCart.name + " existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben",
|
__("Carts_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben", {
|
||||||
|
name: importedCart.name,
|
||||||
|
}),
|
||||||
importedCart.name + today()
|
importedCart.name + today()
|
||||||
);
|
);
|
||||||
if (!newName || newName.trim().length == 0) return;
|
if (!newName || newName.trim().length == 0) return;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
%%_templates/_header.html%% %%_templates/_menu.html%%
|
%%_templates/_header.html%% %%_templates/_menu.html%%
|
||||||
|
|
||||||
<div class="w-full relative px-4 flex-1">
|
<div class="w-full relative px-4 flex-1">
|
||||||
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">Preisänderungen</h1>
|
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">__Preisänderungen__</h1>
|
||||||
<items-filter x-id="items-filter" pricechanges pricedirection stores misc placeholder="Filtern... (min. 3 Zeichen)"></items-filter>
|
<items-filter x-id="items-filter" pricechanges pricedirection stores misc placeholder="__Filtern...__ __(min. 3 Zeichen)__"></items-filter>
|
||||||
<items-list chart></items-list>
|
<items-list chart></items-list>
|
||||||
%%_templates/_loader.html%%
|
%%_templates/_loader.html%%
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
%%_templates/_header.html%% %%_templates/_menu.html%%
|
%%_templates/_header.html%% %%_templates/_menu.html%%
|
||||||
|
|
||||||
<div class="max-w-lg mx-auto my-8 px-4">
|
<div class="max-w-lg mx-auto my-8 px-4">
|
||||||
<h1 class="text-2xl font-bold">Impressum</h1>
|
<h1 class="text-2xl font-bold">__Impressum__</h1>
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
<strong>Angaben gemäß § 25 Mediengesetz (Österreich)</strong><br /><br />
|
<strong>Angaben gemäß § 25 Mediengesetz (Österreich)</strong><br /><br />
|
||||||
<b>Medieninhaber:</b> Mario Zechner<br />
|
<b>__Medieninhaber__:</b> Mario Zechner<br />
|
||||||
<b>Kontakt:</b> <a href="mailto:badlogicgames@gmail.com">badlogicgames@gmail.com</a><br />
|
<b>__Kontakt__:</b> <a href="mailto:badlogicgames@gmail.com">badlogicgames@gmail.com</a><br />
|
||||||
<b>Adresse:</b> Schörgelgasse 3, 8010 Graz, Österreich
|
<b>__Adresse__:</b> Schörgelgasse 3, 8010 Graz, Österreich
|
||||||
|
</p>
|
||||||
|
<p class="mt-4">
|
||||||
|
__Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.__
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-4">Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
%%_templates/_footer.html%%
|
%%_templates/_footer.html%%
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
|
|
||||||
<div class="w-full max-w-5-xl relative px-4 flex-1">
|
<div class="w-full max-w-5-xl relative px-4 flex-1">
|
||||||
<div class="flex flex-row w-full justify-center font-bold text-primary gap-4 mt-4">
|
<div class="flex flex-row w-full justify-center font-bold text-primary gap-4 mt-4">
|
||||||
<a href="https://www.youtube.com/watch?v=2u-T85yMKGI">Video Anleitung</a>
|
<a href="https://www.youtube.com/watch?v=2u-T85yMKGI">__Video Anleitung__</a>
|
||||||
<a href="https://docs.google.com/document/d/1Q5OWJOICXjSzTEIHBZgJl1p3FsiWFO0lzXIuwGbXBck/edit?usp=sharing" target="_blank">Text Anleitung</a>
|
<a href="https://docs.google.com/document/d/1Q5OWJOICXjSzTEIHBZgJl1p3FsiWFO0lzXIuwGbXBck/edit?usp=sharing" target="_blank"
|
||||||
<a href="./media.html">Medienberichte</a>
|
>__Text Anleitung__</a
|
||||||
|
>
|
||||||
|
<a href="./media.html">__Medienberichte__</a>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">Produktsuche</h1>
|
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">__Produktsuche__</h1>
|
||||||
<items-filter x-id="items-filter" emptyquery stores misc></items-filter>
|
<items-filter x-id="items-filter" emptyquery stores misc></items-filter>
|
||||||
<items-list share json chart></items-list>
|
<items-list share json chart></items-list>
|
||||||
%%_templates/_loader.html%%
|
%%_templates/_loader.html%%
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
// Process redirects before running anything else
|
||||||
|
if (location.href.includes("heissepreise.github.io")) {
|
||||||
|
location.href = "https://heisse-preise.io";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { getQueryParameter } = require("./js/misc");
|
const { getQueryParameter } = require("./js/misc");
|
||||||
const model = require("./model");
|
const model = require("./model");
|
||||||
require("./views");
|
require("./views");
|
||||||
|
@ -7,10 +13,6 @@ const { ProgressBar } = require("./views/progress-bar");
|
||||||
const progressBar = new ProgressBar(STORE_KEYS.length);
|
const progressBar = new ProgressBar(STORE_KEYS.length);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
if (location.href.includes("heissepreise.github.io")) {
|
|
||||||
location.href = "https://heisse-preise.io";
|
|
||||||
}
|
|
||||||
|
|
||||||
await model.load(() => progressBar.addStep());
|
await model.load(() => progressBar.addStep());
|
||||||
const itemsFilter = document.querySelector("items-filter");
|
const itemsFilter = document.querySelector("items-filter");
|
||||||
const itemsList = document.querySelector("items-list");
|
const itemsList = document.querySelector("items-list");
|
||||||
|
|
|
@ -305,7 +305,11 @@ exports.itemsToCSV = (items) => {
|
||||||
*/
|
*/
|
||||||
exports.numberToLocale = (number) => {
|
exports.numberToLocale = (number) => {
|
||||||
try {
|
try {
|
||||||
return number.toLocaleString("at-DE", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
let locale = "at-DE";
|
||||||
|
if (navigator) {
|
||||||
|
locale = navigator.language;
|
||||||
|
}
|
||||||
|
return number.toLocaleString(locale, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return number;
|
return number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
%%_templates/_header.html%% %%_templates/_menu.html%%
|
%%_templates/_header.html%% %%_templates/_menu.html%%
|
||||||
|
|
||||||
<div class="mx-auto my-8 px-4">
|
<div class="mx-auto my-8 px-4">
|
||||||
<h2 class="text-2xl font-bold">Medienberichte</h2>
|
<h2 class="text-2xl font-bold">__Medienberichte__</h2>
|
||||||
<div class="flex flex-col mt-4 gap-4 text-primary">
|
<div class="flex flex-col mt-4 gap-4 text-primary">
|
||||||
<h3 class="text-xl font-bold text-black">Radio & Fernsehen</h3>
|
<h3 class="text-xl font-bold text-black">__Radio & Fernsehen__</h3>
|
||||||
<a href="https://twitter.com/badlogicgames/status/1661401581097570308"
|
<a href="https://twitter.com/badlogicgames/status/1661401581097570308"
|
||||||
>24.5.203 Österreichisches Parlament - Rede von Joachim Schnabel zum Thema Lebensmittelpreise, ÖVP</a
|
>24.5.203 Österreichisches Parlament - Rede von Joachim Schnabel zum Thema Lebensmittelpreise, ÖVP</a
|
||||||
>
|
>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
>
|
>
|
||||||
<video src="https://marioslab.io/uploads/ZIB_2-REWE-%C3%96sterreich-Chef_%C3%BCber_hohe_Lebensmittelpreise-0220056522.mp4" controls></video>
|
<video src="https://marioslab.io/uploads/ZIB_2-REWE-%C3%96sterreich-Chef_%C3%BCber_hohe_Lebensmittelpreise-0220056522.mp4" controls></video>
|
||||||
|
|
||||||
<h3 class="text-xl font-bold text-black">Print & Online</h3>
|
<h3 class="text-xl font-bold text-black">__Print & Online__</h3>
|
||||||
<a href="https://www.puls24.at/news/politik/preisvergleich-politik-braucht-bis-herbst-twitter-user-2-stunden/297474"
|
<a href="https://www.puls24.at/news/politik/preisvergleich-politik-braucht-bis-herbst-twitter-user-2-stunden/297474"
|
||||||
>16.5.2023 PULS 24 - Preisvergleich: Politik braucht "bis Herbst", Twitter-User "2 Stunden"</a
|
>16.5.2023 PULS 24 - Preisvergleich: Politik braucht "bis Herbst", Twitter-User "2 Stunden"</a
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
%%_templates/_header.html%% %%_templates/_menu.html%%
|
%%_templates/_header.html%% %%_templates/_menu.html%%
|
||||||
|
|
||||||
<div class="max-w-lg mx-auto my-8 px-4">
|
<div class="max-w-lg mx-auto my-8 px-4">
|
||||||
<h2 class="text-2xl font-bold text-center">Einstellungen</h2>
|
<h2 class="text-2xl font-bold text-center">__Einstellungen__</h2>
|
||||||
<settings-view></settings-view>
|
<settings-view></settings-view>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,14 @@ const { View } = require("./views/view");
|
||||||
const { Settings } = require("./model/settings");
|
const { Settings } = require("./model/settings");
|
||||||
require("./js/misc");
|
require("./js/misc");
|
||||||
require("./views/custom-checkbox");
|
require("./views/custom-checkbox");
|
||||||
|
const { __ } = require("./browser_i18n");
|
||||||
|
|
||||||
class SettingsView extends View {
|
class SettingsView extends View {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.innerHTML = /*html*/ `
|
this.innerHTML = /*html*/ `
|
||||||
<div class="flex flex-col gap-4 p-4 rounded-xl md:mt-8 bg-gray-100">
|
<div class="flex flex-col gap-4 p-4 rounded-xl md:mt-8 bg-gray-100">
|
||||||
<div>Vorselektierte Ketten</div>
|
<div>${__("Settings_Vorselektierte Ketten")}</div>
|
||||||
<div x-id="stores" class="flex justify-center gap-2 flex-wrap">
|
<div x-id="stores" class="flex justify-center gap-2 flex-wrap">
|
||||||
${STORE_KEYS.map(
|
${STORE_KEYS.map(
|
||||||
(store) => /*html*/ `
|
(store) => /*html*/ `
|
||||||
|
@ -22,7 +23,7 @@ class SettingsView extends View {
|
||||||
).join("")}
|
).join("")}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
Start-Datum für Diagramme
|
${__("Settings_Start-Datum für Diagramme")}
|
||||||
<input
|
<input
|
||||||
x-id="startDate"
|
x-id="startDate"
|
||||||
x-change
|
x-change
|
||||||
|
@ -31,15 +32,21 @@ class SettingsView extends View {
|
||||||
class="flex-grow cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
class="flex-grow cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
Diagramm Typ
|
${__("Settings_Diagramm Typ")}
|
||||||
<select x-id="chartType" x-change x-state class="flex-grow">
|
<select x-id="chartType" x-change x-state class="flex-grow">
|
||||||
<option value="stepped">Stufen</option>
|
<option value="stepped">${__("Settings_Stufen")}</option>
|
||||||
<option value="lines">Linien</option>
|
<option value="lines">${__("Settings_Linien")}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<custom-checkbox x-id="onlyAvailable" x-state x-change label="Nur verfügbare Produkte anzeigen" checked></custom-checkbox>
|
<custom-checkbox x-id="onlyAvailable" x-state x-change label="${__(
|
||||||
<custom-checkbox x-id="stickyChart" x-state x-change label="Diagramm immer anzeigen (wenn verfügbar)" checked></custom-checkbox>
|
"Settings_Nur verfügbare Produkte anzeigen"
|
||||||
<custom-checkbox x-id="stickySearch" x-state x-change label="Suche immer anzeigen (wenn verfügbar)"></custom-checkbox>
|
)}" checked></custom-checkbox>
|
||||||
|
<custom-checkbox x-id="stickyChart" x-state x-change label="${__(
|
||||||
|
"Settings_Diagramm immer anzeigen (wenn verfügbar)"
|
||||||
|
)}" checked></custom-checkbox>
|
||||||
|
<custom-checkbox x-id="stickySearch" x-state x-change label="${__(
|
||||||
|
"Settings_Suche immer anzeigen (wenn verfügbar)"
|
||||||
|
)}"></custom-checkbox>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.setupEventHandlers();
|
this.setupEventHandlers();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const { downloadJSON, dom } = require("../js/misc");
|
const { downloadJSON, dom } = require("../js/misc");
|
||||||
const { View } = require("./view");
|
const { View } = require("./view");
|
||||||
|
const { __ } = require("../browser_i18n");
|
||||||
|
|
||||||
class CartsList extends View {
|
class CartsList extends View {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -8,9 +9,9 @@ class CartsList extends View {
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-primary text-left hidden md:table-row uppercase text-sm text-white">
|
<tr class="bg-primary text-left hidden md:table-row uppercase text-sm text-white">
|
||||||
<th class="px-2">Name</th>
|
<th class="px-2">${__("CartsList_Name")}</th>
|
||||||
<th class="px-2">Produkte</th>
|
<th class="px-2">${__("CartsList_Produkte")}</th>
|
||||||
<th class="px-2">Preis</th>
|
<th class="px-2">${__("CartsList_Preis")}</th>
|
||||||
<th class="px-2"></th>
|
<th class="px-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -26,18 +27,20 @@ class CartsList extends View {
|
||||||
<a x-id="name" class="hover:underline"></a>
|
<a x-id="name" class="hover:underline"></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2">
|
<td class="px-2">
|
||||||
<span class="md:hidden text-sm">Produkte: </span>
|
<span class="md:hidden text-sm">${__("CartsList_Produkte")}: </span>
|
||||||
<span x-id="numProducts"></span>
|
<span x-id="numProducts"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 col-span-2">
|
<td class="px-2 col-span-2">
|
||||||
<span class="md:hidden text-sm">Preisänderungen: </span>
|
<span class="md:hidden text-sm">${__("CartsList_Preisänderungen")}: </span>
|
||||||
<span x-id="price"></span>
|
<span x-id="price"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 col-span-3">
|
<td class="px-2 col-span-3">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<a x-id="share" class="text-primary hover:underline text-sm font-medium">Teilen</a>
|
<a x-id="share" class="text-primary hover:underline text-sm font-medium">${__("CartsList_Teilen")}</a>
|
||||||
<a x-id="json" class="text-primary hover:underline text-sm font-medium" href="">JSON</a>
|
<a x-id="json" class="text-primary hover:underline text-sm font-medium" href="">${__("CartsList_JSON")}</a>
|
||||||
<input x-id="delete" class="ml-auto text-red-500 hover:underline text-sm font-medium" type="button" value="Löschen">
|
<input x-id="delete" class="ml-auto text-red-500 hover:underline text-sm font-medium" type="button" value="${__(
|
||||||
|
"CartsList_Löschen"
|
||||||
|
)}">
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const { STORE_KEYS } = require("../model/stores");
|
const { STORE_KEYS } = require("../model/stores");
|
||||||
const { settings } = require("../model");
|
const { settings } = require("../model");
|
||||||
const { today, log, deltaTime, uniqueDates, calculateItemPriceTimeSeries } = require("../js/misc");
|
const { today, log, deltaTime, uniqueDates, calculateItemPriceTimeSeries } = require("../js/misc");
|
||||||
|
const { __ } = require("../browser_i18n");
|
||||||
const { View } = require("./view");
|
const { View } = require("./view");
|
||||||
require("./custom-checkbox");
|
require("./custom-checkbox");
|
||||||
const moment = require("moment");
|
const moment = require("moment");
|
||||||
|
@ -19,13 +20,13 @@ class ItemsChart extends View {
|
||||||
}">
|
}">
|
||||||
<div class="w-full grow">
|
<div class="w-full grow">
|
||||||
<canvas x-id="canvas" class="bg-white rounded-lg"></canvas>
|
<canvas x-id="canvas" class="bg-white rounded-lg"></canvas>
|
||||||
<div x-id="noData" class="hidden flex items-center justify-center h-full">Keine Daten ausgewählt</div>
|
<div x-id="noData" class="hidden flex items-center justify-center h-full">${__("ItemsChart_Keine Daten ausgewählt")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="filters flex items-center flex-wrap justify-center gap-2 pt-2">
|
<div class="filters flex items-center flex-wrap justify-center gap-2 pt-2">
|
||||||
<custom-checkbox x-id="sumTotal" x-change x-state label="Preissumme Gesamt"></custom-checkbox>
|
<custom-checkbox x-id="sumTotal" x-change x-state label="${__("ItemsChart_Preissumme Gesamt")}"></custom-checkbox>
|
||||||
<custom-checkbox x-id="sumStores" x-change x-state label="Preissumme Ketten"></custom-checkbox>
|
<custom-checkbox x-id="sumStores" x-change x-state label="${__("ItemsChart_Preissumme Ketten")}"></custom-checkbox>
|
||||||
<custom-checkbox x-id="onlyToday" x-change x-state label="Nur heutige Preise"></custom-checkbox>
|
<custom-checkbox x-id="onlyToday" x-change x-state label="${__("ItemsChart_Nur heutige Preise")}"></custom-checkbox>
|
||||||
<custom-checkbox x-id="percentageChange" x-change x-state label="Änderung in % seit"></custom-checkbox>
|
<custom-checkbox x-id="percentageChange" x-change x-state label="${__("ItemsChart_Änderung in % seit")}"></custom-checkbox>
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
||||||
<input x-id="startDate" x-change x-state type="date" value="2017-01-01" />
|
<input x-id="startDate" x-change x-state type="date" value="2017-01-01" />
|
||||||
|
@ -137,7 +138,7 @@ class ItemsChart extends View {
|
||||||
let yAxis = {
|
let yAxis = {
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (value, index, ticks) {
|
callback: function (value, index, ticks) {
|
||||||
return value.toLocaleString("de-DE", {
|
return value.toLocaleString(navigator.language || "de-DE", {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
|
@ -149,7 +150,7 @@ class ItemsChart extends View {
|
||||||
yAxis = {
|
yAxis = {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: "Änderung in % seit " + startDate,
|
text: __("ItemsChart_Änderung in % seit {{date}}", { date: startDate }),
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (value) => {
|
callback: (value) => {
|
||||||
|
@ -212,7 +213,7 @@ class ItemsChart extends View {
|
||||||
if (elements.sumTotal.checked && items.length > 0) {
|
if (elements.sumTotal.checked && items.length > 0) {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
itemsToShow.push({
|
itemsToShow.push({
|
||||||
name: "Preissumme Gesamt",
|
name: __("ItemsChart_Preissumme Gesamt"),
|
||||||
priceHistory: this.calculateOverallPriceChanges(items, onlyToday, percentageChange, startDate, endDate),
|
priceHistory: this.calculateOverallPriceChanges(items, onlyToday, percentageChange, startDate, endDate),
|
||||||
});
|
});
|
||||||
log("ItemsChart - Calculating overall sum total " + ((performance.now() - now) / 1000).toFixed(2) + " secs");
|
log("ItemsChart - Calculating overall sum total " + ((performance.now() - now) / 1000).toFixed(2) + " secs");
|
||||||
|
@ -224,7 +225,7 @@ class ItemsChart extends View {
|
||||||
const storeItems = items.filter((item) => item.store === store);
|
const storeItems = items.filter((item) => item.store === store);
|
||||||
if (storeItems.length > 0) {
|
if (storeItems.length > 0) {
|
||||||
itemsToShow.push({
|
itemsToShow.push({
|
||||||
name: "Preissumme " + store,
|
name: __("ItemsChart_Preissumme {{s}}", { s: store }),
|
||||||
priceHistory: this.calculateOverallPriceChanges(storeItems, onlyToday, percentageChange, startDate, endDate),
|
priceHistory: this.calculateOverallPriceChanges(storeItems, onlyToday, percentageChange, startDate, endDate),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ const { stores, STORE_KEYS, BUDGET_BRANDS } = require("../model/stores");
|
||||||
const { fromCategoryCode, categories } = require("../model/categories");
|
const { fromCategoryCode, categories } = require("../model/categories");
|
||||||
const { settings } = require("../model");
|
const { settings } = require("../model");
|
||||||
const { View } = require("./view");
|
const { View } = require("./view");
|
||||||
|
const { __ } = require("../browser_i18n");
|
||||||
|
|
||||||
class ItemsFilter extends View {
|
class ItemsFilter extends View {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -21,7 +22,9 @@ class ItemsFilter extends View {
|
||||||
const hideStores = this._filterByStores ? "" : "hidden";
|
const hideStores = this._filterByStores ? "" : "hidden";
|
||||||
const hideMisc = this._filterByMisc ? "" : "hidden";
|
const hideMisc = this._filterByMisc ? "" : "hidden";
|
||||||
const hideAvailableObption = this._availableOption ? "" : "hidden";
|
const hideAvailableObption = this._availableOption ? "" : "hidden";
|
||||||
const placeholder = this.hasAttribute("placeholder") ? this.getAttribute("placeholder") : "Produkte suchen... (min. 3 Zeichen)";
|
const placeholder = this.hasAttribute("placeholder")
|
||||||
|
? this.getAttribute("placeholder")
|
||||||
|
: __("ItemsFilter_Produkte suchen...") + " " + __("(min. 3 Zeichen)");
|
||||||
|
|
||||||
this.innerHTML = /*html*/ `
|
this.innerHTML = /*html*/ `
|
||||||
${
|
${
|
||||||
|
@ -29,7 +32,7 @@ class ItemsFilter extends View {
|
||||||
? `
|
? `
|
||||||
<div class="wrapper wrapper--search">
|
<div class="wrapper wrapper--search">
|
||||||
<input x-id="query" x-state x-input-debounce class="rounded-lg px-2 py-1 w-full" type="text" placeholder="${placeholder}" />
|
<input x-id="query" x-state x-input-debounce class="rounded-lg px-2 py-1 w-full" type="text" placeholder="${placeholder}" />
|
||||||
<label for="filter-toggle-search"><abbr title="Filter anzeigen/ausblenden">🎚</abbr></label>
|
<label for="filter-toggle-search"><abbr title="${__("ItemsFilter_Filter anzeigen/ausblenden")}">🎚</abbr></label>
|
||||||
</div>
|
</div>
|
||||||
<input class="toggle toggle--hidden" type="checkbox" id="filter-toggle-search" />
|
<input class="toggle toggle--hidden" type="checkbox" id="filter-toggle-search" />
|
||||||
<div class="wrapper wrapper--sticky">
|
<div class="wrapper wrapper--sticky">
|
||||||
|
@ -43,9 +46,9 @@ class ItemsFilter extends View {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-id="stores" class="flex justify-center gap-2 flex-wrap mt-4 ${hideStores}">
|
<div x-id="stores" class="flex justify-center gap-2 flex-wrap mt-4 ${hideStores}">
|
||||||
<custom-checkbox x-id="allStores" label="Alle" ${
|
<custom-checkbox x-id="allStores" label="${__("ItemsFilter_Alle")}" ${
|
||||||
Object.values(stores).every((store) => store.defaultChecked) ? "checked" : ""
|
Object.values(stores).every((store) => store.defaultChecked) ? "checked" : ""
|
||||||
}></custom-checkbox>
|
}></custom-checkbox>
|
||||||
${STORE_KEYS.map(
|
${STORE_KEYS.map(
|
||||||
(store) => /*html*/ `
|
(store) => /*html*/ `
|
||||||
<custom-checkbox
|
<custom-checkbox
|
||||||
|
@ -59,34 +62,34 @@ class ItemsFilter extends View {
|
||||||
|
|
||||||
<div x-id="priceChanges" class="text-sm flex justify-center gap-4 mt-4 ${hidePriceChanges}">
|
<div x-id="priceChanges" class="text-sm flex justify-center gap-4 mt-4 ${hidePriceChanges}">
|
||||||
<label>
|
<label>
|
||||||
<input x-id="priceChangesToday" x-state type="radio" name="type" checked /> Datum
|
<input x-id="priceChangesToday" x-state type="radio" name="type" checked /> ${__("ItemsFilter_Datum")}
|
||||||
<select x-id="priceChangesDate" x-state x-change class="bg-white rounded-full">
|
<select x-id="priceChangesDate" x-state x-change class="bg-white rounded-full">
|
||||||
<option>${today()}</option>
|
<option>${today()}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input x-id="priceChangesCheaper" x-state type="radio" name="type" /> Billiger seit letzter Änderung
|
<input x-id="priceChangesCheaper" x-state type="radio" name="type" /> ${__("ItemsFilter_Billiger seit letzter Änderung")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-id="misc" class="flex items-center justify-center flex-wrap gap-2 mt-4 ${hideMisc}">
|
<div x-id="misc" class="flex items-center justify-center flex-wrap gap-2 mt-4 ${hideMisc}">
|
||||||
<custom-checkbox
|
<custom-checkbox
|
||||||
x-id="budgetBrands" x-state x-change
|
x-id="budgetBrands" x-state x-change
|
||||||
label="Nur Diskont-Eigenmarken"
|
label="${__("ItemsFilter_Nur Diskont-Eigenmarken")}"
|
||||||
abbr="${BUDGET_BRANDS.map((budgetBrand) => budgetBrand.toUpperCase()).join(", ")}"
|
abbr="${BUDGET_BRANDS.map((budgetBrand) => budgetBrand.toUpperCase()).join(", ")}"
|
||||||
></custom-checkbox>
|
></custom-checkbox>
|
||||||
<custom-checkbox x-id="bio" x-state x-change label="Nur Bio"></custom-checkbox>
|
<custom-checkbox x-id="bio" x-state x-change label="${__("ItemsFilter_Nur Bio")}"></custom-checkbox>
|
||||||
<custom-checkbox x-id="exact" x-state x-change label="Exaktes Wort"></custom-checkbox>
|
<custom-checkbox x-id="exact" x-state x-change label="${__("ItemsFilter_Exaktes Wort")}"></custom-checkbox>
|
||||||
<label class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
<label class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
||||||
Preis € <input x-id="minPrice" x-state x-input class="w-12" type="number" min="0" value="0">
|
${__("ItemsFilter_Preis €")} <input x-id="minPrice" x-state x-input class="w-12" type="number" min="0" value="0">
|
||||||
-
|
-
|
||||||
<input x-id="maxPrice" x-state x-input-debounce class="w-12" type="number" min="0" value="100">
|
<input x-id="maxPrice" x-state x-input-debounce class="w-12" type="number" min="0" value="100">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-id="priceDirection" class="flex justify-center gap-2 mt-4 ${this._hideMisc ? "" : "mb-4"} ${hidePriceDirection}">
|
<div x-id="priceDirection" class="flex justify-center gap-2 mt-4 ${this._hideMisc ? "" : "mb-4"} ${hidePriceDirection}">
|
||||||
<custom-checkbox x-id="priceIncreased" x-state x-change label="Teurer" checked class="gray"></custom-checkbox>
|
<custom-checkbox x-id="priceIncreased" x-state x-change label="${__("ItemsFilter_Teurer")}" checked class="gray"></custom-checkbox>
|
||||||
<custom-checkbox x-id="priceDecreased" x-state x-change label="Billiger" checked class="gray"></custom-checkbox>
|
<custom-checkbox x-id="priceDecreased" x-state x-change label="${__("ItemsFilter_Billiger")}" checked class="gray"></custom-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-id="categories" class="flex justify-center gap-2 flex-wrap mt-4 hidden">
|
<div x-id="categories" class="flex justify-center gap-2 flex-wrap mt-4 hidden">
|
||||||
|
|
|
@ -14,6 +14,7 @@ const { vectorizeItems, similaritySortItems } = require("../js/knn");
|
||||||
const { stores } = require("../model/stores");
|
const { stores } = require("../model/stores");
|
||||||
const { View } = require("./view");
|
const { View } = require("./view");
|
||||||
const { ItemsChart } = require("./items-chart");
|
const { ItemsChart } = require("./items-chart");
|
||||||
|
const { __ } = require("../browser_i18n");
|
||||||
|
|
||||||
class ItemsList extends View {
|
class ItemsList extends View {
|
||||||
static priceTypeId = 0;
|
static priceTypeId = 0;
|
||||||
|
@ -34,32 +35,34 @@ class ItemsList extends View {
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col md:flex-row gap-2 items-center">
|
<div class="flex flex-col md:flex-row gap-2 items-center">
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<span x-id="numItemsLabel">Resultate</span><span x-id="numItems"></span>
|
<span x-id="numItemsLabel">${__("ItemsList_Resultate")}</span><span x-id="numItems"></span>
|
||||||
<span>
|
<span>
|
||||||
<a x-id="json" class="hidden text-primary font-medium hover:underline" href="">JSON</a>
|
<a x-id="json" class="hidden text-primary font-medium hover:underline" href="">JSON</a>
|
||||||
<a x-id="csv" class="hidden text-primary font-medium hover:underline" href="">CSV</a>
|
<a x-id="csv" class="hidden text-primary font-medium hover:underline" href="">CSV</a>
|
||||||
</span>
|
</span>
|
||||||
<custom-checkbox x-id="enableChart" x-change x-state label="Diagramm" class="${
|
<custom-checkbox x-id="enableChart" x-change x-state label="${__("ItemsList_Diagramm")}" class="${
|
||||||
this._chart ? "" : "hidden"
|
this._chart ? "" : "hidden"
|
||||||
}"></custom-checkbox>
|
}"></custom-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<label><input x-id="salesPrice" x-change x-state type="radio" name="priceType${
|
<label><input x-id="salesPrice" x-change x-state type="radio" name="priceType${ItemsList.priceTypeId}" checked> ${__(
|
||||||
ItemsList.priceTypeId
|
"ItemsList_Verkaufspreis"
|
||||||
}" checked> Verkaufspreis</label>
|
)}</label>
|
||||||
<label><input x-id="unitPrice" x-change x-state type="radio" name="priceType${ItemsList.priceTypeId++}"> Mengenpreis</label>
|
<label><input x-id="unitPrice" x-change x-state type="radio" name="priceType${ItemsList.priceTypeId++}"> ${__(
|
||||||
|
"ItemsList_Mengenpreis"
|
||||||
|
)}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="${hideSort}">
|
<label class="${hideSort}">
|
||||||
Sortieren
|
${__("ItemsList_Sortieren")}
|
||||||
<select x-id="sort" x-change x-state>
|
<select x-id="sort" x-change x-state>
|
||||||
<option value="price-asc">Preis aufsteigend</option>
|
<option value="price-asc">${__("ItemsList_Preis aufsteigend")}</option>
|
||||||
<option value="price-desc">Preis absteigend</option>
|
<option value="price-desc">${__("ItemsList_Preis absteigend")}</option>
|
||||||
<option value="quantity-asc">Menge aufsteigend</option>
|
<option value="quantity-asc">${__("ItemsList_Menge aufsteigend")}</option>
|
||||||
<option value="quantity-desc">Menge absteigend</option>
|
<option value="quantity-desc">${__("ItemsList_Menge absteigend")}</option>
|
||||||
<option value="store-and-name">Kette & Name</option>
|
<option value="store-and-name">${__("ItemsList_Kette & Name")}</option>
|
||||||
<option value="name-similarity" x-id="nameSimilarity" disabled>Namensähnlichkeit</option>
|
<option value="name-similarity" x-id="nameSimilarity" disabled>${__("ItemsList_Namensähnlichkeit")}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,9 +70,9 @@ class ItemsList extends View {
|
||||||
<table x-id="itemsTable" class="hidden rounded-b-xl overflow-hidden w-full text-left">
|
<table x-id="itemsTable" class="hidden rounded-b-xl overflow-hidden w-full text-left">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-primary text-white md:table-row uppercase text-sm">
|
<tr class="bg-primary text-white md:table-row uppercase text-sm">
|
||||||
<th class="text-center">Kette</th>
|
<th class="text-center">${__("ItemsList_Kette")}</th>
|
||||||
<th>Name</th>
|
<th>${__("ItemsList_Name")}</th>
|
||||||
<th x-id="expandPriceHistories" class="cursor-pointer">Preis ▼</th>
|
<th x-id="expandPriceHistories" class="cursor-pointer">${__("ItemsList_Preis")} ▼</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -106,7 +109,7 @@ class ItemsList extends View {
|
||||||
this._showAllPriceHistories = false;
|
this._showAllPriceHistories = false;
|
||||||
elements.expandPriceHistories.addEventListener("click", () => {
|
elements.expandPriceHistories.addEventListener("click", () => {
|
||||||
const showAll = (this._showAllPriceHistories = !this._showAllPriceHistories);
|
const showAll = (this._showAllPriceHistories = !this._showAllPriceHistories);
|
||||||
elements.expandPriceHistories.innerText = showAll ? "Preis ▲" : "Preis ▼";
|
elements.expandPriceHistories.innerText = __("ItemsList_Preis") + (showAll ? " ▲" : " ▼");
|
||||||
elements.tableBody.querySelectorAll(".priceinfo").forEach((el) => (showAll ? el.classList.remove("hidden") : el.classList.add("hidden")));
|
elements.tableBody.querySelectorAll(".priceinfo").forEach((el) => (showAll ? el.classList.remove("hidden") : el.classList.add("hidden")));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user