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:
- - Podpora ručního refresh týdne
- - Úprava pro přepracovanou podobu stránek Sladovnická
+ - Zobrazení alergenu při najetí myší
{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