commit
14f4b40b3c
|
@ -4,6 +4,7 @@ const chokidar = require("chokidar");
|
|||
const esbuild = require("esbuild");
|
||||
const { exec } = require("child_process");
|
||||
const { promisify } = require("util");
|
||||
const i18n = require("./i18n");
|
||||
|
||||
function deleteDirectory(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) => {
|
||||
const filename = filename1 || filename2;
|
||||
const filenamePath = path.join(fileDir, filename);
|
||||
try {
|
||||
const data = fs.readFileSync(filenamePath, "utf8");
|
||||
const replacedData = replaceFileContents(data, path.dirname(filenamePath));
|
||||
return replacedData;
|
||||
} catch (error) {
|
||||
console.error(`Error reading file "${filenamePath}":`, error);
|
||||
return "";
|
||||
return fileContent.replace(pattern, (_, filename, translationKey) => {
|
||||
if (filename != undefined) {
|
||||
const filenamePath = path.join(fileDir, filename);
|
||||
try {
|
||||
const data = fs.readFileSync(filenamePath, "utf8");
|
||||
const replacedData = replaceFileContents(data, path.dirname(filenamePath), locale);
|
||||
return replacedData;
|
||||
} catch (error) {
|
||||
console.error(`Error reading file "${filenamePath}":`, error);
|
||||
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) {
|
||||
const fileDir = path.dirname(inputFile);
|
||||
if (inputFile.includes(".mp3")) {
|
||||
let extension = path.extname(inputFile);
|
||||
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);
|
||||
if (filter(inputFile, false, data)) return;
|
||||
fs.writeFileSync(outputFile, data);
|
||||
} else {
|
||||
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}`);
|
||||
}
|
||||
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) {
|
||||
if (deleteOutput) {
|
||||
deleteDirectory(outputDir);
|
||||
|
@ -135,6 +169,7 @@ async function bundle(inputDir, outputDir, watch) {
|
|||
bundleHTML(inputDir, outputDir, false, watch, (filePath, isDir, data) => {
|
||||
if (isDir) return false;
|
||||
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 (data.includes(`require("`)) return true;
|
||||
return false;
|
||||
|
|
|
@ -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;
|
|
@ -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)"
|
||||
}
|
|
@ -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)"
|
||||
}
|
|
@ -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 express = require("express");
|
||||
const compression = require("compression");
|
||||
const i18n = require("./i18n");
|
||||
|
||||
function copyItemsToSite(dataDir) {
|
||||
const items = analysis.readJSON(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`);
|
||||
|
@ -45,6 +46,7 @@ function parseArguments() {
|
|||
const args = process.argv.slice(2);
|
||||
let port = process.env.PORT !== undefined && process.env.PORT != "" ? parseInt(process.env.PORT) : 3000;
|
||||
let liveReload = process.env.NODE_ENV === "development" || false;
|
||||
let skipDataUpdate = false;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === "-p" || args[i] === "--port") {
|
||||
port = parseInt(args[i + 1]);
|
||||
|
@ -54,6 +56,8 @@ function parseArguments() {
|
|||
throw new Error("Live reload is only supported in development mode");
|
||||
}
|
||||
liveReload = true;
|
||||
} else if (args[i] === "--skip-data-update") {
|
||||
skipDataUpdate = true;
|
||||
} else if (args[i] === "-h" || args[i] === "--help") {
|
||||
console.log("Usage: node server.js [-p|--port PORT] [-l|--live-reload]");
|
||||
console.log();
|
||||
|
@ -64,7 +68,7 @@ function parseArguments() {
|
|||
}
|
||||
}
|
||||
|
||||
return { port, liveReload };
|
||||
return { port, liveReload, skipDataUpdate };
|
||||
}
|
||||
|
||||
function setupLogging() {
|
||||
|
@ -81,7 +85,7 @@ function setupLogging() {
|
|||
|
||||
(async () => {
|
||||
const dataDir = "data";
|
||||
const { port, liveReload } = parseArguments();
|
||||
const { port, liveReload, skipDataUpdate } = parseArguments();
|
||||
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir);
|
||||
|
@ -102,25 +106,45 @@ function setupLogging() {
|
|||
setupLogging();
|
||||
bundle.bundle("site", outputDir, liveReload);
|
||||
|
||||
analysis.migrateCompression(dataDir, ".json", ".json.br");
|
||||
analysis.migrateCompression(dataDir, ".json.gz", ".json.br");
|
||||
if (!skipDataUpdate) {
|
||||
analysis.migrateCompression(dataDir, ".json", ".json.br");
|
||||
analysis.migrateCompression(dataDir, ".json.gz", ".json.br");
|
||||
|
||||
if (fs.existsSync(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`)) {
|
||||
copyItemsToSite(dataDir);
|
||||
analysis.updateData(dataDir, (_newItems) => {
|
||||
if (fs.existsSync(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`)) {
|
||||
copyItemsToSite(dataDir);
|
||||
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);
|
||||
});
|
||||
} else {
|
||||
await analysis.updateData(dataDir);
|
||||
copyItemsToSite(dataDir);
|
||||
}
|
||||
scheduleFunction(5, 0, 0, async () => {
|
||||
items = await analysis.updateData(dataDir);
|
||||
copyItemsToSite(dataDir);
|
||||
});
|
||||
|
||||
const app = express();
|
||||
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"));
|
||||
const server = http.createServer(app).listen(port, () => {
|
||||
console.log(`App listening on port ${port}`);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<footer>
|
||||
<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="imprint.html">Impressum</a>
|
||||
<a class="font-medium" href="data/log.txt">Logs</a>
|
||||
<a class="font-medium" href="settings.html">__Einstellungen__</a>
|
||||
<a class="font-medium" href="imprint.html">__Impressum__</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">
|
||||
<path d="
|
||||
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>
|
||||
</div>
|
||||
<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>
|
||||
<small class="text-center mb-6">
|
||||
<p>Alle Angaben ohne Gewähr, Irrtümer vorbehalten. <br />
|
||||
Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.
|
||||
<p>__Alle Angaben ohne Gewähr, Irrtümer vorbehalten.__<br />
|
||||
__Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.__
|
||||
</p>
|
||||
</small>
|
||||
</footer>
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<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." />
|
||||
<title>Heisse Preise</title>
|
||||
<meta name="description" content="__page_description__" />
|
||||
<title>__Heisse Preise__</title>
|
||||
<link
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
|
||||
|
@ -15,4 +15,4 @@
|
|||
|
||||
<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">
|
||||
<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">
|
||||
<a href="index.html">Suche</a>
|
||||
<a href="changes.html">Preisänderungen</a>
|
||||
<a href="carts.html">Warenkörbe</a>
|
||||
<a href="index.html">__Suche__</a>
|
||||
<a href="changes.html">__Preisänderungen__</a>
|
||||
<a href="carts.html">__Warenkörbe__</a>
|
||||
</div>
|
|
@ -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>
|
||||
|
||||
<div x-id="noItems" class="hidden block text-center my-3">
|
||||
Noch keine Produkte im Warenkorb.<br />
|
||||
Produkte suchen und mit '+' zum Warenkorb hinzufügen.
|
||||
__Noch keine Produkte im Warenkorb.__<br />
|
||||
__Produkte suchen und mit '+' zum Warenkorb hinzufügen.__
|
||||
</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="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>
|
||||
%%_templates/_loader.html%%
|
||||
</div>
|
||||
|
|
19
site/cart.js
19
site/cart.js
|
@ -6,6 +6,7 @@ const { STORE_KEYS, stores } = require("./model/stores");
|
|||
require("./views");
|
||||
const { ProgressBar } = require("./views/progress-bar");
|
||||
const progressBar = new ProgressBar(STORE_KEYS.length);
|
||||
const { __ } = require("./browser_i18n");
|
||||
|
||||
let carts = null;
|
||||
|
||||
|
@ -35,8 +36,12 @@ class CartHeader extends View {
|
|||
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">
|
||||
<span x-id="name"></span>
|
||||
</h1>
|
||||
<a x-id="share" class="hidden cursor-pointer font-bold text-sm text-primary hover:underline block text-center mt-3">Teilen</a>
|
||||
<input x-id="save" class="hidden cursor-pointer font-bold text-sm text-primary block mx-auto mt-3" type="button" value="Speichern">
|
||||
<a x-id="share" class="hidden cursor-pointer font-bold text-sm text-primary hover:underline block text-center mt-3">${__(
|
||||
"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;
|
||||
|
@ -47,7 +52,9 @@ class CartHeader extends View {
|
|||
let newName = cart.name;
|
||||
while (true) {
|
||||
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()
|
||||
);
|
||||
if (!newName || newName.trim().length == 0) return;
|
||||
|
@ -69,7 +76,7 @@ class CartHeader extends View {
|
|||
render() {
|
||||
const cart = this.model.cart;
|
||||
const elements = this.elements;
|
||||
elements.name.innerText = `Warenkorb '${cart.name}'`;
|
||||
elements.name.innerText = __("Cart_Warenkorb {{name}}", { name: cart.name });
|
||||
if (this.model.linked) {
|
||||
elements.save.classList.remove("hidden");
|
||||
} else {
|
||||
|
@ -112,7 +119,7 @@ function loadCart() {
|
|||
}
|
||||
|
||||
if (cart == null) {
|
||||
alert("Warenkorb '" + cartName + "' existiert nicht.");
|
||||
alert(__("Cart_Warenkorb '{{name}}' existiert nicht.", { name: cartName }));
|
||||
location.href = "carts.html";
|
||||
}
|
||||
|
||||
|
@ -133,7 +140,7 @@ function loadCart() {
|
|||
STORE_KEYS.forEach((store) => {
|
||||
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.chart.elements.sumStores.checked = models.items.length < 2000;
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
<div class="w-full relative px-4 flex-1">
|
||||
<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">
|
||||
<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="import" value="Importieren" 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="import" value="__Importieren__" class="text-primary font-medium hover:underline cursor-pointer" />
|
||||
</div>
|
||||
<carts-list id="carts" class="carts-list w-full"></carts-list>
|
||||
%%_templates/_loader.html%%
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
const { downloadJSON, today } = require("./js/misc");
|
||||
const model = require("./model");
|
||||
require("./views");
|
||||
const { __ } = require("./browser_i18n");
|
||||
|
||||
const { STORE_KEYS } = require("./model/stores");
|
||||
const { ProgressBar } = require("./views/progress-bar");
|
||||
const progressBar = new ProgressBar(STORE_KEYS.length);
|
||||
|
||||
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;
|
||||
name = name.trim();
|
||||
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;
|
||||
}
|
||||
model.carts.add(name);
|
||||
|
@ -36,7 +37,9 @@ function importCart(importedCart) {
|
|||
let newName = importedCart.name;
|
||||
while (true) {
|
||||
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()
|
||||
);
|
||||
if (!newName || newName.trim().length == 0) return;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
%%_templates/_header.html%% %%_templates/_menu.html%%
|
||||
|
||||
<div class="w-full relative px-4 flex-1">
|
||||
<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>
|
||||
<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-list chart></items-list>
|
||||
%%_templates/_loader.html%%
|
||||
</div>
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
%%_templates/_header.html%% %%_templates/_menu.html%%
|
||||
|
||||
<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">
|
||||
<strong>Angaben gemäß § 25 Mediengesetz (Österreich)</strong><br /><br />
|
||||
<b>Medieninhaber:</b> Mario Zechner<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>__Medieninhaber__:</b> Mario Zechner<br />
|
||||
<b>__Kontakt__:</b> <a href="mailto:badlogicgames@gmail.com">badlogicgames@gmail.com</a><br />
|
||||
<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 class="mt-4">Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.</p>
|
||||
</div>
|
||||
|
||||
%%_templates/_footer.html%%
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
<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">
|
||||
<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="./media.html">Medienberichte</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="./media.html">__Medienberichte__</a>
|
||||
</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-list share json chart></items-list>
|
||||
%%_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 model = require("./model");
|
||||
require("./views");
|
||||
|
@ -7,10 +13,6 @@ const { ProgressBar } = require("./views/progress-bar");
|
|||
const progressBar = new ProgressBar(STORE_KEYS.length);
|
||||
|
||||
(async () => {
|
||||
if (location.href.includes("heissepreise.github.io")) {
|
||||
location.href = "https://heisse-preise.io";
|
||||
}
|
||||
|
||||
await model.load(() => progressBar.addStep());
|
||||
const itemsFilter = document.querySelector("items-filter");
|
||||
const itemsList = document.querySelector("items-list");
|
||||
|
|
|
@ -305,7 +305,11 @@ exports.itemsToCSV = (items) => {
|
|||
*/
|
||||
exports.numberToLocale = (number) => {
|
||||
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) {
|
||||
return number;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
%%_templates/_header.html%% %%_templates/_menu.html%%
|
||||
|
||||
<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">
|
||||
<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"
|
||||
>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>
|
||||
|
||||
<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"
|
||||
>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%%
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,13 +3,14 @@ const { View } = require("./views/view");
|
|||
const { Settings } = require("./model/settings");
|
||||
require("./js/misc");
|
||||
require("./views/custom-checkbox");
|
||||
const { __ } = require("./browser_i18n");
|
||||
|
||||
class SettingsView extends View {
|
||||
constructor() {
|
||||
super();
|
||||
this.innerHTML = /*html*/ `
|
||||
<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">
|
||||
${STORE_KEYS.map(
|
||||
(store) => /*html*/ `
|
||||
|
@ -22,7 +23,7 @@ class SettingsView extends View {
|
|||
).join("")}
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
Start-Datum für Diagramme
|
||||
${__("Settings_Start-Datum für Diagramme")}
|
||||
<input
|
||||
x-id="startDate"
|
||||
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">
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
Diagramm Typ
|
||||
${__("Settings_Diagramm Typ")}
|
||||
<select x-id="chartType" x-change x-state class="flex-grow">
|
||||
<option value="stepped">Stufen</option>
|
||||
<option value="lines">Linien</option>
|
||||
<option value="stepped">${__("Settings_Stufen")}</option>
|
||||
<option value="lines">${__("Settings_Linien")}</option>
|
||||
</select>
|
||||
</div>
|
||||
<custom-checkbox x-id="onlyAvailable" x-state x-change label="Nur verfügbare Produkte anzeigen" checked></custom-checkbox>
|
||||
<custom-checkbox x-id="stickyChart" x-state x-change label="Diagramm immer anzeigen (wenn verfügbar)" checked></custom-checkbox>
|
||||
<custom-checkbox x-id="stickySearch" x-state x-change label="Suche immer anzeigen (wenn verfügbar)"></custom-checkbox>
|
||||
<custom-checkbox x-id="onlyAvailable" x-state x-change label="${__(
|
||||
"Settings_Nur verfügbare Produkte anzeigen"
|
||||
)}" 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>
|
||||
`;
|
||||
this.setupEventHandlers();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const { downloadJSON, dom } = require("../js/misc");
|
||||
const { View } = require("./view");
|
||||
const { __ } = require("../browser_i18n");
|
||||
|
||||
class CartsList extends View {
|
||||
constructor() {
|
||||
|
@ -8,9 +9,9 @@ class CartsList extends View {
|
|||
<table class="w-full">
|
||||
<thead>
|
||||
<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">Produkte</th>
|
||||
<th class="px-2">Preis</th>
|
||||
<th class="px-2">${__("CartsList_Name")}</th>
|
||||
<th class="px-2">${__("CartsList_Produkte")}</th>
|
||||
<th class="px-2">${__("CartsList_Preis")}</th>
|
||||
<th class="px-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -26,18 +27,20 @@ class CartsList extends View {
|
|||
<a x-id="name" class="hover:underline"></a>
|
||||
</td>
|
||||
<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>
|
||||
</td>
|
||||
<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>
|
||||
</td>
|
||||
<td class="px-2 col-span-3">
|
||||
<div class="flex gap-4">
|
||||
<a x-id="share" class="text-primary hover:underline text-sm font-medium">Teilen</a>
|
||||
<a x-id="json" class="text-primary hover:underline text-sm font-medium" href="">JSON</a>
|
||||
<input x-id="delete" class="ml-auto text-red-500 hover:underline text-sm font-medium" type="button" value="Löschen">
|
||||
<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="">${__("CartsList_JSON")}</a>
|
||||
<input x-id="delete" class="ml-auto text-red-500 hover:underline text-sm font-medium" type="button" value="${__(
|
||||
"CartsList_Löschen"
|
||||
)}">
|
||||
</div>
|
||||
</td>
|
||||
`
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const { STORE_KEYS } = require("../model/stores");
|
||||
const { settings } = require("../model");
|
||||
const { today, log, deltaTime, uniqueDates, calculateItemPriceTimeSeries } = require("../js/misc");
|
||||
const { __ } = require("../browser_i18n");
|
||||
const { View } = require("./view");
|
||||
require("./custom-checkbox");
|
||||
const moment = require("moment");
|
||||
|
@ -19,13 +20,13 @@ class ItemsChart extends View {
|
|||
}">
|
||||
<div class="w-full grow">
|
||||
<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 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="sumStores" x-change x-state label="Preissumme Ketten"></custom-checkbox>
|
||||
<custom-checkbox x-id="onlyToday" x-change x-state label="Nur heutige Preise"></custom-checkbox>
|
||||
<custom-checkbox x-id="percentageChange" x-change x-state label="Änderung in % seit"></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="${__("ItemsChart_Preissumme Ketten")}"></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="${__("ItemsChart_Änderung in % seit")}"></custom-checkbox>
|
||||
<div
|
||||
class="cursor-pointer inline-flex items-center gap-x-1 rounded-full bg-white border border-gray-400 px-2 py-1 text-xs font-medium text-gray-600">
|
||||
<input x-id="startDate" x-change x-state type="date" value="2017-01-01" />
|
||||
|
@ -137,7 +138,7 @@ class ItemsChart extends View {
|
|||
let yAxis = {
|
||||
ticks: {
|
||||
callback: function (value, index, ticks) {
|
||||
return value.toLocaleString("de-DE", {
|
||||
return value.toLocaleString(navigator.language || "de-DE", {
|
||||
minimumFractionDigits: 2,
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
|
@ -149,7 +150,7 @@ class ItemsChart extends View {
|
|||
yAxis = {
|
||||
title: {
|
||||
display: true,
|
||||
text: "Änderung in % seit " + startDate,
|
||||
text: __("ItemsChart_Änderung in % seit {{date}}", { date: startDate }),
|
||||
},
|
||||
ticks: {
|
||||
callback: (value) => {
|
||||
|
@ -212,7 +213,7 @@ class ItemsChart extends View {
|
|||
if (elements.sumTotal.checked && items.length > 0) {
|
||||
const now = performance.now();
|
||||
itemsToShow.push({
|
||||
name: "Preissumme Gesamt",
|
||||
name: __("ItemsChart_Preissumme Gesamt"),
|
||||
priceHistory: this.calculateOverallPriceChanges(items, onlyToday, percentageChange, startDate, endDate),
|
||||
});
|
||||
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);
|
||||
if (storeItems.length > 0) {
|
||||
itemsToShow.push({
|
||||
name: "Preissumme " + store,
|
||||
name: __("ItemsChart_Preissumme {{s}}", { s: store }),
|
||||
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 { settings } = require("../model");
|
||||
const { View } = require("./view");
|
||||
const { __ } = require("../browser_i18n");
|
||||
|
||||
class ItemsFilter extends View {
|
||||
constructor() {
|
||||
|
@ -21,7 +22,9 @@ class ItemsFilter extends View {
|
|||
const hideStores = this._filterByStores ? "" : "hidden";
|
||||
const hideMisc = this._filterByMisc ? "" : "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*/ `
|
||||
${
|
||||
|
@ -29,7 +32,7 @@ class ItemsFilter extends View {
|
|||
? `
|
||||
<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}" />
|
||||
<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>
|
||||
<input class="toggle toggle--hidden" type="checkbox" id="filter-toggle-search" />
|
||||
<div class="wrapper wrapper--sticky">
|
||||
|
@ -43,9 +46,9 @@ class ItemsFilter extends View {
|
|||
</div>
|
||||
|
||||
<div x-id="stores" class="flex justify-center gap-2 flex-wrap mt-4 ${hideStores}">
|
||||
<custom-checkbox x-id="allStores" label="Alle" ${
|
||||
Object.values(stores).every((store) => store.defaultChecked) ? "checked" : ""
|
||||
}></custom-checkbox>
|
||||
<custom-checkbox x-id="allStores" label="${__("ItemsFilter_Alle")}" ${
|
||||
Object.values(stores).every((store) => store.defaultChecked) ? "checked" : ""
|
||||
}></custom-checkbox>
|
||||
${STORE_KEYS.map(
|
||||
(store) => /*html*/ `
|
||||
<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}">
|
||||
<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">
|
||||
<option>${today()}</option>
|
||||
</select>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div x-id="misc" class="flex items-center justify-center flex-wrap gap-2 mt-4 ${hideMisc}">
|
||||
<custom-checkbox
|
||||
x-id="budgetBrands" x-state x-change
|
||||
label="Nur Diskont-Eigenmarken"
|
||||
label="${__("ItemsFilter_Nur Diskont-Eigenmarken")}"
|
||||
abbr="${BUDGET_BRANDS.map((budgetBrand) => budgetBrand.toUpperCase()).join(", ")}"
|
||||
></custom-checkbox>
|
||||
<custom-checkbox x-id="bio" x-state x-change label="Nur Bio"></custom-checkbox>
|
||||
<custom-checkbox x-id="exact" x-state x-change label="Exaktes Wort"></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="${__("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">
|
||||
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">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<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="priceDecreased" x-state x-change label="Billiger" 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="${__("ItemsFilter_Billiger")}" checked class="gray"></custom-checkbox>
|
||||
</div>
|
||||
|
||||
<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 { View } = require("./view");
|
||||
const { ItemsChart } = require("./items-chart");
|
||||
const { __ } = require("../browser_i18n");
|
||||
|
||||
class ItemsList extends View {
|
||||
static priceTypeId = 0;
|
||||
|
@ -34,32 +35,34 @@ class ItemsList extends View {
|
|||
<div>
|
||||
<div class="flex flex-col md: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>
|
||||
<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>
|
||||
</span>
|
||||
<custom-checkbox x-id="enableChart" x-change x-state label="Diagramm" class="${
|
||||
this._chart ? "" : "hidden"
|
||||
}"></custom-checkbox>
|
||||
<custom-checkbox x-id="enableChart" x-change x-state label="${__("ItemsList_Diagramm")}" class="${
|
||||
this._chart ? "" : "hidden"
|
||||
}"></custom-checkbox>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<label><input x-id="salesPrice" x-change x-state type="radio" name="priceType${
|
||||
ItemsList.priceTypeId
|
||||
}" checked> Verkaufspreis</label>
|
||||
<label><input x-id="unitPrice" x-change x-state type="radio" name="priceType${ItemsList.priceTypeId++}"> Mengenpreis</label>
|
||||
<label><input x-id="salesPrice" x-change x-state type="radio" name="priceType${ItemsList.priceTypeId}" checked> ${__(
|
||||
"ItemsList_Verkaufspreis"
|
||||
)}</label>
|
||||
<label><input x-id="unitPrice" x-change x-state type="radio" name="priceType${ItemsList.priceTypeId++}"> ${__(
|
||||
"ItemsList_Mengenpreis"
|
||||
)}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="${hideSort}">
|
||||
Sortieren
|
||||
${__("ItemsList_Sortieren")}
|
||||
<select x-id="sort" x-change x-state>
|
||||
<option value="price-asc">Preis aufsteigend</option>
|
||||
<option value="price-desc">Preis absteigend</option>
|
||||
<option value="quantity-asc">Menge aufsteigend</option>
|
||||
<option value="quantity-desc">Menge absteigend</option>
|
||||
<option value="store-and-name">Kette & Name</option>
|
||||
<option value="name-similarity" x-id="nameSimilarity" disabled>Namensähnlichkeit</option>
|
||||
<option value="price-asc">${__("ItemsList_Preis aufsteigend")}</option>
|
||||
<option value="price-desc">${__("ItemsList_Preis absteigend")}</option>
|
||||
<option value="quantity-asc">${__("ItemsList_Menge aufsteigend")}</option>
|
||||
<option value="quantity-desc">${__("ItemsList_Menge absteigend")}</option>
|
||||
<option value="store-and-name">${__("ItemsList_Kette & Name")}</option>
|
||||
<option value="name-similarity" x-id="nameSimilarity" disabled>${__("ItemsList_Namensähnlichkeit")}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -67,9 +70,9 @@ class ItemsList extends View {
|
|||
<table x-id="itemsTable" class="hidden rounded-b-xl overflow-hidden w-full text-left">
|
||||
<thead>
|
||||
<tr class="bg-primary text-white md:table-row uppercase text-sm">
|
||||
<th class="text-center">Kette</th>
|
||||
<th>Name</th>
|
||||
<th x-id="expandPriceHistories" class="cursor-pointer">Preis ▼</th>
|
||||
<th class="text-center">${__("ItemsList_Kette")}</th>
|
||||
<th>${__("ItemsList_Name")}</th>
|
||||
<th x-id="expandPriceHistories" class="cursor-pointer">${__("ItemsList_Preis")} ▼</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -106,7 +109,7 @@ class ItemsList extends View {
|
|||
this._showAllPriceHistories = false;
|
||||
elements.expandPriceHistories.addEventListener("click", () => {
|
||||
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")));
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue