From 81f67c8424b51098570f82fa6f0f2ce16eb237d5 Mon Sep 17 00:00:00 2001 From: Martin Berka Date: Mon, 6 Oct 2025 16:28:38 +0200 Subject: [PATCH] =?UTF-8?q?Podpora=20parsov=C3=A1n=C3=AD=20a=20zobrazen?= =?UTF-8?q?=C3=AD=20alergen=C5=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 33 +++++++++++++++++++++--- server/src/restaurants.ts | 54 +++++++++++++++++++++++++++++++-------- types/schemas/_index.yml | 6 +++++ 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index eb66400..c008376 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -30,6 +30,24 @@ const EASTER_EGG_STYLE = { animationTimingFunction: "ease" } +// Mapování čísel alergenů na jejich názvy +const ALLERGENS: { [key: number]: string } = { + 1: "Obiloviny obsahující lepek", + 2: "Korýši a výrobky z nich", + 3: "Vejce a výrobky z nich", + 4: "Ryby a výrobky z nich", + 5: "Arašidy a výrobky z nich", + 6: "Sója a výrobky z nich", + 7: "Mléko a výrobky z nich (včetně laktózy)", + 8: "Skořápkové plody", + 9: "Celer a výrobky z něj", + 10: "Hořčice a výrobky z ní", + 11: "Sezamová semena a výrobky z nich", + 12: "Oxid siřičitý a siřičitany", + 13: "Vlčí bob (Lupina) a výrobky z něj", + 14: "Měkkýši a výrobky z nich" +} + // Výchozí doba trvání animace v sekundách, pokud není přetíženo v konfiguračním JSONu const EASTER_EGG_DEFAULT_DURATION = 0.75; @@ -352,7 +370,17 @@ function App() { (!hideSoups || !f.isSoup) && doAddClickFoodChoice(location, index)}> {f.amount} - {f.name} + + {f.name} + {f.allergens && f.allergens.length > 0 && ( + <> ({f.allergens.map((a, idx) => ( + + {a} + {idx < f.allergens!.length - 1 && ','} + + ))}) + )} + {f.price} )} @@ -412,8 +440,7 @@ function App() { */} Poslední změny: {dayIndex != null && diff --git a/server/src/restaurants.ts b/server/src/restaurants.ts index b1aaed4..869f62a 100644 --- a/server/src/restaurants.ts +++ b/server/src/restaurants.ts @@ -53,6 +53,28 @@ const sanitizeText = (text: string): string => { return text.replace('\t', '').replace(' , ', ', ').trim(); } +/** + * Parsuje čísla alergenů z názvu jídla a vrací vyčištěný název spolu s polem alergenů. + * Alergeny jsou očekávány na konci názvu ve formátu číslic oddělených čárkami. + * + * @param name původní název jídla + * @returns objekt obsahující vyčištěný název a pole alergenů + */ +const parseAllergens = (name: string): { cleanName: string, allergens: number[] } => { + // Regex pro nalezení čísel na konci řetězce oddělených čárkami a případnými mezerami + const regex = /\s+(\d+(?:\s*,\s*\d+)*)\s*$/; + const match = regex.exec(name); + + if (match) { + const allergenString = match[1]; + const allergens = allergenString.split(',').map(num => parseInt(num.trim(), 10)).filter(num => !isNaN(num)); + const cleanName = name.replace(regex, '').trim(); + return { cleanName, allergens }; + } + + return { cleanName: name, allergens: [] }; +} + /** * Stáhne a vrátí aktuální HTML z dané URL. * @@ -101,8 +123,9 @@ export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = f } const soupAmount = sanitizeText($(soupCells.get(0)).text()); - const soupName = sanitizeText($(soupCells.get(1)).text()); + const soupNameRaw = sanitizeText($(soupCells.get(1)).text()); const soupPrice = sanitizeText($(soupCells.get(2)).text().replace(' ', '\xA0')); + const soupParsed = parseAllergens(soupNameRaw); // Parsování hlavních jídel const mainCourseElement = dayChildren.get(1); @@ -114,25 +137,28 @@ export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = f // Přidáme polévku do seznamu jídel currentDayFood.push({ amount: soupAmount, - name: soupName, + name: soupParsed.cleanName, price: soupPrice, isSoup: true, + allergens: soupParsed.allergens.length > 0 ? soupParsed.allergens : undefined, }); // Projdeme všechny řádky hlavních jídel mainCourseRows.each((i, row) => { const cells = $(row).children('td'); const amount = sanitizeText($(cells.get(0)).text()); - const name = sanitizeText($(cells.get(1)).text()); + const nameRaw = sanitizeText($(cells.get(1)).text()); const price = sanitizeText($(cells.get(2)).text().replace(' ', '\xA0')); + const parsed = parseAllergens(nameRaw); // Přeskočíme prázdné řádky (první řádek může být prázdný) - if (name.trim().length > 0) { + if (parsed.cleanName.trim().length > 0) { currentDayFood.push({ amount, - name, + name: parsed.cleanName, price, isSoup: false, + allergens: parsed.allergens.length > 0 ? parsed.allergens : undefined, }); } }); @@ -293,19 +319,25 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal continue; } let price = 'na\xA0váhu'; - let name = text.replace('•', ''); + let nameRaw = text.replace('•', ''); if (text.toLowerCase().endsWith('kč')) { const tmp = text.replace('\xA0', ' ').split(' '); const split = [tmp.slice(0, -2).join(' ')].concat(tmp.slice(-2)); price = `${split.slice(1)[0]}\xA0Kč` - name = split[0].replace('•', ''); + nameRaw = split[0].replace('•', ''); } + if (nameRaw.endsWith('–')) { + nameRaw = nameRaw.slice(0, -1).trim(); + } + + const parsed = parseAllergens(nameRaw); result[currentDayIndex] ??= []; result[currentDayIndex].push({ amount: '-', - name, + name: parsed.cleanName, price, - isSoup: isTextSoupName(name), + isSoup: isTextSoupName(parsed.cleanName), + allergens: parsed.allergens.length > 0 ? parsed.allergens : undefined, }) } } @@ -405,9 +437,11 @@ export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean = $('.menicka').each((i, element) => { const currentDayFood: Food[] = []; $(element).find('.popup-gallery li').each((j, element) => { + const rawName = $(element).children('div.polozka').text(); + const nameWithoutNumber = rawName.replace(/^\d+\.\s*/, ''); currentDayFood.push({ amount: '-', - name: $(element).children('div.polozka').text(), + name: nameWithoutNumber, price: $(element).children('div.cena').text().replace(/ /g, '\xA0'), isSoup: $(element).hasClass('polevka'), }); diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml index fde9cf1..b4be218 100644 --- a/types/schemas/_index.yml +++ b/types/schemas/_index.yml @@ -151,6 +151,12 @@ Food: isSoup: description: Příznak, zda se jedná o polévku type: boolean + allergens: + description: Seznam čísel alergenů obsažených v jídle + type: array + items: + type: integer + example: [1, 3, 7] RestaurantDayMenu: description: Menu restaurace na konkrétní den type: object