fix(search): robust Czech fulltext search engine with stemming, multi-token matching and highlight
This commit is contained in:
@@ -29,12 +29,12 @@ function playSpeaker(freq = 800, duration = 0.04, type = 'square') {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data references
|
// Data references loaded from data.js
|
||||||
const KNIHA_RECEPTU = window.KNIHA_RECEPTU || [];
|
const KNIHA_RECEPTU = window.KNIHA_RECEPTU || [];
|
||||||
const HISTORICKA_JIDLA = window.HISTORICKA_JIDLA || [];
|
const HISTORICKA_JIDLA = window.HISTORICKA_JIDLA || [];
|
||||||
|
|
||||||
// App State
|
// App State
|
||||||
let currentView = 'menu'; // 'menu', 'recept', 'recepty_list', 'suroviny', 'davno', 'oblibena', 'kronika', 'altan'
|
let currentView = 'menu'; // 'menu', 'recept', 'recepty_list', 'hledat', 'suroviny', 'davno', 'oblibena', 'kronika', 'altan'
|
||||||
let currentRecipeId = 1;
|
let currentRecipeId = 1;
|
||||||
let currentCategory = 'Vše';
|
let currentCategory = 'Vše';
|
||||||
let listPage = 0;
|
let listPage = 0;
|
||||||
@@ -50,11 +50,163 @@ setInterval(() => {
|
|||||||
timeEl.textContent = d.toTimeString().split(' ')[0];
|
timeEl.textContent = d.toTimeString().split(' ')[0];
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Normalization function (strip accents & non-alphanumeric)
|
// =========================================================================
|
||||||
|
// CZECH TEXT NORMALIZATION & FULLTEXT SEARCH ENGINE
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Normalizace: malá písmena, odstranění diakritiky
|
||||||
function norm(str) {
|
function norm(str) {
|
||||||
return (str || "").toLowerCase()
|
return (str || "").toLowerCase()
|
||||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
||||||
.replace(/[^a-z0-9]/g, "");
|
}
|
||||||
|
|
||||||
|
// Český lehký stemmer (odstranění pádových a slovesných koncovek pro dokonalou flexi)
|
||||||
|
function stem(word) {
|
||||||
|
let w = norm(word).replace(/[^a-z0-9]/g, "");
|
||||||
|
if (w.length <= 3) return w;
|
||||||
|
// Dlouhé přídavné a slovesné koncovky
|
||||||
|
w = w.replace(/(ovy|ova|ove|ymi|ych|ich|ovi|ami|ach|ech|eho|emu|ajici|ejici)$/, "");
|
||||||
|
if (w.length > 3) {
|
||||||
|
// Slovesa a podstatná jména
|
||||||
|
w = w.replace(/(ime|ite|eme|ete|aji|eji|ala|alo|ali|ila|ilo|ili|ula|ulo|uli)$/, "");
|
||||||
|
}
|
||||||
|
if (w.length > 3) {
|
||||||
|
// Dvojpísmenné koncovky
|
||||||
|
w = w.replace(/(em|ou|am|um|ym|im|ky|ka|ku|ek|ce|ci|ho|mu|mi|es|as|at|et|it|ut|ti|al|il|ul|ne|na|ny)$/, "");
|
||||||
|
}
|
||||||
|
if (w.length > 3) {
|
||||||
|
// Krátké samohlásky
|
||||||
|
w = w.replace(/[aeiouy]$/, "");
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rozdělení dotazu na čistá slova
|
||||||
|
function tokenize(str) {
|
||||||
|
return norm(str)
|
||||||
|
.split(/[\s,.;:()!?"'\/\\+*#%&<=>[\]{}|~_`^$]+/)
|
||||||
|
.filter(t => t.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kontrola shody slova:
|
||||||
|
function wordMatches(targetWord, queryWord, queryStem) {
|
||||||
|
const tw = norm(targetWord);
|
||||||
|
if (tw === queryWord) return true;
|
||||||
|
if (tw.startsWith(queryWord) || queryWord.startsWith(tw)) return true;
|
||||||
|
|
||||||
|
const ts = stem(targetWord);
|
||||||
|
if (ts === queryStem) return true;
|
||||||
|
if (queryStem.length >= 3 && ts.startsWith(queryStem)) return true;
|
||||||
|
if (ts.length >= 3 && queryStem.startsWith(ts)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zvýraznění shod v textu (HTML mark)
|
||||||
|
function highlightMatches(text, query) {
|
||||||
|
if (!query || !text) return text;
|
||||||
|
const tokens = tokenize(query).filter(t => t.length > 1);
|
||||||
|
if (tokens.length === 0) return text;
|
||||||
|
|
||||||
|
const stems = tokens.map(stem);
|
||||||
|
const parts = text.split(/(\s+|[.,;()\-+*\/:"'!?<>])/);
|
||||||
|
|
||||||
|
return parts.map(part => {
|
||||||
|
if (!part.trim()) return part;
|
||||||
|
const np = norm(part);
|
||||||
|
const sp = stem(part);
|
||||||
|
const isMatch = tokens.some((tok, idx) => wordMatches(part, tok, stems[idx]));
|
||||||
|
if (isMatch) {
|
||||||
|
return `<mark class="match-highlight">${part}</mark>`;
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Robustní vyhledávání v sešitu Kamily Pešek
|
||||||
|
function searchKniha(query, categoryFilter = 'Vše') {
|
||||||
|
if (!query || !query.trim()) return [];
|
||||||
|
const tokens = tokenize(query);
|
||||||
|
if (tokens.length === 0) return [];
|
||||||
|
const stems = tokens.map(stem);
|
||||||
|
|
||||||
|
const source = categoryFilter === 'Vše'
|
||||||
|
? KNIHA_RECEPTU
|
||||||
|
: KNIHA_RECEPTU.filter(r => r.kategorie === categoryFilter);
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
source.forEach(r => {
|
||||||
|
const titleWords = tokenize(r.nazev);
|
||||||
|
const surWords = tokenize((r.suroviny || []).join(" "));
|
||||||
|
const posWords = tokenize(r.postup || "");
|
||||||
|
const katWords = tokenize(r.kategorie || "");
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
let matchedTokens = 0;
|
||||||
|
let snippet = "";
|
||||||
|
|
||||||
|
tokens.forEach((tok, idx) => {
|
||||||
|
const st = stems[idx];
|
||||||
|
let hit = false;
|
||||||
|
|
||||||
|
// 1. Shoda v názvu (priorita #1)
|
||||||
|
if (titleWords.some(tw => wordMatches(tw, tok, st))) {
|
||||||
|
score += 150;
|
||||||
|
hit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Shoda v surovinách
|
||||||
|
if (surWords.some(sw => wordMatches(sw, tok, st))) {
|
||||||
|
score += 50;
|
||||||
|
hit = true;
|
||||||
|
if (!snippet) {
|
||||||
|
const found = (r.suroviny || []).find(s => tokenize(s).some(sw => wordMatches(sw, tok, st)));
|
||||||
|
if (found) snippet = "Surovina: " + found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Shoda v kategorii
|
||||||
|
if (katWords.some(kw => wordMatches(kw, tok, st))) {
|
||||||
|
score += 25;
|
||||||
|
hit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Shoda v postupu
|
||||||
|
if (posWords.some(pw => wordMatches(pw, tok, st))) {
|
||||||
|
score += 20;
|
||||||
|
hit = true;
|
||||||
|
if (!snippet && r.postup) {
|
||||||
|
snippet = "Postup: " + r.postup.substring(0, 110) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hit) matchedTokens++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Masivní bonus za pokrytí všech hledaných slov (AND logika)
|
||||||
|
if (matchedTokens === tokens.length) {
|
||||||
|
score += 500 * tokens.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (score > 0) {
|
||||||
|
results.push({
|
||||||
|
recipe: r,
|
||||||
|
score: score,
|
||||||
|
matchedCount: matchedTokens,
|
||||||
|
snippet: snippet
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seřadit: nejdřív ty, co splňují všechny tokeny, pak podle skóre
|
||||||
|
results.sort((a, b) => {
|
||||||
|
const fullA = a.matchedCount === tokens.length ? 1 : 0;
|
||||||
|
const fullB = b.matchedCount === tokens.length ? 1 : 0;
|
||||||
|
if (fullA !== fullB) return fullB - fullA;
|
||||||
|
return b.score - a.score || a.recipe.id - b.recipe.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories list
|
// Categories list
|
||||||
@@ -66,10 +218,15 @@ const KATEGORIE = [
|
|||||||
'Moučníky a sladké',
|
'Moučníky a sladké',
|
||||||
'Přílohy a bezmasá',
|
'Přílohy a bezmasá',
|
||||||
'Omáčky',
|
'Omáčky',
|
||||||
'Saláty a zelenina'
|
'Saláty a zelenina',
|
||||||
|
'Ostatní'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Views renderers
|
// =========================================================================
|
||||||
|
// VIEWS & CONTROLLERS
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// 1. HLAVNÍ NABÍDKA
|
||||||
function renderMainMenu() {
|
function renderMainMenu() {
|
||||||
currentView = 'menu';
|
currentView = 'menu';
|
||||||
winTitleEl.textContent = "KUCHAŘKA KAMILY PEŠEK // HLAVNÍ NABÍDKA PASCAL 7.0";
|
winTitleEl.textContent = "KUCHAŘKA KAMILY PEŠEK // HLAVNÍ NABÍDKA PASCAL 7.0";
|
||||||
@@ -86,19 +243,30 @@ function renderMainMenu() {
|
|||||||
sešitu (strany 1–43) a <strong>${HISTORICKA_JIDLA.length} záznamů rodinného jídelníčku</strong> z let 1998–2021.
|
sešitu (strany 1–43) a <strong>${HISTORICKA_JIDLA.length} záznamů rodinného jídelníčku</strong> z let 1998–2021.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="dos-header" style="color:var(--dos-cyan)">── RYCHLÉ VYHLEDÁVÁNÍ V KUCHAŘCE ───────────────────────────────────────────</div>
|
||||||
|
<div class="dos-input-group" style="margin:6px 0 12px 0">
|
||||||
|
<span style="color:var(--dos-yellow); font-weight:bold">HLEDAT:</span>
|
||||||
|
<input type="text" id="menu-quick-search" class="dos-input" placeholder="Napište název, surovinu či postup (např. kuře, bábovka, bramborový salát, česnek)...">
|
||||||
|
<button class="dos-btn" onclick="triggerMenuSearch()">HLEDAT [ENTER]</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="dos-header" style="color:var(--dos-cyan)">── HLAVNÍ PROGRAMOVÉ MODULY ────────────────────────────────────────────────</div>
|
<div class="dos-header" style="color:var(--dos-cyan)">── HLAVNÍ PROGRAMOVÉ MODULY ────────────────────────────────────────────────</div>
|
||||||
<div class="dos-menu-list">
|
<div class="dos-menu-list">
|
||||||
|
<div class="dos-menu-item" onclick="openSearch()">
|
||||||
|
<span class="hotkey">[H / /]</span>
|
||||||
|
<span style="color:var(--dos-yellow); font-weight:bold">>> FULLTEXTOVÉ HLEDÁNÍ V CELÉM SEŠITU (${KNIHA_RECEPTU.length} receptů) <<</span>
|
||||||
|
</div>
|
||||||
<div class="dos-menu-item" onclick="openReceptyList('Vše')">
|
<div class="dos-menu-item" onclick="openReceptyList('Vše')">
|
||||||
<span class="hotkey">[R]</span>
|
<span class="hotkey">[R]</span>
|
||||||
<span style="color:var(--dos-white); font-weight:bold">>> PROCHÁZET CELÝ RECEPTÁŘ KAMILY PEŠEK (${KNIHA_RECEPTU.length} receptů dle kategorií) <<</span>
|
<span style="color:var(--dos-white); font-weight:bold">Procházet receptář Kamily Pešek podle kategorií</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dos-menu-item" onclick="openSuroviny()">
|
<div class="dos-menu-item" onclick="openSuroviny()">
|
||||||
<span class="hotkey">[S]</span>
|
<span class="hotkey">[S]</span>
|
||||||
<span style="color:var(--dos-yellow); font-weight:bold">>> VAŘIT PODLE SUROVIN VE ŠPAJZU << (Algoritmus prohledání lednice)</span>
|
<span style="color:var(--dos-green); font-weight:bold">Vařit podle surovin ve špajzu a lednici (Algoritmus KUCHARKA.EXE)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dos-menu-item" onclick="openRandomRecipe()">
|
<div class="dos-menu-item" onclick="openRandomRecipe()">
|
||||||
<span class="hotkey">[N]</span>
|
<span class="hotkey">[N]</span>
|
||||||
<span style="color:var(--dos-green)">Náhodný recept pro dnešní den (Inspirace z Kamilina sešitu)</span>
|
<span>Náhodný recept pro dnešní den (Inspirace ze sešitu)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dos-menu-item" onclick="openDavno()">
|
<div class="dos-menu-item" onclick="openDavno()">
|
||||||
<span class="hotkey">[D]</span>
|
<span class="hotkey">[D]</span>
|
||||||
@@ -110,7 +278,7 @@ function renderMainMenu() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="dos-menu-item" onclick="openKronika()">
|
<div class="dos-menu-item" onclick="openKronika()">
|
||||||
<span class="hotkey">[K]</span>
|
<span class="hotkey">[K]</span>
|
||||||
<span>Kronika rodinného stolu (Prohlížeč ${HISTORICKA_JIDLA.length} záznamů z Bohnic a Dejvic)</span>
|
<span>Kronika rodinného stolu (${HISTORICKA_JIDLA.length} záznamů Bohnice a Dejvice)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dos-menu-item" onclick="openAltan()">
|
<div class="dos-menu-item" onclick="openAltan()">
|
||||||
<span class="hotkey">[A]</span>
|
<span class="hotkey">[A]</span>
|
||||||
@@ -118,8 +286,8 @@ function renderMainMenu() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dos-header" style="color:var(--dos-cyan)">── RYCHLÝ VÝBĚR KATEGORIE ──────────────────────────────────────────────────</div>
|
<div class="dos-header" style="color:var(--dos-cyan)">── RYCHLÝ VÝBĚR DLE KATEGORIE ──────────────────────────────────────────────</div>
|
||||||
<div style="display:flex; flex-wrap:wrap; gap:8px; margin:10px 0">
|
<div style="display:flex; flex-wrap:wrap; gap:8px; margin:8px 0">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
KATEGORIE.slice(1).forEach((cat, idx) => {
|
KATEGORIE.slice(1).forEach((cat, idx) => {
|
||||||
@@ -129,32 +297,186 @@ function renderMainMenu() {
|
|||||||
|
|
||||||
html += `</div>
|
html += `</div>
|
||||||
|
|
||||||
<div style="margin-top:20px; color:var(--dos-grey)">
|
<div style="margin-top:14px; color:var(--dos-grey)">
|
||||||
Zvolte klávesu <span class="hotkey">[R, S, N, D, O, K, A]</span> nebo kategorii <span class="hotkey">[1-7]</span>. Na mobilu klepněte prstem.
|
Zvolte klávesu <span class="hotkey">[H, R, S, N, D, O, K, A, /]</span> nebo kategorii <span class="hotkey">[1-8]</span>. Na mobilu klepněte prstem.
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
screenEl.innerHTML = html;
|
screenEl.innerHTML = html;
|
||||||
|
|
||||||
|
const quickInput = document.getElementById('menu-quick-search');
|
||||||
|
if (quickInput) {
|
||||||
|
quickInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') triggerMenuSearch();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// List of recipes with category filter & pagination
|
function triggerMenuSearch() {
|
||||||
function openReceptyList(category = 'Vše', page = 0) {
|
const input = document.getElementById('menu-quick-search');
|
||||||
|
if (input && input.value.trim()) {
|
||||||
|
openSearch(input.value.trim());
|
||||||
|
} else {
|
||||||
|
openSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. FULLTEXTOVÉ VYHLEDÁVÁNÍ (DEDIKOVANÉ ROZHRANÍ)
|
||||||
|
function openSearch(defaultQuery = "") {
|
||||||
|
currentView = 'hledat';
|
||||||
|
playSpeaker(750, 0.05);
|
||||||
|
winTitleEl.textContent = `HLEDÁNÍ V KUCHAŘCE KAMILY PEŠEK // FULLTEXT ENGINE`;
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
|
||||||
|
FULLTEXTOVÉ VYHLEDÁVÁNÍ // RECEPTÁŘ KAMILY PEŠEK (${KNIHA_RECEPTU.length} RECEPTŮ)
|
||||||
|
════════════════════════════════════════════════════════════════════════════════</div>
|
||||||
|
|
||||||
|
<div class="dos-box">
|
||||||
|
Zadejte hledaný název jídla, surovinu nebo kousek postupu (např. <em>kuře, bábovka, kmínová, guláš, česnek, ořechy, brambory</em>).
|
||||||
|
Vyhledávač automaticky ohýbá česká slova a prohledává všech <strong>${KNIHA_RECEPTU.length} receptů ze sešitu</strong>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dos-input-group" style="margin:10px 0 12px 0">
|
||||||
|
<span style="color:var(--dos-yellow); font-weight:bold">HLEDAT V RECEPTÁŘI:</span>
|
||||||
|
<input type="text" id="global-search-input" class="dos-input" placeholder="Zadejte hledané slovo..." value="${defaultQuery}">
|
||||||
|
<button class="dos-btn" onclick="executeGlobalSearch()">HLEDAT [ENTER]</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:12px">
|
||||||
|
<span style="color:var(--dos-cyan); align-self:center; font-size:13px">Časté dotazy:</span>
|
||||||
|
<button class="dos-btn" onclick="quickQuery('kuře')">kuře</button>
|
||||||
|
<button class="dos-btn" onclick="quickQuery('brambory')">brambory</button>
|
||||||
|
<button class="dos-btn" onclick="quickQuery('bábovka')">bábovka</button>
|
||||||
|
<button class="dos-btn" onclick="quickQuery('polévka')">polévka</button>
|
||||||
|
<button class="dos-btn" onclick="quickQuery('guláš')">guláš</button>
|
||||||
|
<button class="dos-btn" onclick="quickQuery('salát')">salát</button>
|
||||||
|
<button class="dos-btn" onclick="quickQuery('cukroví')">cukroví</button>
|
||||||
|
<button class="dos-btn" onclick="quickQuery('česnek')">česnek</button>
|
||||||
|
<button class="dos-btn" onclick="quickQuery('tvaroh')">tvaroh</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="global-search-results">
|
||||||
|
<!-- Dynamické výsledky hledání -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:20px; display:flex; gap:10px">
|
||||||
|
<button class="dos-btn" onclick="renderMainMenu()">[ESC] ZPĚT DO MENU</button>
|
||||||
|
<button class="dos-btn" onclick="openReceptyList('Vše')">[R] CELÝ SEZNAM RECEPTŮ</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
screenEl.innerHTML = html;
|
||||||
|
|
||||||
|
const input = document.getElementById('global-search-input');
|
||||||
|
input.focus();
|
||||||
|
input.setSelectionRange(input.value.length, input.value.length);
|
||||||
|
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
executeGlobalSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') executeGlobalSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (defaultQuery) {
|
||||||
|
executeGlobalSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickQuery(q) {
|
||||||
|
const input = document.getElementById('global-search-input');
|
||||||
|
if (input) {
|
||||||
|
input.value = q;
|
||||||
|
executeGlobalSearch();
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeGlobalSearch() {
|
||||||
|
const input = document.getElementById('global-search-input');
|
||||||
|
const resEl = document.getElementById('global-search-results');
|
||||||
|
if (!input || !resEl) return;
|
||||||
|
|
||||||
|
const query = input.value.trim();
|
||||||
|
if (!query) {
|
||||||
|
resEl.innerHTML = `<div style="color:var(--dos-grey); padding:10px">Zadejte alespoň 2 písmena pro vyhledání v sešitu Kamily Pešek...</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playSpeaker(850, 0.03);
|
||||||
|
const results = searchKniha(query, 'Vše');
|
||||||
|
|
||||||
|
let html = "";
|
||||||
|
if (results.length > 0) {
|
||||||
|
html += `<div style="color:var(--dos-yellow); margin-bottom:8px; font-weight:bold">
|
||||||
|
★ Nalezeno ${results.length} receptů pro dotaz "${query}":
|
||||||
|
</div>`;
|
||||||
|
html += `<div class="dos-menu-list">`;
|
||||||
|
|
||||||
|
results.slice(0, 20).forEach(item => {
|
||||||
|
const r = item.recipe;
|
||||||
|
const highlightedTitle = highlightMatches(r.nazev, query);
|
||||||
|
const highlightedSnippet = item.snippet ? highlightMatches(item.snippet, query) : "";
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="dos-menu-item" onclick="openRecipe(${r.id})" style="flex-direction:column; align-items:flex-start">
|
||||||
|
<div style="display:flex; width:100%; justify-content:space-between; align-items:center">
|
||||||
|
<span>
|
||||||
|
<span class="hotkey">[${r.id}]</span>
|
||||||
|
<span style="color:var(--dos-white); font-weight:bold">${highlightedTitle}</span>
|
||||||
|
</span>
|
||||||
|
<span class="tag-badge">Str. ${r.strana} | ${r.kategorie}</span>
|
||||||
|
</div>
|
||||||
|
${highlightedSnippet ? `<div class="search-result-snippet">${highlightedSnippet}</div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
if (results.length > 20) {
|
||||||
|
html += `<div style="color:var(--dos-grey); margin-top:8px; font-size:13px">...a dalších ${results.length - 20} receptů. Upřesněte dotaz pro zúžení výběru.</div>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html += `<div style="color:var(--dos-red); padding:12px 0">
|
||||||
|
Pro dotaz "${query}" nebyl v sešitu Kamily Pešek nalezen žádný recept.
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Prohledat i kroniku 568 jídel jako zálohu
|
||||||
|
const histMatches = HISTORICKA_JIDLA.filter(j => norm(j.jidlo).includes(norm(query)));
|
||||||
|
if (histMatches.length > 0) {
|
||||||
|
html += `<div style="color:var(--dos-cyan); margin:12px 0 6px 0; font-weight:bold">
|
||||||
|
📜 Nalezeno ale v Kronice rodinného stolu (1998–2021):
|
||||||
|
</div>`;
|
||||||
|
histMatches.slice(0, 8).forEach(j => {
|
||||||
|
html += `<div style="padding:3px 8px; border-left:2px solid var(--dos-cyan); margin:3px 0">
|
||||||
|
<span style="color:var(--dos-white)">${j.jidlo}</span>
|
||||||
|
<span style="color:var(--dos-grey)"> [${j.datum}, ${j.typ}]</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resEl.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. SEZNAM RECEPTŮ PODLE KATEGORIÍ S INTEGROVANÝM VYHLEDÁVÁNÍM
|
||||||
|
function openReceptyList(category = 'Vše', page = 0, initialQuery = "") {
|
||||||
currentView = 'recepty_list';
|
currentView = 'recepty_list';
|
||||||
currentCategory = category;
|
currentCategory = category;
|
||||||
listPage = page;
|
listPage = page;
|
||||||
playSpeaker(700, 0.04);
|
playSpeaker(700, 0.04);
|
||||||
|
|
||||||
const filtered = category === 'Vše' ? KNIHA_RECEPTU : KNIHA_RECEPTU.filter(r => r.kategorie === category);
|
const baseList = category === 'Vše' ? KNIHA_RECEPTU : KNIHA_RECEPTU.filter(r => r.kategorie === category);
|
||||||
const totalPages = Math.ceil(filtered.length / PAGE_SIZE);
|
const totalPages = Math.ceil(baseList.length / PAGE_SIZE);
|
||||||
const pageItems = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
const pageItems = baseList.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
||||||
|
|
||||||
winTitleEl.textContent = `RECEPTÁŘ KAMILY PEŠEK // ${category.toUpperCase()} (${filtered.length} RECEPTŮ)`;
|
winTitleEl.textContent = `RECEPTÁŘ KAMILY PEŠEK // ${category.toUpperCase()} (${baseList.length} RECEPTŮ)`;
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
|
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
|
||||||
RECEPTÁŘ KAMILY PEŠEK // KATEGORIE: ${category.toUpperCase()} (Strana ${page + 1}/${totalPages || 1})
|
RECEPTÁŘ KAMILY PEŠEK // KATEGORIE: ${category.toUpperCase()} (Strana ${page + 1}/${totalPages || 1})
|
||||||
════════════════════════════════════════════════════════════════════════════════</div>
|
════════════════════════════════════════════════════════════════════════════════</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:12px">
|
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:10px">
|
||||||
`;
|
`;
|
||||||
KATEGORIE.forEach(cat => {
|
KATEGORIE.forEach(cat => {
|
||||||
const activeClass = cat === category ? 'style="background-color:var(--dos-yellow); color:var(--dos-black)"' : '';
|
const activeClass = cat === category ? 'style="background-color:var(--dos-yellow); color:var(--dos-black)"' : '';
|
||||||
@@ -163,14 +485,17 @@ function openReceptyList(category = 'Vše', page = 0) {
|
|||||||
|
|
||||||
html += `</div>
|
html += `</div>
|
||||||
|
|
||||||
<div class="dos-input-group" style="margin:8px 0 14px 0">
|
<div class="dos-input-group" style="margin:6px 0 12px 0">
|
||||||
<input type="text" id="recipe-search-input" class="dos-input" placeholder="Rychlé hledání v receptech (např. bábovka, kuře, kmínová, závin)...">
|
<input type="text" id="recipe-search-input" class="dos-input" placeholder="Rychlé hledání v receptech (např. bábovka, kuře, brambory, kmínová)..." value="${initialQuery}">
|
||||||
|
<button class="dos-btn" onclick="clearRecipeSearch('${category}')">ZRUŠIT FILTR</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="search-status-bar" style="margin-bottom:8px; font-size:13px; color:var(--dos-yellow)"></div>
|
||||||
|
|
||||||
<div class="dos-menu-list" id="recipe-items-container">
|
<div class="dos-menu-list" id="recipe-items-container">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
pageItems.forEach((r, idx) => {
|
pageItems.forEach((r) => {
|
||||||
html += `<div class="dos-menu-item" onclick="openRecipe(${r.id})">
|
html += `<div class="dos-menu-item" onclick="openRecipe(${r.id})">
|
||||||
<span class="hotkey">[${r.id}]</span>
|
<span class="hotkey">[${r.id}]</span>
|
||||||
<span style="color:var(--dos-white)">${r.nazev}</span>
|
<span style="color:var(--dos-white)">${r.nazev}</span>
|
||||||
@@ -180,14 +505,14 @@ function openReceptyList(category = 'Vše', page = 0) {
|
|||||||
|
|
||||||
html += `</div>
|
html += `</div>
|
||||||
|
|
||||||
<div style="margin-top:16px; display:flex; justify-content:space-between; align-items:center">
|
<div id="recipe-pagination" style="margin-top:16px; display:flex; justify-content:space-between; align-items:center">
|
||||||
<div style="display:flex; gap:10px">
|
<div style="display:flex; gap:10px">
|
||||||
<button class="dos-btn" onclick="renderMainMenu()">[ESC] HLAVNÍ NABÍDKA</button>
|
<button class="dos-btn" onclick="renderMainMenu()">[ESC] HLAVNÍ NABÍDKA</button>
|
||||||
${page > 0 ? `<button class="dos-btn" onclick="openReceptyList('${category}', ${page - 1})">[◄] PŘEDCHOZÍ STRANA</button>` : ''}
|
${page > 0 ? `<button class="dos-btn" onclick="openReceptyList('${category}', ${page - 1})">[◄] PŘEDCHOZÍ STRANA</button>` : ''}
|
||||||
${page < totalPages - 1 ? `<button class="dos-btn" onclick="openReceptyList('${category}', ${page + 1})">[►] DALŠÍ STRANA</button>` : ''}
|
${page < totalPages - 1 ? `<button class="dos-btn" onclick="openReceptyList('${category}', ${page + 1})">[►] DALŠÍ STRANA</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style="color:var(--dos-yellow); font-size:13px">
|
<div style="color:var(--dos-yellow); font-size:13px">
|
||||||
Celkem ${filtered.length} receptů
|
Celkem ${baseList.length} receptů v této kategorii
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -195,29 +520,67 @@ function openReceptyList(category = 'Vše', page = 0) {
|
|||||||
|
|
||||||
// Search input live filtering
|
// Search input live filtering
|
||||||
const searchInput = document.getElementById('recipe-search-input');
|
const searchInput = document.getElementById('recipe-search-input');
|
||||||
searchInput.addEventListener('input', (e) => {
|
|
||||||
const q = norm(e.target.value);
|
|
||||||
if (!q) {
|
|
||||||
openReceptyList(category, page);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const matched = filtered.filter(r => norm(r.nazev).includes(q) || norm(r.postup).includes(q) || r.suroviny.some(s => norm(s).includes(q)));
|
|
||||||
const container = document.getElementById('recipe-items-container');
|
const container = document.getElementById('recipe-items-container');
|
||||||
let mHtml = "";
|
const pagination = document.getElementById('recipe-pagination');
|
||||||
matched.slice(0, 15).forEach(r => {
|
const statusBar = document.getElementById('search-status-bar');
|
||||||
mHtml += `<div class="dos-menu-item" onclick="openRecipe(${r.id})">
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
const q = searchInput.value.trim();
|
||||||
|
if (!q) {
|
||||||
|
statusBar.innerHTML = "";
|
||||||
|
pagination.style.display = "flex";
|
||||||
|
let defaultHtml = "";
|
||||||
|
pageItems.forEach((r) => {
|
||||||
|
defaultHtml += `<div class="dos-menu-item" onclick="openRecipe(${r.id})">
|
||||||
<span class="hotkey">[${r.id}]</span>
|
<span class="hotkey">[${r.id}]</span>
|
||||||
<span style="color:var(--dos-white)">${r.nazev}</span>
|
<span style="color:var(--dos-white)">${r.nazev}</span>
|
||||||
<span class="tag-badge" style="margin-left:auto">Str. ${r.strana} | ${r.kategorie}</span>
|
<span class="tag-badge" style="margin-left:auto">Str. ${r.strana} | ${r.kategorie}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
|
container.innerHTML = defaultHtml;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hledat primárně v celé knize, pokud uživatel píše dotaz
|
||||||
|
const matched = searchKniha(q, 'Vše');
|
||||||
|
pagination.style.display = "none";
|
||||||
|
statusBar.innerHTML = `Nalezeno <strong>${matched.length} receptů</strong> v celém sešitu pro dotaz "<em>${q}</em>":`;
|
||||||
|
|
||||||
|
let mHtml = "";
|
||||||
|
matched.slice(0, 25).forEach(item => {
|
||||||
|
const r = item.recipe;
|
||||||
|
const highlightedTitle = highlightMatches(r.nazev, q);
|
||||||
|
const snippet = item.snippet ? highlightMatches(item.snippet, q) : "";
|
||||||
|
mHtml += `<div class="dos-menu-item" onclick="openRecipe(${r.id})" style="flex-direction:column; align-items:flex-start">
|
||||||
|
<div style="display:flex; width:100%; justify-content:space-between; align-items:center">
|
||||||
|
<span>
|
||||||
|
<span class="hotkey">[${r.id}]</span>
|
||||||
|
<span style="color:var(--dos-white); font-weight:bold">${highlightedTitle}</span>
|
||||||
|
</span>
|
||||||
|
<span class="tag-badge">Str. ${r.strana} | ${r.kategorie}</span>
|
||||||
|
</div>
|
||||||
|
${snippet ? `<div class="search-result-snippet">${snippet}</div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
if (matched.length === 0) {
|
if (matched.length === 0) {
|
||||||
mHtml = `<div style="color:var(--dos-red); padding:10px">Žádný recept neodpovídá hledanému výrazu "${e.target.value}".</div>`;
|
mHtml = `<div style="color:var(--dos-red); padding:10px">Žádný recept v sešitu neodpovídá hledanému výrazu "${q}".</div>`;
|
||||||
}
|
}
|
||||||
container.innerHTML = mHtml;
|
container.innerHTML = mHtml;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', handleSearch);
|
||||||
|
|
||||||
|
if (initialQuery) {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearRecipeSearch(category) {
|
||||||
|
openReceptyList(category, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. DETAIL RECEPTU
|
||||||
function openRecipe(id) {
|
function openRecipe(id) {
|
||||||
const r = KNIHA_RECEPTU.find(x => x.id === id);
|
const r = KNIHA_RECEPTU.find(x => x.id === id);
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
@@ -236,15 +599,19 @@ function openRecipe(id) {
|
|||||||
<div class="dos-header" style="color:var(--dos-cyan)">── POTŘEBNÉ SUROVINY ZE SEŠITU ─────────────────────────────────────────────</div>
|
<div class="dos-header" style="color:var(--dos-cyan)">── POTŘEBNÉ SUROVINY ZE SEŠITU ─────────────────────────────────────────────</div>
|
||||||
<ul style="list-style:none; padding-left:10px; margin-bottom:15px">
|
<ul style="list-style:none; padding-left:10px; margin-bottom:15px">
|
||||||
`;
|
`;
|
||||||
|
if (r.suroviny && r.suroviny.length > 0) {
|
||||||
r.suroviny.forEach(s => {
|
r.suroviny.forEach(s => {
|
||||||
html += `<li style="padding:2px 0">• <span style="color:var(--dos-yellow)">${s}</span></li>`;
|
html += `<li style="padding:2px 0">• <span style="color:var(--dos-yellow)">${s}</span></li>`;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
html += `<li style="padding:2px 0; color:var(--dos-grey)">• Běžné základní suroviny dle chuti a zvyklostí</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
html += `</ul>
|
html += `</ul>
|
||||||
|
|
||||||
<div class="dos-header" style="color:var(--dos-cyan)">── POSTUP PŘÍPRAVY ZE SEŠITU KAMILY PEŠEK ─────────────────────────────────</div>
|
<div class="dos-header" style="color:var(--dos-cyan)">── POSTUP PŘÍPRAVY ZE SEŠITU KAMILY PEŠEK ─────────────────────────────────</div>
|
||||||
<div class="dos-box" style="line-height:1.55; color:var(--dos-white)">
|
<div class="dos-box" style="line-height:1.55; color:var(--dos-white)">
|
||||||
${r.postup}
|
${r.postup || "Tradiční rodinný postup podle zápisu Kamily Pešek."}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="secret-badge">
|
<div class="secret-badge">
|
||||||
@@ -254,6 +621,7 @@ ${r.postup}
|
|||||||
|
|
||||||
<div style="margin-top:20px; display:flex; gap:10px; flex-wrap:wrap">
|
<div style="margin-top:20px; display:flex; gap:10px; flex-wrap:wrap">
|
||||||
<button class="dos-btn" onclick="openReceptyList(currentCategory, listPage)">[ESC] ZPĚT DO SEZNAMU</button>
|
<button class="dos-btn" onclick="openReceptyList(currentCategory, listPage)">[ESC] ZPĚT DO SEZNAMU</button>
|
||||||
|
<button class="dos-btn" onclick="openSearch()">[/] HLEDAT JINÝ</button>
|
||||||
<button class="dos-btn" onclick="renderMainMenu()">[H] HLAVNÍ NABÍDKA</button>
|
<button class="dos-btn" onclick="renderMainMenu()">[H] HLAVNÍ NABÍDKA</button>
|
||||||
${id > 1 ? `<button class="dos-btn" onclick="openRecipe(${id - 1})">[◄] PŘEDCHOZÍ (${id - 1})</button>` : ''}
|
${id > 1 ? `<button class="dos-btn" onclick="openRecipe(${id - 1})">[◄] PŘEDCHOZÍ (${id - 1})</button>` : ''}
|
||||||
${id < KNIHA_RECEPTU.length ? `<button class="dos-btn" onclick="openRecipe(${id + 1})">[►] DALŠÍ (${id + 1})</button>` : ''}
|
${id < KNIHA_RECEPTU.length ? `<button class="dos-btn" onclick="openRecipe(${id + 1})">[►] DALŠÍ (${id + 1})</button>` : ''}
|
||||||
@@ -262,11 +630,14 @@ ${r.postup}
|
|||||||
screenEl.innerHTML = html;
|
screenEl.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. NÁHODNÝ RECEPT
|
||||||
function openRandomRecipe() {
|
function openRandomRecipe() {
|
||||||
|
if (KNIHA_RECEPTU.length === 0) return;
|
||||||
const randomId = Math.floor(Math.random() * KNIHA_RECEPTU.length) + 1;
|
const randomId = Math.floor(Math.random() * KNIHA_RECEPTU.length) + 1;
|
||||||
openRecipe(randomId);
|
openRecipe(randomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. VAŘENÍ ZE SUROVIN VE ŠPAJZU (ALGORITMUS)
|
||||||
function openSuroviny(defaultQuery = "") {
|
function openSuroviny(defaultQuery = "") {
|
||||||
currentView = 'suroviny';
|
currentView = 'suroviny';
|
||||||
playSpeaker(700, 0.06);
|
playSpeaker(700, 0.06);
|
||||||
@@ -287,6 +658,15 @@ function openSuroviny(defaultQuery = "") {
|
|||||||
<button class="dos-btn" onclick="doSearchSuroviny()">HLEDAT [ENTER]</button>
|
<button class="dos-btn" onclick="doSearchSuroviny()">HLEDAT [ENTER]</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; gap:6px; flex-wrap:wrap; margin:10px 0">
|
||||||
|
<span style="color:var(--dos-cyan); align-self:center; font-size:13px">Rychlé koše:</span>
|
||||||
|
<button class="dos-btn" onclick="fillPantry('kuře, máslo, česnek')">Kuře + česnek</button>
|
||||||
|
<button class="dos-btn" onclick="fillPantry('brambory, cibule, vejce')">Brambory + vejce</button>
|
||||||
|
<button class="dos-btn" onclick="fillPantry('tvaroh, mouka, cukr')">Tvaroh + mouka</button>
|
||||||
|
<button class="dos-btn" onclick="fillPantry('jablka, skořice, máslo')">Jablka + skořice</button>
|
||||||
|
<button class="dos-btn" onclick="fillPantry('mleté maso, cibule, česnek')">Mleté maso</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="vysledky-hledani">
|
<div id="vysledky-hledani">
|
||||||
<!-- Dynamic match results -->
|
<!-- Dynamic match results -->
|
||||||
</div>
|
</div>
|
||||||
@@ -300,9 +680,7 @@ function openSuroviny(defaultQuery = "") {
|
|||||||
const input = document.getElementById('suroviny-input');
|
const input = document.getElementById('suroviny-input');
|
||||||
input.focus();
|
input.focus();
|
||||||
input.addEventListener('keydown', (e) => {
|
input.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') doSearchSuroviny();
|
||||||
doSearchSuroviny();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (defaultQuery) {
|
if (defaultQuery) {
|
||||||
@@ -310,6 +688,14 @@ function openSuroviny(defaultQuery = "") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fillPantry(str) {
|
||||||
|
const input = document.getElementById('suroviny-input');
|
||||||
|
if (input) {
|
||||||
|
input.value = str;
|
||||||
|
doSearchSuroviny();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function doSearchSuroviny() {
|
function doSearchSuroviny() {
|
||||||
const query = document.getElementById('suroviny-input').value.trim();
|
const query = document.getElementById('suroviny-input').value.trim();
|
||||||
const resEl = document.getElementById('vysledky-hledani');
|
const resEl = document.getElementById('vysledky-hledani');
|
||||||
@@ -319,29 +705,31 @@ function doSearchSuroviny() {
|
|||||||
}
|
}
|
||||||
playSpeaker(1000, 0.04);
|
playSpeaker(1000, 0.04);
|
||||||
|
|
||||||
const tokens = query.toLowerCase().split(/[\s,]+/).filter(t => t.length > 1).map(norm);
|
const tokens = tokenize(query);
|
||||||
|
const stems = tokens.map(stem);
|
||||||
|
|
||||||
// Score Recipes (both suroviny and postup)
|
// Score Recipes (suroviny, nazev, postup)
|
||||||
const scoredRecipes = KNIHA_RECEPTU.map(r => {
|
const scoredRecipes = KNIHA_RECEPTU.map(r => {
|
||||||
let matches = [];
|
let matches = [];
|
||||||
const fullText = (r.suroviny.join(' ') + ' ' + r.postup).toLowerCase();
|
const normFull = norm(r.nazev + " " + (r.suroviny || []).join(" ") + " " + (r.postup || ""));
|
||||||
const nFull = norm(fullText);
|
|
||||||
|
|
||||||
tokens.forEach(tok => {
|
tokens.forEach((tok, idx) => {
|
||||||
if (nFull.includes(tok)) {
|
const st = stems[idx];
|
||||||
|
if (normFull.includes(tok) || (st.length >= 3 && normFull.includes(st))) {
|
||||||
matches.push(tok);
|
matches.push(tok);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
matches = [...new Set(matches)];
|
matches = [...new Set(matches)];
|
||||||
return { recipe: r, score: matches.length, matchedTokens: matches };
|
return { recipe: r, score: matches.length, matchedTokens: matches };
|
||||||
}).filter(x => x.score > 0).sort((a, b) => b.score - a.score);
|
}).filter(x => x.score > 0).sort((a, b) => b.score - a.score || a.recipe.id - b.recipe.id);
|
||||||
|
|
||||||
// Score Historical Database
|
// Score Historical Database
|
||||||
const scoredHist = HISTORICKA_JIDLA.map(j => {
|
const scoredHist = HISTORICKA_JIDLA.map(j => {
|
||||||
let matches = [];
|
let matches = [];
|
||||||
const nJidlo = norm(j.jidlo);
|
const nJidlo = norm(j.jidlo);
|
||||||
tokens.forEach(tok => {
|
tokens.forEach((tok, idx) => {
|
||||||
if (nJidlo.includes(tok)) {
|
const st = stems[idx];
|
||||||
|
if (nJidlo.includes(tok) || (st.length >= 3 && nJidlo.includes(st))) {
|
||||||
matches.push(tok);
|
matches.push(tok);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -364,7 +752,7 @@ function doSearchSuroviny() {
|
|||||||
|
|
||||||
if (scoredHist.length > 0) {
|
if (scoredHist.length > 0) {
|
||||||
html += `<div style="color:var(--dos-cyan); margin:15px 0 5px 0; font-weight:bold">📜 ZÁZNAMY Z HISTORICKÉHO JÍDELNÍČKU (1998–2021):</div>`;
|
html += `<div style="color:var(--dos-cyan); margin:15px 0 5px 0; font-weight:bold">📜 ZÁZNAMY Z HISTORICKÉHO JÍDELNÍČKU (1998–2021):</div>`;
|
||||||
scoredHist.slice(0, 10).forEach(item => {
|
scoredHist.slice(0, 8).forEach(item => {
|
||||||
html += `<div style="padding:3px 8px; border-left:2px solid var(--dos-cyan); margin:4px 0">
|
html += `<div style="padding:3px 8px; border-left:2px solid var(--dos-cyan); margin:4px 0">
|
||||||
<span style="color:var(--dos-white)">${item.jidlo.jidlo}</span>
|
<span style="color:var(--dos-white)">${item.jidlo.jidlo}</span>
|
||||||
<span style="color:var(--dos-grey)"> [${item.jidlo.datum}, ${item.jidlo.typ}]</span>
|
<span style="color:var(--dos-grey)"> [${item.jidlo.datum}, ${item.jidlo.typ}]</span>
|
||||||
@@ -380,6 +768,7 @@ function doSearchSuroviny() {
|
|||||||
resEl.innerHTML = html;
|
resEl.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 7. DÁVNO NEVAŘENO
|
||||||
function openDavno() {
|
function openDavno() {
|
||||||
currentView = 'davno';
|
currentView = 'davno';
|
||||||
playSpeaker(600, 0.05);
|
playSpeaker(600, 0.05);
|
||||||
@@ -412,12 +801,12 @@ function openDavno() {
|
|||||||
screenEl.innerHTML = html;
|
screenEl.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 8. OBLÍBENÁ JÍDLA
|
||||||
function openOblibena() {
|
function openOblibena() {
|
||||||
currentView = 'oblibena';
|
currentView = 'oblibena';
|
||||||
playSpeaker(850, 0.05);
|
playSpeaker(850, 0.05);
|
||||||
winTitleEl.textContent = "OBLÍBENÁ JÍDLA // ŽELEZNÁ STATISTIKA STOLU";
|
winTitleEl.textContent = "OBLÍBENÁ JÍDLA // ŽELEZNÁ STATISTIKA STOLU";
|
||||||
|
|
||||||
// Count occurrences of keywords in historical records
|
|
||||||
const freq = {};
|
const freq = {};
|
||||||
HISTORICKA_JIDLA.forEach(j => {
|
HISTORICKA_JIDLA.forEach(j => {
|
||||||
const title = j.jidlo.split(',')[0].trim();
|
const title = j.jidlo.split(',')[0].trim();
|
||||||
@@ -456,10 +845,11 @@ function openOblibena() {
|
|||||||
screenEl.innerHTML = html;
|
screenEl.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 9. KRONIKA RODINNÉHO STOLU
|
||||||
function openKronika(page = 0) {
|
function openKronika(page = 0) {
|
||||||
currentView = 'kronika';
|
currentView = 'kronika';
|
||||||
playSpeaker(650, 0.05);
|
playSpeaker(700, 0.04);
|
||||||
winTitleEl.textContent = `KRONIKA RODINNÉHO STOLU // ${HISTORICKA_JIDLA.length} ZÁZNAMŮ BOHNICE (1998–2021)`;
|
winTitleEl.textContent = `KRONIKA STOLU BOHNICE & DEJVICE (${HISTORICKA_JIDLA.length} ZÁZNAMŮ)`;
|
||||||
|
|
||||||
const totalPages = Math.ceil(HISTORICKA_JIDLA.length / 20);
|
const totalPages = Math.ceil(HISTORICKA_JIDLA.length / 20);
|
||||||
const items = HISTORICKA_JIDLA.slice(page * 20, (page + 1) * 20);
|
const items = HISTORICKA_JIDLA.slice(page * 20, (page + 1) * 20);
|
||||||
@@ -468,31 +858,37 @@ function openKronika(page = 0) {
|
|||||||
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
|
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
|
||||||
KRONIKA RODINNÉHO STOLU // BOHNICE & DEJVICE (Strana ${page + 1}/${totalPages})
|
KRONIKA RODINNÉHO STOLU // BOHNICE & DEJVICE (Strana ${page + 1}/${totalPages})
|
||||||
════════════════════════════════════════════════════════════════════════════════</div>
|
════════════════════════════════════════════════════════════════════════════════</div>
|
||||||
|
|
||||||
<div class="dos-box">
|
<div class="dos-box">
|
||||||
Celkem ${HISTORICKA_JIDLA.length} autentických záznamů z databázových souborů JIDLA.TXT a JIDLA-bohnice.TXT.
|
Kompletní záznamy o tom, co se skutečně vařilo a jedlo v rodině Kamily Pešek v letech 1998–2021.
|
||||||
Od srpna 1998, přes dětská léta až po rok 2021.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="max-height:360px; overflow-y:auto; border:1px solid var(--dos-cyan); padding:8px">
|
<div class="dos-menu-list">
|
||||||
`;
|
`;
|
||||||
items.forEach(j => {
|
items.forEach((j) => {
|
||||||
html += `<div style="padding:3px 0; border-bottom:1px dotted rgba(0,170,170,0.3)">
|
html += `<div style="padding:4px 8px; border-bottom:1px solid #002244; display:flex; justify-content:space-between">
|
||||||
<span style="color:var(--dos-yellow); font-weight:bold; width:95px; display:inline-block">${j.datum}</span>
|
<span><strong style="color:var(--dos-yellow)">${j.datum}:</strong> <span style="color:var(--dos-white)">${j.jidlo}</span></span>
|
||||||
<span class="tag-badge">[${j.typ}]</span>
|
<span class="tag-badge">${j.typ}</span>
|
||||||
<span style="color:var(--dos-white); margin-left:6px">${j.jidlo}</span>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
html += `</div>
|
html += `</div>
|
||||||
<div style="margin-top:16px; display:flex; gap:10px">
|
|
||||||
<button class="dos-btn" onclick="renderMainMenu()">[ESC] ZPĚT DO MENU</button>
|
<div style="margin-top:16px; display:flex; justify-content:space-between">
|
||||||
${page > 0 ? `<button class="dos-btn" onclick="openKronika(${page - 1})">[◄] PŘEDCHOZÍ (20)</button>` : ''}
|
<div style="display:flex; gap:10px">
|
||||||
${page < totalPages - 1 ? `<button class="dos-btn" onclick="openKronika(${page + 1})">[►] DALŠÍ (20)</button>` : ''}
|
<button class="dos-btn" onclick="renderMainMenu()">[ESC] MENU</button>
|
||||||
|
${page > 0 ? `<button class="dos-btn" onclick="openKronika(${page - 1})">[◄] PŘEDCHOZÍ</button>` : ''}
|
||||||
|
${page < totalPages - 1 ? `<button class="dos-btn" onclick="openKronika(${page + 1})">[►] DALŠÍ</button>` : ''}
|
||||||
|
</div>
|
||||||
|
<div style="color:var(--dos-yellow); font-size:13px">
|
||||||
|
Záznam ${page * 20 + 1}–${Math.min((page + 1) * 20, HISTORICKA_JIDLA.length)} z ${HISTORICKA_JIDLA.length}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
screenEl.innerHTML = html;
|
screenEl.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 10. ALTÁN / NÁPOVĚDA
|
||||||
function openAltan() {
|
function openAltan() {
|
||||||
currentView = 'altan';
|
currentView = 'altan';
|
||||||
playSpeaker(750, 0.05);
|
playSpeaker(750, 0.05);
|
||||||
@@ -529,9 +925,12 @@ function openAltan() {
|
|||||||
screenEl.innerHTML = html;
|
screenEl.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global Keyboard Handler (Turbo Pascal Hotkeys)
|
// =========================================================================
|
||||||
|
// GLOBAL KEYBOARD & TOUCH NAVIGATION
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
// If in input, only hijack ESC
|
// If typing in input, only ESC blurs
|
||||||
if (document.activeElement && document.activeElement.tagName === 'INPUT') {
|
if (document.activeElement && document.activeElement.tagName === 'INPUT') {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
@@ -543,6 +942,11 @@ window.addEventListener('keydown', (e) => {
|
|||||||
playSpeaker(800, 0.03);
|
playSpeaker(800, 0.03);
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
|
case '/':
|
||||||
|
case 'h': case 'H':
|
||||||
|
e.preventDefault();
|
||||||
|
openSearch();
|
||||||
|
break;
|
||||||
case 'r': case 'R': case 'F2':
|
case 'r': case 'R': case 'F2':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openReceptyList('Vše');
|
openReceptyList('Vše');
|
||||||
@@ -572,7 +976,7 @@ window.addEventListener('keydown', (e) => {
|
|||||||
case 'Escape':
|
case 'Escape':
|
||||||
renderMainMenu();
|
renderMainMenu();
|
||||||
break;
|
break;
|
||||||
case '1': case '2': case '3': case '4': case '5': case '6': case '7':
|
case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8':
|
||||||
if (currentView === 'menu') {
|
if (currentView === 'menu') {
|
||||||
const idx = parseInt(e.key);
|
const idx = parseInt(e.key);
|
||||||
if (KATEGORIE[idx]) openReceptyList(KATEGORIE[idx]);
|
if (KATEGORIE[idx]) openReceptyList(KATEGORIE[idx]);
|
||||||
@@ -586,6 +990,7 @@ document.querySelectorAll('[data-act]').forEach(el => {
|
|||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
const act = el.getAttribute('data-act');
|
const act = el.getAttribute('data-act');
|
||||||
if (act === 'menu') renderMainMenu();
|
if (act === 'menu') renderMainMenu();
|
||||||
|
else if (act === 'hledat') openSearch();
|
||||||
else if (act === 'recepty') openReceptyList('Vše');
|
else if (act === 'recepty') openReceptyList('Vše');
|
||||||
else if (act === 'suroviny') openSuroviny();
|
else if (act === 'suroviny') openSuroviny();
|
||||||
else if (act === 'davno') openDavno();
|
else if (act === 'davno') openDavno();
|
||||||
|
|||||||
+7
-4
@@ -11,6 +11,7 @@
|
|||||||
<!-- Turbo Pascal Top Menu Bar -->
|
<!-- Turbo Pascal Top Menu Bar -->
|
||||||
<header id="menu-bar">
|
<header id="menu-bar">
|
||||||
<span class="menu-item active" data-act="menu"><span class="hotkey">S</span>oubor</span>
|
<span class="menu-item active" data-act="menu"><span class="hotkey">S</span>oubor</span>
|
||||||
|
<span class="menu-item" data-act="hledat"><span class="hotkey">H</span>ledat [/]</span>
|
||||||
<span class="menu-item" data-act="recepty"><span class="hotkey">R</span>eceptář (331)</span>
|
<span class="menu-item" data-act="recepty"><span class="hotkey">R</span>eceptář (331)</span>
|
||||||
<span class="menu-item" data-act="suroviny"><span class="hotkey">Š</span>pajz</span>
|
<span class="menu-item" data-act="suroviny"><span class="hotkey">Š</span>pajz</span>
|
||||||
<span class="menu-item" data-act="davno"><span class="hotkey">D</span>ávno</span>
|
<span class="menu-item" data-act="davno"><span class="hotkey">D</span>ávno</span>
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
|
|
||||||
<!-- Mobile Touch Keyboard Bar -->
|
<!-- Mobile Touch Keyboard Bar -->
|
||||||
<nav id="touch-controls">
|
<nav id="touch-controls">
|
||||||
|
<button class="t-btn highlight" data-act="hledat">HLEDAT [/]</button>
|
||||||
<button class="t-btn highlight" data-act="recepty">RECEPTÁŘ (331)</button>
|
<button class="t-btn highlight" data-act="recepty">RECEPTÁŘ (331)</button>
|
||||||
<button class="t-btn highlight" data-act="suroviny">ŠPAJZ [S]</button>
|
<button class="t-btn highlight" data-act="suroviny">ŠPAJZ [S]</button>
|
||||||
<button class="t-btn" data-act="davno">DÁVNO [D]</button>
|
<button class="t-btn" data-act="davno">DÁVNO [D]</button>
|
||||||
@@ -49,11 +51,12 @@
|
|||||||
<!-- Turbo Pascal Bottom Status Bar -->
|
<!-- Turbo Pascal Bottom Status Bar -->
|
||||||
<footer id="status-bar">
|
<footer id="status-bar">
|
||||||
<span class="f-key" data-act="altan"><span class="f-num">F1</span> Nápověda</span>
|
<span class="f-key" data-act="altan"><span class="f-num">F1</span> Nápověda</span>
|
||||||
|
<span class="f-key" data-act="hledat"><span class="f-num">/</span> Hledat</span>
|
||||||
<span class="f-key" data-act="recepty"><span class="f-num">F2</span> Receptář</span>
|
<span class="f-key" data-act="recepty"><span class="f-num">F2</span> Receptář</span>
|
||||||
<span class="f-key" data-act="suroviny"><span class="f-num">F3</span> Vařit ze špajzu</span>
|
<span class="f-key" data-act="suroviny"><span class="f-num">F3</span> Špajz</span>
|
||||||
<span class="f-key" data-act="davno"><span class="f-num">F4</span> Dávno nevařeno</span>
|
<span class="f-key" data-act="davno"><span class="f-num">F4</span> Dávno</span>
|
||||||
<span class="f-key" data-act="kronika"><span class="f-num">F5</span> Kronika 568</span>
|
<span class="f-key" data-act="kronika"><span class="f-num">F5</span> Kronika</span>
|
||||||
<span class="f-key" data-act="menu"><span class="f-num">ESC</span> Hlavní menu</span>
|
<span class="f-key" data-act="menu"><span class="f-num">ESC</span> Menu</span>
|
||||||
<span class="f-key sound-toggle" id="sound-btn">🔊 ZVUK: ON</span>
|
<span class="f-key sound-toggle" id="sound-btn">🔊 ZVUK: ON</span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1747
-2552
File diff suppressed because it is too large
Load Diff
@@ -238,6 +238,24 @@ html, body {
|
|||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Highlight & Search Results */
|
||||||
|
.match-highlight {
|
||||||
|
background-color: var(--dos-yellow);
|
||||||
|
color: var(--dos-black);
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 1px 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-snippet {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--dos-grey);
|
||||||
|
margin-top: 3px;
|
||||||
|
padding-left: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
border-left: 2px solid var(--dos-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
/* Touch controls on mobile */
|
/* Touch controls on mobile */
|
||||||
#touch-controls {
|
#touch-controls {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user