CI / Generate TypeScript types (push) Successful in 11s
CI / Server unit tests (push) Successful in 30s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 2m0s
CI / Build and push Docker image (push) Successful in 47s
CI / Notify (push) Successful in 2s
614 lines
23 KiB
TypeScript
614 lines
23 KiB
TypeScript
import axios from "axios";
|
||
import { load } from 'cheerio';
|
||
import { getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getMenuZastavkaUmichalaMock, getMenuSenkSerikovaMock } from "./mock";
|
||
import { formatDate } from "./utils";
|
||
import { Food } from "../../types/gen/types.gen";
|
||
|
||
export class StaleWeekError extends Error {
|
||
constructor(public food: Food[][]) { super('Data jsou z jiného týdne'); }
|
||
}
|
||
|
||
// Fráze v názvech jídel, které naznačují že se jedná o polévku
|
||
const SOUP_NAMES = [
|
||
'polévka',
|
||
'česnečka',
|
||
'česnekový krém',
|
||
'cibulačka',
|
||
'vývar',
|
||
'fazolová',
|
||
'cuketový krém',
|
||
'boršč',
|
||
'slepičí s ',
|
||
'zeleninová s ',
|
||
'zeleninový krém',
|
||
'hovězí s ',
|
||
'kachní kaldoun',
|
||
'dršťková',
|
||
'žampionový krém',
|
||
'zelná',
|
||
'čočková',
|
||
'gulášová',
|
||
'bramboračka',
|
||
'dýňový krém',
|
||
'ovarová',
|
||
'fazolačka',
|
||
'kyselica',
|
||
'rajčatová s ',
|
||
'hrášková s ',
|
||
'hrachová s ',
|
||
];
|
||
const DAYS_IN_WEEK = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle'];
|
||
|
||
// URL na týdenní menu jednotlivých restaurací
|
||
const SLADOVNICKA_URL = 'https://sladovnicka.unasplzenchutna.cz/cz/#denni-nabidka';
|
||
const U_MOTLIKU_URL = 'https://www.umotliku.cz/menu';
|
||
const TECHTOWER_URL = 'https://www.equifarm.cz/restaurace-techtower';
|
||
const ZASTAVKAUMICHALA_URL = 'https://www.zastavkaumichala.cz';
|
||
const SENKSERIKOVA_URL = 'https://www.menicka.cz/6561-pivovarsky-senk-serikova.html';
|
||
|
||
/**
|
||
* Vrátí true, pokud předaný text obsahuje některé ze slov, které naznačuje, že se jedná o polévku.
|
||
* Využito tam, kde nelze polévku identifikovat lepším způsobem (TechTower).
|
||
*
|
||
* @param text vstupní text
|
||
* @returns true, pokud text představuje polévku
|
||
*/
|
||
export const isTextSoupName = (text: string): boolean => {
|
||
for (const name of SOUP_NAMES) {
|
||
if (text.toLowerCase().includes(name)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
export const capitalize = (word: string): string => {
|
||
return word.charAt(0).toUpperCase() + word.slice(1);
|
||
}
|
||
|
||
export 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ů
|
||
*/
|
||
export 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 => Number.parseInt(num.trim(), 10)).filter(num => !Number.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.
|
||
*
|
||
* @param url URL pro stažení
|
||
* @returns stažené HTML
|
||
*/
|
||
const getHtml = async (url: string): Promise<any> => {
|
||
return await axios.get(url).then(res => res.data).then(content => content);
|
||
}
|
||
|
||
/**
|
||
* Získá obědovou nabídku Sladovnické pro jeden týden.
|
||
*
|
||
* @param firstDayOfWeek první den v týdnu, pro který získat menu
|
||
* @param mock zda vrátit mock data
|
||
* @returns seznam jídel pro daný týden
|
||
*/
|
||
export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => {
|
||
if (mock) {
|
||
return getMenuSladovnickaMock();
|
||
}
|
||
|
||
const html = await getHtml(SLADOVNICKA_URL);
|
||
const $ = load(html);
|
||
|
||
// Zjistíme, které dny jsou k dispozici z tab elementů
|
||
const tabElements = $('#daily-menu-tab-list').children('button[id^="daily-menu-tab-"]');
|
||
const availableDays: { [dayIndex: number]: number } = {}; // mapování dayIndex -> contentIndex
|
||
|
||
tabElements.each((contentIndex, tabElement) => {
|
||
const dayText = $(tabElement).find('.daily-menu-tab__day').text().toLowerCase();
|
||
const dayIndex = DAYS_IN_WEEK.indexOf(dayText);
|
||
if (dayIndex !== -1 && dayIndex < 5) { // pouze pracovní dny (0-4)
|
||
availableDays[dayIndex] = contentIndex;
|
||
}
|
||
});
|
||
|
||
const menuContentElements = $('#daily-menu-content-list').children('.daily-menu-content__content').not('.daily-menu-content__content--static');
|
||
|
||
const result: Food[][] = [];
|
||
|
||
// Inicializujeme všechny pracovní dny (0-4) prázdnými poli
|
||
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
||
result[dayIndex] = [];
|
||
}
|
||
|
||
// Projdeme pouze dostupné dny
|
||
for (const [dayIndex, contentIndex] of Object.entries(availableDays)) {
|
||
const dayIndexNum = Number.parseInt(dayIndex);
|
||
const contentIndexNum = contentIndex;
|
||
|
||
if (contentIndexNum >= menuContentElements.length) {
|
||
continue; // Přeskočíme, pokud content element neexistuje
|
||
}
|
||
|
||
const contentElement = $(menuContentElements[contentIndexNum]);
|
||
const itemElement = contentElement.find('.daily-menu-content__item');
|
||
const table = itemElement.find('table.daily-menu-content__table tbody');
|
||
const rows = table.children('tr');
|
||
|
||
const currentDayFood: Food[] = [];
|
||
|
||
// Projdeme všechny řádky - první je polévka, zbytek jsou hlavní jídla
|
||
rows.each((i, row) => {
|
||
const cells = $(row).children('td');
|
||
if (cells.length !== 3) {
|
||
return; // Přeskočíme řádky s nesprávnou strukturou
|
||
}
|
||
|
||
const amount = sanitizeText($(cells.get(0)).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
|
||
if (parsed.cleanName.trim().length > 0) {
|
||
currentDayFood.push({
|
||
amount,
|
||
name: parsed.cleanName,
|
||
price,
|
||
isSoup: i === 0, // První řádek je polévka
|
||
allergens: parsed.allergens.length > 0 ? parsed.allergens : undefined,
|
||
});
|
||
}
|
||
});
|
||
|
||
result[dayIndexNum] = currentDayFood;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Získá obědovou nabídku restaurace U Motlíků pro jeden týden.
|
||
*
|
||
* @param firstDayOfWeek první den v týdnu, pro který získat menu
|
||
* @param mock zda vrátit mock data
|
||
* @returns seznam jídel pro dané datum
|
||
*/
|
||
export const getMenuUMotliku = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => {
|
||
if (mock) {
|
||
return getMenuUMotlikuMock();
|
||
}
|
||
|
||
const html = await getHtml(U_MOTLIKU_URL);
|
||
const $ = load(html);
|
||
|
||
// Najdeme první tabulku, nad kterou je v H3 datum začínající co nejdřívějším dnem v aktuálním týdnu
|
||
const tables = $('table.table.table-hover.Xtable-striped');
|
||
let usedTable;
|
||
let usedDate = new Date(firstDayOfWeek);
|
||
for (let i = 0; i < 4; i++) {
|
||
const dayOfWeekString = `${usedDate.getDate()}.${usedDate.getMonth() + 1}.`;
|
||
for (const tableNode of tables) {
|
||
const table = $(tableNode);
|
||
const h3 = table.parent().prev();
|
||
const s1 = h3.text().split("-")[0].split(".");
|
||
const foundFirstDayString = `${s1[0]}.${s1[1]}.`;
|
||
if (foundFirstDayString === dayOfWeekString) {
|
||
usedTable = table;
|
||
}
|
||
}
|
||
if (usedTable != null) {
|
||
break;
|
||
}
|
||
usedDate.setDate(usedDate.getDate() + 1);
|
||
}
|
||
|
||
if (usedTable == null) {
|
||
const firstDayOfWeekString = `${firstDayOfWeek.getDate()}.${firstDayOfWeek.getMonth() + 1}.`;
|
||
throw new Error(`Nepodařilo se najít tabulku pro týden začínající ${firstDayOfWeekString}`);
|
||
}
|
||
|
||
const body = usedTable.children().first();
|
||
const rows = body.children();
|
||
|
||
const result: Food[][] = [];
|
||
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
||
if (!(dayIndex in result)) {
|
||
result[dayIndex] = [];
|
||
}
|
||
let parsing = false;
|
||
let isSoup = false;
|
||
rows.each((i, row) => {
|
||
const firstChild = $(row).children().get(0);
|
||
if (firstChild?.name == 'th') {
|
||
const childText = $(firstChild).text();
|
||
if (capitalize(DAYS_IN_WEEK[dayIndex]) === childText) {
|
||
parsing = true;
|
||
} else if (parsing) {
|
||
// Narazili jsme na další den - konec parsování
|
||
parsing = false;
|
||
return;
|
||
}
|
||
} else if (parsing) {
|
||
const children = $(row).children();
|
||
if (children.length === 1) { // Nadpis "Polévka" nebo "Hlavní jídlo"
|
||
const foodType = children.first().text();
|
||
if (foodType === 'Polévka') {
|
||
isSoup = true;
|
||
} else if (foodType === 'Hlavní jídlo') {
|
||
isSoup = false;
|
||
} else {
|
||
throw new Error("Neočekáváný typ jídla: " + foodType);
|
||
}
|
||
} else {
|
||
if (children.length !== 3) {
|
||
throw new Error("Neočekávaný počet child elementů pro jídlo: " + children.length + ", očekávány 3");
|
||
}
|
||
const amount = sanitizeText($(children.get(0)).text());
|
||
const name = sanitizeText($(children.get(1)).text());
|
||
const price = sanitizeText($(children.get(2)).text()).replace(',-', '').replace(' ', '\xA0');
|
||
result[dayIndex].push({
|
||
amount,
|
||
name,
|
||
price,
|
||
isSoup,
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Získá obědovou nabídku TechTower pro jeden týden.
|
||
*
|
||
* @param firstDayOfWeek první den v týdnu, pro který získat menu
|
||
* @param mock zda vrátit mock data
|
||
* @returns seznam jídel pro dané datum
|
||
*/
|
||
export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => {
|
||
if (mock) {
|
||
return getMenuTechTowerMock();
|
||
}
|
||
|
||
const html = await getHtml(TECHTOWER_URL);
|
||
const $ = load(html);
|
||
|
||
let secondTry = false;
|
||
let thirdTry = false;
|
||
// První pokus - varianta "Obědy"
|
||
let fonts = $('font.wsw-41');
|
||
let font: any = undefined;
|
||
fonts.each((i, f) => {
|
||
if ($(f).text().trim().startsWith('Obědy')) {
|
||
font = f;
|
||
}
|
||
})
|
||
// Druhý pokus - varianta "Jídelní lístek" (starší formát)
|
||
if (!font) {
|
||
fonts = $('font.wnd-font-size-90');
|
||
fonts.each((i, f) => {
|
||
if ($(f).text().trim().startsWith('Jídelní lístek')) {
|
||
font = f;
|
||
secondTry = true;
|
||
}
|
||
})
|
||
}
|
||
// Třetí pokus - nový formát: font.wsw-41 s textem "Jídelní lístek" (vše v jednom bloku)
|
||
if (!font) {
|
||
fonts = $('font.wsw-41');
|
||
fonts.each((i, f) => {
|
||
if ($(f).text().trim().startsWith('Jídelní lístek')) {
|
||
font = f;
|
||
thirdTry = true;
|
||
}
|
||
})
|
||
}
|
||
// Pátý pokus - formát z 07/2026: hlavička "Jídelní lístek" je v libovolném <font>
|
||
// (typicky wsw-31), jednotlivé položky jsou v samostatných <p> uvnitř vnořeného
|
||
// <div><font class="wsw-31">, den a jeho první položka mohou být ve stejném <p>
|
||
// oddělené <br>. Třída wsw-41 nyní obsahuje otevírací dobu, nikoli menu.
|
||
let fifthTry = false;
|
||
if (!font) {
|
||
$('font').each((_, f) => {
|
||
if (font) return;
|
||
if ($(f).text().trim().startsWith('Jídelní lístek')) {
|
||
font = f;
|
||
fifthTry = true;
|
||
}
|
||
})
|
||
}
|
||
if (!font) {
|
||
throw new Error('Chyba: nenalezen <font> pro obědy v HTML Techtower.');
|
||
}
|
||
// Šestý pokus - formát z 10/2026: hlavička je <p><strong><font class="wsw-41">,
|
||
// jednotlivé dny a položky jsou sourozenecké <p> v témže bloku. Třetí pokus sice najde
|
||
// hlavičku, ale <font> nemá žádné sourozenecké <p>, proto se použije parsing pátého pokusu.
|
||
if (thirdTry && $(font).parent().siblings('p').length === 0) {
|
||
thirdTry = false;
|
||
fifthTry = true;
|
||
}
|
||
|
||
const result: Food[][] = [];
|
||
|
||
// Z textu jedné položky odvodí název, cenu a alergeny (sdíleno napříč formáty).
|
||
const buildFoodItem = (text: string): Food => {
|
||
let price = 'na\xA0váhu';
|
||
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č`;
|
||
nameRaw = split[0].replace('•', '');
|
||
} else if (text.toLowerCase().endsWith(',-')) {
|
||
const tmp = text.replace('\xA0', ' ').split(' ');
|
||
const split = [tmp.slice(0, -1).join(' ')].concat(tmp.slice(-1));
|
||
price = `${split.slice(1)[0].replace(',-', '')}\xA0Kč`;
|
||
nameRaw = split[0].replace('•', '');
|
||
}
|
||
if (nameRaw.endsWith('–') || nameRaw.endsWith('—')) {
|
||
nameRaw = nameRaw.slice(0, -1).trim();
|
||
}
|
||
const parsed = parseAllergens(nameRaw);
|
||
return {
|
||
amount: '-',
|
||
name: parsed.cleanName,
|
||
price,
|
||
isSoup: isTextSoupName(parsed.cleanName),
|
||
allergens: parsed.allergens.length > 0 ? parsed.allergens : undefined,
|
||
};
|
||
};
|
||
|
||
// Pátý pokus (parsing): projdeme všechny <p> ve společném kontejneru, každý rozdělíme
|
||
// podle <br> na jednotlivé řádky. Řádek s názvem dne přepne aktuální den, ostatní řádky
|
||
// jsou položky. Zvládne tak i den a první položku ve stejném <p>.
|
||
if (fifthTry) {
|
||
let parsing = false;
|
||
let currentDayIndex = 0;
|
||
let container = $(font).parent().parent();
|
||
if (container.find('p').length === 0) {
|
||
// Formát 10/2026 - hlavička je přímo v <p>, kontejnerem je jeho rodič
|
||
container = $(font).closest('p').parent();
|
||
}
|
||
container.find('p').each((_, p) => {
|
||
const pHtml = $(p).html() ?? '';
|
||
const lines = pHtml.split(/<br\s*\/?>/i)
|
||
.map(part => $(`<span>${part}</span>`).text().trim())
|
||
.filter(line => line.length > 0);
|
||
for (const text of lines) {
|
||
const lower = text.toLocaleLowerCase();
|
||
// Za aktuálním týdnem může následovat menu na další týden - to již nezpracováváme
|
||
if (parsing && lower.startsWith('následující týden')) {
|
||
return false;
|
||
}
|
||
if (DAYS_IN_WEEK.includes(lower)) {
|
||
if (result[DAYS_IN_WEEK.indexOf(lower)]) {
|
||
// Opakující se den = začátek dalšího týdne
|
||
return false;
|
||
}
|
||
currentDayIndex = DAYS_IN_WEEK.indexOf(lower);
|
||
parsing = true;
|
||
result[currentDayIndex] ??= [];
|
||
continue;
|
||
}
|
||
if (!parsing) {
|
||
continue;
|
||
}
|
||
result[currentDayIndex].push(buildFoodItem(text));
|
||
}
|
||
});
|
||
return finalizeTechTower(result, $(font).text().trim(), firstDayOfWeek);
|
||
}
|
||
|
||
// Čtvrtý pokus (detekce): thirdTry našel <font>, ale nový formát má každý den v jednom <p>
|
||
// s položkami oddělenými <br> místo separátních <p> pro každou položku
|
||
const fourthTry = thirdTry && $(font).parent().siblings('p').toArray().some(el => {
|
||
const firstChild = $(el).contents().first();
|
||
return firstChild.is('strong') && DAYS_IN_WEEK.includes(firstChild.text().trim().toLocaleLowerCase());
|
||
});
|
||
|
||
const siblings = (fourthTry || thirdTry)
|
||
? $(font).parent().siblings('p')
|
||
: secondTry
|
||
? $(font).parent().parent().parent().siblings('p')
|
||
: $(font).parent().parent().siblings();
|
||
|
||
if (fourthTry) {
|
||
siblings.each((_, el) => {
|
||
const $el = $(el);
|
||
const firstChild = $el.contents().first();
|
||
if (!firstChild.is('strong')) return;
|
||
const dayName = firstChild.text().trim().toLocaleLowerCase();
|
||
if (!DAYS_IN_WEEK.includes(dayName)) return;
|
||
|
||
const dayIndex = DAYS_IN_WEEK.indexOf(dayName);
|
||
result[dayIndex] ??= [];
|
||
|
||
const elHtml = $el.html() ?? '';
|
||
const itemLines = elHtml.split(/<br\s*\/?>/i).slice(1)
|
||
.map(part => $(`<span>${part}</span>`).text().trim())
|
||
.filter(line => line.length > 0);
|
||
|
||
for (const text of itemLines) {
|
||
result[dayIndex].push(buildFoodItem(text));
|
||
}
|
||
});
|
||
} else {
|
||
let parsing = false;
|
||
let currentDayIndex = 0;
|
||
for (let i = 0; i < siblings.length; i++) {
|
||
const text = $(siblings.get(i)).text().trim().replace('\t', '').replace('\n', ' ');
|
||
if (DAYS_IN_WEEK.includes(text.toLocaleLowerCase())) {
|
||
// Zjistíme aktuální index
|
||
currentDayIndex = DAYS_IN_WEEK.indexOf(text.toLocaleLowerCase());
|
||
if (!parsing) {
|
||
// Našli jsme libovolný den v týdnu a ještě neparsujeme, tak začneme
|
||
parsing = true;
|
||
}
|
||
} else if (parsing) {
|
||
if (text.length == 0) {
|
||
// Prázdná řádka - bývá na zcela náhodných místech ¯\_(ツ)_/¯
|
||
continue;
|
||
}
|
||
result[currentDayIndex] ??= [];
|
||
result[currentDayIndex].push(buildFoodItem(text));
|
||
}
|
||
}
|
||
}
|
||
|
||
return finalizeTechTower(result, $(font).text().trim(), firstDayOfWeek);
|
||
}
|
||
|
||
/**
|
||
* Ověří, že datum v hlavičce TechTower spadá do požadovaného týdne, jinak vyhodí StaleWeekError.
|
||
*
|
||
* Datum v hlavičce nemusí být přesně pondělí (např. je-li pondělí svátek, začíná hlavička
|
||
* úterým), proto stačí, aby první nalezené datum padlo do libovolného dne požadovaného týdne.
|
||
* Toleruje i mezery za tečkami (formát "7. 7.-10. 7.").
|
||
*/
|
||
const finalizeTechTower = (result: Food[][], headerText: string, firstDayOfWeek: Date): Food[][] => {
|
||
const dateMatch = headerText.match(/(\d{1,2})\.\s*(\d{1,2})\./);
|
||
if (dateMatch) {
|
||
const foundDay = parseInt(dateMatch[1]);
|
||
const foundMonth = parseInt(dateMatch[2]) - 1; // JS months are 0-based
|
||
let matchesWeek = false;
|
||
for (let d = 0; d < 7; d++) {
|
||
const day = new Date(firstDayOfWeek);
|
||
day.setDate(firstDayOfWeek.getDate() + d);
|
||
if (day.getDate() === foundDay && day.getMonth() === foundMonth) {
|
||
matchesWeek = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!matchesWeek) {
|
||
throw new StaleWeekError(result);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Získá obědovou nabídku ZastavkaUmichala pro jeden týden.
|
||
*
|
||
* @param firstDayOfWeek první den v týdnu, pro který získat menu
|
||
* @param mock zda vrátit mock data
|
||
* @returns seznam jídel pro dané datum
|
||
*/
|
||
export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => {
|
||
if (mock) {
|
||
return getMenuZastavkaUmichalaMock();
|
||
}
|
||
|
||
const today = new Date();
|
||
today.setHours(0,0,0,0);
|
||
const headers = {
|
||
"Cookie": "_nss=1; PHPSESSID=9e37de17e0326b0942613d6e67a30e69",
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
|
||
};
|
||
const result: Food[][] = [];
|
||
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
||
const currentDate = new Date(firstDayOfWeek);
|
||
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
|
||
currentDate.setHours(0,0,0,0);
|
||
if (currentDate < today || (currentDate.getTime() === today.getTime() && new Date().getHours() >= 14)) {
|
||
result[dayIndex] = [{
|
||
amount: undefined,
|
||
name: "Pro tento den není uveřejněna nabídka jídel",
|
||
price: "",
|
||
isSoup: false,
|
||
}];
|
||
} else {
|
||
const url = (currentDate.getTime() === today.getTime())
|
||
? ZASTAVKAUMICHALA_URL
|
||
: ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + formatDate(currentDate, 'DD.MM.YYYY');
|
||
const html = await axios.get(url, {
|
||
headers,
|
||
}).then(res => res.data).then(content => content);
|
||
const $ = load(html);
|
||
|
||
const currentDayFood: Food[] = [];
|
||
$('.foodsList li').each((index, element) => {
|
||
currentDayFood.push({
|
||
amount: '-',
|
||
name: sanitizeText($(element).contents().not('span').text()),
|
||
price: sanitizeText($(element).find('span').text()),
|
||
isSoup: (index === 0),
|
||
});
|
||
});
|
||
result[dayIndex] = currentDayFood;
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Získá obědovou nabídku SenkSerikova pro jeden týden.
|
||
*
|
||
* @param firstDayOfWeek první den v týdnu, pro který získat menu
|
||
* @param mock zda vrátit mock data
|
||
* @returns seznam jídel pro dané datum
|
||
*/
|
||
export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => {
|
||
if (mock) {
|
||
return getMenuSenkSerikovaMock();
|
||
}
|
||
|
||
const decoder = new TextDecoder('windows-1250');
|
||
const html = await axios.get(SENKSERIKOVA_URL, {
|
||
responseType: 'arraybuffer',
|
||
responseEncoding: 'binary'
|
||
}).then(res => decoder.decode(new Uint8Array(res.data))).then(content => content);
|
||
const $ = load(html);
|
||
|
||
const today = new Date();
|
||
today.setHours(0,0,0,0);
|
||
const currentDate = new Date(firstDayOfWeek);
|
||
const result: Food[][] = [];
|
||
let dayIndex = 0;
|
||
currentDate.setHours(0,0,0,0);
|
||
while (currentDate < today) {
|
||
result[dayIndex] = [{
|
||
amount: undefined,
|
||
name: "Pro tento den není uveřejněna nabídka jídel",
|
||
price: "",
|
||
isSoup: false,
|
||
}];
|
||
dayIndex = dayIndex + 1;
|
||
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
|
||
}
|
||
|
||
$('.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: nameWithoutNumber,
|
||
price: $(element).children('div.cena').text().replaceAll(' ', '\xA0'),
|
||
isSoup: $(element).hasClass('polevka'),
|
||
});
|
||
});
|
||
result[dayIndex++] = currentDayFood;
|
||
});
|
||
return result;
|
||
}
|