Files

1012 lines
44 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =========================================================================
KUCHAŘKA KAMILY PEŠEK // PASCAL 7.0 WEB ENGINE (331 RECEPTŮ & 568 JÍDEL)
Kulinářská paměť Kamily Pešek & Software-salon ALTÁN (Helena Heroutová)
========================================================================= */
// Audio engine (Retro PC Speaker via Web Audio API)
let audioCtx = null;
let soundEnabled = true;
function playSpeaker(freq = 800, duration = 0.04, type = 'square') {
if (!soundEnabled) return;
try {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
} catch (e) {}
}
// Data references loaded from data.js
const KNIHA_RECEPTU = window.KNIHA_RECEPTU || [];
const HISTORICKA_JIDLA = window.HISTORICKA_JIDLA || [];
// App State
let currentView = 'menu'; // 'menu', 'recept', 'recepty_list', 'hledat', 'suroviny', 'davno', 'oblibena', 'kronika', 'altan'
let currentRecipeId = 1;
let currentCategory = 'Vše';
let listPage = 0;
const PAGE_SIZE = 12;
const screenEl = document.getElementById('screen-content');
const winTitleEl = document.getElementById('win-title');
const timeEl = document.getElementById('time-display');
// Clock
setInterval(() => {
const d = new Date();
timeEl.textContent = d.toTimeString().split(' ')[0];
}, 1000);
// =========================================================================
// CZECH TEXT NORMALIZATION & FULLTEXT SEARCH ENGINE
// =========================================================================
// Normalizace: malá písmena, odstranění diakritiky
function norm(str) {
return (str || "").toLowerCase()
.normalize("NFD").replace(/[\u0300-\u036f]/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
const KATEGORIE = [
'Vše',
'Polévky',
'Masitá jídla',
'Vánoční cukroví',
'Moučníky a sladké',
'Přílohy a bezmasá',
'Omáčky',
'Saláty a zelenina',
'Ostatní'
];
// =========================================================================
// VIEWS & CONTROLLERS
// =========================================================================
// 1. HLAVNÍ NABÍDKA
function renderMainMenu() {
currentView = 'menu';
winTitleEl.textContent = "KUCHAŘKA KAMILY PEŠEK // HLAVNÍ NABÍDKA PASCAL 7.0";
let html = `
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
KUCHAŘKA KAMILY PEŠEK // AUTENTICKÝ SEŠIT RECEPTŮ (1998–2026)
Software-salon ALTÁN (Ing. Helena Heroutová, Dejvice) & Konzdat
════════════════════════════════════════════════════════════════════════════════</div>
<div class="dos-box">
<span style="color:var(--dos-yellow)">Vítejte v rodinném gastronomickém archivu babičky <strong>Kamily Pešek</strong>.</span>
Tento systém obsahuje <strong>${KNIHA_RECEPTU.length} přepsaných receptů</strong> z jejího ručně psaného
sešitu (strany 1–43) a <strong>${HISTORICKA_JIDLA.length} záznamů rodinného jídelníčku</strong> z let 1998–2021.
</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-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')">
<span class="hotkey">[R]</span>
<span style="color:var(--dos-white); font-weight:bold">Procházet receptář Kamily Pešek podle kategorií</span>
</div>
<div class="dos-menu-item" onclick="openSuroviny()">
<span class="hotkey">[S]</span>
<span style="color:var(--dos-green); font-weight:bold">Vařit podle surovin ve špajzu a lednici (Algoritmus KUCHARKA.EXE)</span>
</div>
<div class="dos-menu-item" onclick="openRandomRecipe()">
<span class="hotkey">[N]</span>
<span>Náhodný recept pro dnešní den (Inspirace ze sešitu)</span>
</div>
<div class="dos-menu-item" onclick="openDavno()">
<span class="hotkey">[D]</span>
<span>Doporučení: Dávno nevařeno (Prevence stereotypu - DoporucD)</span>
</div>
<div class="dos-menu-item" onclick="openOblibena()">
<span class="hotkey">[O]</span>
<span>Železná statistika oblíbených jídel rodinného stolu (DoporucO)</span>
</div>
<div class="dos-menu-item" onclick="openKronika()">
<span class="hotkey">[K]</span>
<span>Kronika rodinného stolu (${HISTORICKA_JIDLA.length} záznamů Bohnice a Dejvice)</span>
</div>
<div class="dos-menu-item" onclick="openAltan()">
<span class="hotkey">[A]</span>
<span>O programu & Software-salon ALTÁN (Historie Dejvic & Bohnic)</span>
</div>
</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:8px 0">
`;
KATEGORIE.slice(1).forEach((cat, idx) => {
const count = KNIHA_RECEPTU.filter(r => r.kategorie === cat).length;
html += `<button class="dos-btn" onclick="openReceptyList('${cat}')">[${idx + 1}] ${cat} (${count})</button>`;
});
html += `</div>
<div style="margin-top:14px; color:var(--dos-grey)">
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>
`;
screenEl.innerHTML = html;
const quickInput = document.getElementById('menu-quick-search');
if (quickInput) {
quickInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') triggerMenuSearch();
});
}
}
function triggerMenuSearch() {
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';
currentCategory = category;
listPage = page;
playSpeaker(700, 0.04);
const baseList = category === 'Vše' ? KNIHA_RECEPTU : KNIHA_RECEPTU.filter(r => r.kategorie === category);
const totalPages = Math.ceil(baseList.length / PAGE_SIZE);
const pageItems = baseList.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
winTitleEl.textContent = `RECEPTÁŘ KAMILY PEŠEK // ${category.toUpperCase()} (${baseList.length} RECEPTŮ)`;
let html = `
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
RECEPTÁŘ KAMILY PEŠEK // KATEGORIE: ${category.toUpperCase()} (Strana ${page + 1}/${totalPages || 1})
════════════════════════════════════════════════════════════════════════════════</div>
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:10px">
`;
KATEGORIE.forEach(cat => {
const activeClass = cat === category ? 'style="background-color:var(--dos-yellow); color:var(--dos-black)"' : '';
html += `<button class="dos-btn" ${activeClass} onclick="openReceptyList('${cat}', 0)">${cat}</button>`;
});
html += `</div>
<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, brambory, kmínová)..." value="${initialQuery}">
<button class="dos-btn" onclick="clearRecipeSearch('${category}')">ZRUŠIT FILTR</button>
</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">
`;
pageItems.forEach((r) => {
html += `<div class="dos-menu-item" onclick="openRecipe(${r.id})">
<span class="hotkey">[${r.id}]</span>
<span style="color:var(--dos-white)">${r.nazev}</span>
<span class="tag-badge" style="margin-left:auto">Str. ${r.strana} | ${r.kategorie}</span>
</div>`;
});
html += `</div>
<div id="recipe-pagination" style="margin-top:16px; display:flex; justify-content:space-between; align-items:center">
<div style="display:flex; gap:10px">
<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 < totalPages - 1 ? `<button class="dos-btn" onclick="openReceptyList('${category}', ${page + 1})">[►] DALŠÍ STRANA</button>` : ''}
</div>
<div style="color:var(--dos-yellow); font-size:13px">
Celkem ${baseList.length} receptů v této kategorii
</div>
</div>
`;
screenEl.innerHTML = html;
// Search input live filtering
const searchInput = document.getElementById('recipe-search-input');
const container = document.getElementById('recipe-items-container');
const pagination = document.getElementById('recipe-pagination');
const statusBar = document.getElementById('search-status-bar');
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 style="color:var(--dos-white)">${r.nazev}</span>
<span class="tag-badge" style="margin-left:auto">Str. ${r.strana} | ${r.kategorie}</span>
</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) {
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;
};
searchInput.addEventListener('input', handleSearch);
if (initialQuery) {
handleSearch();
}
}
function clearRecipeSearch(category) {
openReceptyList(category, 0);
}
// 4. DETAIL RECEPTU
function openRecipe(id) {
const r = KNIHA_RECEPTU.find(x => x.id === id);
if (!r) return;
currentView = 'recept';
currentRecipeId = id;
playSpeaker(900, 0.05);
winTitleEl.textContent = `RECEPT [${r.id}/${KNIHA_RECEPTU.length}]: ${r.nazev.toUpperCase()}`;
let html = `
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
RECEPT Č. ${r.id}: ${r.nazev}
Původní sešit Kamily Pešek: Strana ${r.strana} | Kategorie: ${r.kategorie}
════════════════════════════════════════════════════════════════════════════════</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">
`;
if (r.suroviny && r.suroviny.length > 0) {
r.suroviny.forEach(s => {
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>
<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)">
${r.postup || "Tradiční rodinný postup podle zápisu Kamily Pešek."}
</div>
<div class="secret-badge">
<strong>★ TAJEMSTVÍ KAMILY PEŠEK:</strong><br>
<em>"${r.tajemstvi}"</em>
</div>
<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="openSearch()">[/] HLEDAT JINÝ</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 < KNIHA_RECEPTU.length ? `<button class="dos-btn" onclick="openRecipe(${id + 1})">[►] DALŠÍ (${id + 1})</button>` : ''}
</div>
`;
screenEl.innerHTML = html;
}
// 5. NÁHODNÝ RECEPT
function openRandomRecipe() {
if (KNIHA_RECEPTU.length === 0) return;
const randomId = Math.floor(Math.random() * KNIHA_RECEPTU.length) + 1;
openRecipe(randomId);
}
// 6. VAŘENÍ ZE SUROVIN VE ŠPAJZU (ALGORITMUS)
function openSuroviny(defaultQuery = "") {
currentView = 'suroviny';
playSpeaker(700, 0.06);
winTitleEl.textContent = "ALGORITMUS: VAŘENÍ PODLE SUROVIN VE ŠPAJZU A LEDNICI";
let html = `
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
VAŘENÍ PODLE SUROVIN // ROZHODOVACÍ SYSTÉM PRO HOSPODYNI (KUCHARKA.EXE)
════════════════════════════════════════════════════════════════════════════════</div>
<div class="dos-box">
Zadejte suroviny, které máte právě doma ve špajzu a lednici (oddělené čárkou či mezerou).
Algoritmus prohledá <strong>všech ${KNIHA_RECEPTU.length} receptů Kamily Pešek</strong> i <strong>${HISTORICKA_JIDLA.length} jídel z kroniky</strong>.
</div>
<div class="dos-input-group">
<span style="color:var(--dos-yellow); font-weight:bold">MÁM DOMA:</span>
<input type="text" id="suroviny-input" class="dos-input" placeholder="např. brambory, maso, smetana, vejce, kuře, jablka..." value="${defaultQuery}">
<button class="dos-btn" onclick="doSearchSuroviny()">HLEDAT [ENTER]</button>
</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">
<!-- Dynamic match results -->
</div>
<div style="margin-top:20px">
<button class="dos-btn" onclick="renderMainMenu()">[ESC] ZPĚT DO HLAVNÍHO MENU</button>
</div>
`;
screenEl.innerHTML = html;
const input = document.getElementById('suroviny-input');
input.focus();
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') doSearchSuroviny();
});
if (defaultQuery) {
doSearchSuroviny();
}
}
function fillPantry(str) {
const input = document.getElementById('suroviny-input');
if (input) {
input.value = str;
doSearchSuroviny();
}
}
function doSearchSuroviny() {
const query = document.getElementById('suroviny-input').value.trim();
const resEl = document.getElementById('vysledky-hledani');
if (!query) {
resEl.innerHTML = `<div style="color:var(--dos-red)">Zadejte alespoň jednu surovinu!</div>`;
return;
}
playSpeaker(1000, 0.04);
const tokens = tokenize(query);
const stems = tokens.map(stem);
// Score Recipes (suroviny, nazev, postup)
const scoredRecipes = KNIHA_RECEPTU.map(r => {
let matches = [];
const normFull = norm(r.nazev + " " + (r.suroviny || []).join(" ") + " " + (r.postup || ""));
tokens.forEach((tok, idx) => {
const st = stems[idx];
if (normFull.includes(tok) || (st.length >= 3 && normFull.includes(st))) {
matches.push(tok);
}
});
matches = [...new Set(matches)];
return { recipe: r, score: matches.length, matchedTokens: matches };
}).filter(x => x.score > 0).sort((a, b) => b.score - a.score || a.recipe.id - b.recipe.id);
// Score Historical Database
const scoredHist = HISTORICKA_JIDLA.map(j => {
let matches = [];
const nJidlo = norm(j.jidlo);
tokens.forEach((tok, idx) => {
const st = stems[idx];
if (nJidlo.includes(tok) || (st.length >= 3 && nJidlo.includes(st))) {
matches.push(tok);
}
});
matches = [...new Set(matches)];
return { jidlo: j, score: matches.length, matchedTokens: matches };
}).filter(x => x.score > 0).sort((a, b) => b.score - a.score);
let html = `<div class="dos-header" style="color:var(--dos-green)">VÝSLEDEK SHODY PRO: "${query}" (Nalezeno ${scoredRecipes.length} receptů Kamily Pešek)</div>`;
if (scoredRecipes.length > 0) {
html += `<div style="color:var(--dos-yellow); margin:10px 0; font-weight:bold">★ NALEZENÉ RECEPTY ZE SEŠITU KAMILY PEŠEK:</div>`;
scoredRecipes.slice(0, 15).forEach(item => {
html += `<div class="dos-menu-item" onclick="openRecipe(${item.recipe.id})">
<span class="hotkey">[${item.recipe.id}]</span>
<span style="color:var(--dos-white)">${item.recipe.nazev}</span>
<span style="color:var(--dos-green); margin-left:auto">Shoda: ${item.score} surovin (${item.matchedTokens.join(', ')})</span>
</div>`;
});
}
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>`;
scoredHist.slice(0, 8).forEach(item => {
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-grey)"> [${item.jidlo.datum}, ${item.jidlo.typ}]</span>
<span style="color:var(--dos-yellow); float:right">shoda: ${item.matchedTokens.join(', ')}</span>
</div>`;
});
}
if (scoredRecipes.length === 0 && scoredHist.length === 0) {
html += `<div style="color:var(--dos-red); padding:10px">Pro tyto suroviny nebyla v databázi nalezena přímá shoda. Zkuste zadat základní ingredience jako: brambory, maso, vejce, smetana, kuře.</div>`;
}
resEl.innerHTML = html;
}
// 7. DÁVNO NEVAŘENO
function openDavno() {
currentView = 'davno';
playSpeaker(600, 0.05);
winTitleEl.textContent = "DOPORUČENÍ: DÁVNO NEVAŘENO // PREVENCE STEREOTYPU";
let html = `
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
DOPORUČENÍ: DÁVNO NEVAŘENO // HISTORICKÝ CYKLUS RODINY (DoporucD)
════════════════════════════════════════════════════════════════════════════════</div>
<div class="dos-box">
Tento modul dekompilovaný z původního Delphi programu prochází archiv datumu
a vyhledává pokrmy, které se na stole neobjevily nejdéle, aby se jídelníček netočil.
</div>
<div class="dos-header" style="color:var(--dos-yellow)">DOPORUČENÁ JÍDLA S NEJDELŠÍM ČASOVÝM ODSTUPEM:</div>
<div class="dos-menu-list">
`;
HISTORICKA_JIDLA.slice(0, 12).forEach((j, idx) => {
html += `<div style="padding:5px 10px; border-bottom:1px dashed var(--dos-cyan); display:flex; justify-content:space-between">
<span><strong style="color:var(--dos-cyan)">${idx + 1}.</strong> <span style="color:var(--dos-white)">${j.jidlo}</span></span>
<span><span style="color:var(--dos-yellow)">Naposledy: ${j.datum}</span> | ${j.typ}</span>
</div>`;
});
html += `</div>
<div style="margin-top:20px">
<button class="dos-btn" onclick="renderMainMenu()">[ESC] ZPĚT DO MENU</button>
</div>
`;
screenEl.innerHTML = html;
}
// 8. OBLÍBENÁ JÍDLA
function openOblibena() {
currentView = 'oblibena';
playSpeaker(850, 0.05);
winTitleEl.textContent = "OBLÍBENÁ JÍDLA // ŽELEZNÁ STATISTIKA STOLU";
const freq = {};
HISTORICKA_JIDLA.forEach(j => {
const title = j.jidlo.split(',')[0].trim();
freq[title] = (freq[title] || 0) + 1;
});
const top = Object.entries(freq).sort((a, b) => b[1] - a[1]).slice(0, 12);
let html = `
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
ŽELEZNÁ STATISTIKA OBLÍBENÝCH JÍDEL RODINNÉHO STOLU (DoporucO)
════════════════════════════════════════════════════════════════════════════════</div>
<div class="dos-box">
Frekvenční žebříček z ${HISTORICKA_JIDLA.length} záznamů rodinného jídelníčku z let 1998–2021:
</div>
<div class="dos-menu-list">
`;
const maxVal = top[0] ? top[0][1] : 1;
top.forEach(([name, count], idx) => {
const barWidth = Math.max(2, Math.round((count / maxVal) * 24));
const bar = "█".repeat(barWidth);
html += `<div style="padding:4px 8px; font-family:monospace">
<span style="color:var(--dos-yellow); width:24px; display:inline-block">${idx + 1}.</span>
<span style="color:var(--dos-white); display:inline-block; width:300px">${name}</span>
<span style="color:var(--dos-cyan)">${bar}</span>
<span style="color:var(--dos-yellow); margin-left:10px">${count}×</span>
</div>`;
});
html += `</div>
<div style="margin-top:20px">
<button class="dos-btn" onclick="renderMainMenu()">[ESC] ZPĚT DO MENU</button>
</div>
`;
screenEl.innerHTML = html;
}
// 9. KRONIKA RODINNÉHO STOLU
function openKronika(page = 0) {
currentView = 'kronika';
playSpeaker(700, 0.04);
winTitleEl.textContent = `KRONIKA STOLU BOHNICE & DEJVICE (${HISTORICKA_JIDLA.length} ZÁZNAMŮ)`;
const totalPages = Math.ceil(HISTORICKA_JIDLA.length / 20);
const items = HISTORICKA_JIDLA.slice(page * 20, (page + 1) * 20);
let html = `
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
KRONIKA RODINNÉHO STOLU // BOHNICE & DEJVICE (Strana ${page + 1}/${totalPages})
════════════════════════════════════════════════════════════════════════════════</div>
<div class="dos-box">
Kompletní záznamy o tom, co se skutečně vařilo a jedlo v rodině Kamily Pešek v letech 1998–2021.
</div>
<div class="dos-menu-list">
`;
items.forEach((j) => {
html += `<div style="padding:4px 8px; border-bottom:1px solid #002244; display:flex; justify-content:space-between">
<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>
</div>`;
});
html += `</div>
<div style="margin-top:16px; display:flex; justify-content:space-between">
<div style="display:flex; gap:10px">
<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>
`;
screenEl.innerHTML = html;
}
// 10. ALTÁN / NÁPOVĚDA
function openAltan() {
currentView = 'altan';
playSpeaker(750, 0.05);
winTitleEl.textContent = "SOFTWARE-SALON ALTÁN // DEJVICE & BOHNICE";
let html = `
<div class="dos-header">════════════════════════════════════════════════════════════════════════════════
SOFTWARE-SALON ALTÁN // VÁCLAVKOVA 2, PRAHA 6 (DEJVICE)
════════════════════════════════════════════════════════════════════════════════</div>
<div class="dos-box" style="line-height:1.6">
<span style="color:var(--dos-yellow); font-weight:bold">PŮVODNÍ PROGRAMÁTORSKÁ STOPA Z JEDNOTKY TURBO PASCALU (POMUCKY.BAK):</span><br><br>
<code>
const rec : array[1..3] of string = (<br>
&nbsp;&nbsp;'S PŘIPOMÍNKAMI A DOTAZY SE OBRACEJTE NA ADRESU:',<br>
&nbsp;&nbsp;' Ing. B. Mikulecký, CSc., Ing. H. Heroutová',<br>
&nbsp;&nbsp;' Software-salon ALTÁN ',<br>
&nbsp;&nbsp;' Václavkova 2, 160 00 Praha 6, tel. 02/311 13 25');
</code>
<br><br>
Ing. Helena Heroutová patřila k průkopnické generaci českých softwarových inženýrek,
které v 80. a 90. letech tvořily základní procedurální knihovny pro Turbo Pascal
a DOS v dejvickém ateliéru na Václavkově ulici.<br><br>
Když se na přelomu tisíciletí objevilo vývojové prostředí Borland Delphi, koncept
rodinné kuchařky se přetavil do plnohodnotné 32bitové aplikace KUCHARKA.EXE.<br><br>
Tato webová aplikace věrně propojuje kulinářské dědictví babičky <strong>Kamily Pešek</strong>
(kompletní rukopisný sešit čítající <strong>${KNIHA_RECEPTU.length} receptů</strong>)
s procedurální logikou Turbo Pascalu pro budoucí generace.
</div>
<div style="margin-top:20px">
<button class="dos-btn" onclick="renderMainMenu()">[ESC] ZPĚT DO MENU</button>
</div>
`;
screenEl.innerHTML = html;
}
// =========================================================================
// GLOBAL KEYBOARD & TOUCH NAVIGATION
// =========================================================================
window.addEventListener('keydown', (e) => {
// If typing in input, only ESC blurs
if (document.activeElement && document.activeElement.tagName === 'INPUT') {
if (e.key === 'Escape') {
document.activeElement.blur();
renderMainMenu();
}
return;
}
playSpeaker(800, 0.03);
switch (e.key) {
case '/':
case 'h': case 'H':
e.preventDefault();
openSearch();
break;
case 'r': case 'R': case 'F2':
e.preventDefault();
openReceptyList('Vše');
break;
case 's': case 'S': case 'F3':
e.preventDefault();
openSuroviny();
break;
case 'n': case 'N':
openRandomRecipe();
break;
case 'd': case 'D': case 'F4':
e.preventDefault();
openDavno();
break;
case 'o': case 'O':
openOblibena();
break;
case 'k': case 'K': case 'F5':
e.preventDefault();
openKronika();
break;
case 'a': case 'A': case 'F1':
e.preventDefault();
openAltan();
break;
case 'Escape':
renderMainMenu();
break;
case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8':
if (currentView === 'menu') {
const idx = parseInt(e.key);
if (KATEGORIE[idx]) openReceptyList(KATEGORIE[idx]);
}
break;
}
});
// Touch / Navigation controls handler
document.querySelectorAll('[data-act]').forEach(el => {
el.addEventListener('click', () => {
const act = el.getAttribute('data-act');
if (act === 'menu') renderMainMenu();
else if (act === 'hledat') openSearch();
else if (act === 'recepty') openReceptyList('Vše');
else if (act === 'suroviny') openSuroviny();
else if (act === 'davno') openDavno();
else if (act === 'oblibena') openOblibena();
else if (act === 'kronika') openKronika();
else if (act === 'altan') openAltan();
});
});
// Sound toggle
document.getElementById('sound-btn').addEventListener('click', () => {
soundEnabled = !soundEnabled;
document.getElementById('sound-btn').textContent = soundEnabled ? '🔊 ZVUK: ON' : '🔈 ZVUK: OFF';
if (soundEnabled) playSpeaker(1200, 0.08);
});
// Initial boot
renderMainMenu();