Cache crossed-out ingredients & steps in recipe detail view with reset buttons

This commit is contained in:
2026-10-25 17:58:04 +01:00
parent a4ccf5993e
commit e11762e264
2 changed files with 191 additions and 3 deletions
+46
View File
@@ -633,11 +633,41 @@
margin: 0 0 12px 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
border-bottom: 1.5px solid var(--altrosa);
padding-bottom: 4px;
}
.cb-reset-checks-btn {
display: inline-flex;
align-items: center;
gap: 4px;
background: rgba(30, 77, 59, 0.05);
border: 1px dashed rgba(30, 77, 59, 0.35);
color: var(--tannengruen);
font-family: var(--font-hand);
font-size: 0.92rem;
font-weight: 600;
padding: 2px 9px;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
line-height: 1;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.cb-reset-checks-btn:hover {
background: rgba(30, 77, 59, 0.12);
border-color: var(--tannengruen);
border-style: solid;
}
.cb-reset-checks-btn:active {
transform: scale(0.96);
}
.ingredients-list {
list-style: none;
padding: 0;
@@ -2572,6 +2602,21 @@ body.cook-mode-on .cookmode-text-box .cookmode-status-text {
background: rgba(34, 34, 34, 0.04);
}
.matrix-row {
cursor: pointer;
transition: background 0.15s ease;
}
.matrix-row:hover {
background: rgba(30, 77, 59, 0.05);
}
.matrix-row.is-checked td {
text-decoration: line-through;
opacity: 0.45;
color: #777;
}
/* Interactive step completion in cook mode */
.step-card {
cursor: pointer;
@@ -2740,6 +2785,7 @@ body.cook-mode-on .cookmode-text-box .cookmode-status-text {
.cookmode-toggle-btn,
.cb-nav-cookmode-btn,
.cb-mobile-cookmode-item,
.cb-reset-checks-btn,
.recipe-cookmode-banner,
.step-done-check,
.no-print {
+145 -3
View File
@@ -60,6 +60,9 @@
</div>
<span id="cookmode-mobile-badge" class="cb-badge-pill">Aus</span>
</button>
<button type="button" id="reset-all-menu-btn" onclick="resetAllChecks()" class="cb-mobile-item no-print" style="display: none;">
<span>↺</span> <span>Abgehaktes zurücksetzen</span>
</button>
<hr class="cb-mobile-divider">
{% if not is_shared %}
<button type="button" onclick="openShareModal()" class="cb-mobile-item">
@@ -189,7 +192,12 @@
<!-- Zutatenleiste ohne Portionsangabe (da schon in Kopfzeile) -->
<div class="ingredients-box {% if recipe.has_portion_table %}has-portion-matrix{% endif %}">
<h4 class="section-hand-title">Zutaten</h4>
<h4 class="section-hand-title">
<span>Zutaten</span>
<button type="button" id="reset-ingredients-btn" class="cb-reset-checks-btn no-print" onclick="resetIngredientChecks()" title="Abgehakte Zutaten zurücksetzen" style="display: none;">
<span>↺</span> <span>Zurücksetzen</span>
</button>
</h4>
{% if recipe.has_portion_table and recipe.portion_matrix %}
<!-- Tabelle für 2/4/6/8 Portionen (z. B. Waffeln / Pancakes) -->
@@ -210,7 +218,7 @@
<th colspan="5">{{ row.text }}</th>
</tr>
{% else %}
<tr class="matrix-row">
<tr class="matrix-row" onclick="toggleIngredientCheck(this)" title="Tippen zum Abhaken">
<td class="matrix-td-name">{{ row.name }}</td>
<td class="matrix-td-amt">{{ row.amounts[2] }}</td>
<td class="matrix-td-amt highlight">{{ row.amounts[4] }}</td>
@@ -242,7 +250,12 @@
<!-- Rechte Spalte: Zubereitung + Notizen + Quelle -->
<section class="recipe-main">
<div>
<h4 class="section-hand-title">Zubereitung</h4>
<h4 class="section-hand-title">
<span>Zubereitung</span>
<button type="button" id="reset-steps-btn" class="cb-reset-checks-btn no-print" onclick="resetStepChecks()" title="Erledigte Schritte zurücksetzen" style="display: none;">
<span>↺</span> <span>Zurücksetzen</span>
</button>
</h4>
<div class="instructions-steps">
{% for step in recipe.instructions.split('\n') %}
{% if step.strip() %}
@@ -717,6 +730,7 @@
// Gespeicherten Zustand beim Laden prüfen
window.addEventListener('DOMContentLoaded', () => {
loadChecksFromStorage();
try {
const saved = localStorage.getItem('wtc_cookmode_active') === 'true';
if (saved) {
@@ -735,14 +749,142 @@
} catch (e) {}
});
// ==========================================
// INTERAKTIVES ABHAKEN (ZUTATEN & SCHRITTE) MIT ZWISCHENSPEICHER
// ==========================================
const RECIPE_CHECK_STORAGE_KEY = 'wtc_recipe_checks_{{ recipe.id }}';
function getCheckableIngredients() {
return Array.from(document.querySelectorAll('.ingredients-list .ingredient-item, .portion-matrix-table .matrix-row'));
}
function getCheckableSteps() {
return Array.from(document.querySelectorAll('.instructions-steps .step-card'));
}
function updateResetButtonsVisibility(hasIngChecks, hasStepChecks) {
const ingResetBtn = document.getElementById('reset-ingredients-btn');
if (ingResetBtn) ingResetBtn.style.display = hasIngChecks ? 'inline-flex' : 'none';
const stepResetBtn = document.getElementById('reset-steps-btn');
if (stepResetBtn) stepResetBtn.style.display = hasStepChecks ? 'inline-flex' : 'none';
const menuResetBtn = document.getElementById('reset-all-menu-btn');
if (menuResetBtn) menuResetBtn.style.display = (hasIngChecks || hasStepChecks) ? 'flex' : 'none';
}
function saveChecksToStorage() {
try {
const ingEls = getCheckableIngredients();
const stepEls = getCheckableSteps();
const checkedIngredients = [];
ingEls.forEach((el, idx) => {
if (el.classList.contains('is-checked')) checkedIngredients.push(idx);
});
const checkedSteps = [];
stepEls.forEach((el, idx) => {
if (el.classList.contains('is-done')) checkedSteps.push(idx);
});
const data = {
recipeId: {{ recipe.id }},
ingredients: checkedIngredients,
steps: checkedSteps,
updatedAt: Date.now()
};
const json = JSON.stringify(data);
sessionStorage.setItem(RECIPE_CHECK_STORAGE_KEY, json);
localStorage.setItem(RECIPE_CHECK_STORAGE_KEY, json);
updateResetButtonsVisibility(checkedIngredients.length > 0, checkedSteps.length > 0);
} catch (e) {
console.warn('Fehler beim Speichern des Abhake-Status:', e);
}
}
function loadChecksFromStorage() {
try {
let raw = sessionStorage.getItem(RECIPE_CHECK_STORAGE_KEY);
if (!raw) {
raw = localStorage.getItem(RECIPE_CHECK_STORAGE_KEY);
}
if (!raw) {
updateResetButtonsVisibility(false, false);
return;
}
const data = JSON.parse(raw);
// Nach 8 Stunden verfällt der Zwischenspeicher automatisch
const EIGHT_HOURS = 8 * 60 * 60 * 1000;
if (data.updatedAt && (Date.now() - data.updatedAt > EIGHT_HOURS)) {
sessionStorage.removeItem(RECIPE_CHECK_STORAGE_KEY);
localStorage.removeItem(RECIPE_CHECK_STORAGE_KEY);
updateResetButtonsVisibility(false, false);
return;
}
const ingEls = getCheckableIngredients();
if (Array.isArray(data.ingredients)) {
data.ingredients.forEach(idx => {
if (ingEls[idx]) ingEls[idx].classList.add('is-checked');
});
}
const stepEls = getCheckableSteps();
if (Array.isArray(data.steps)) {
data.steps.forEach(idx => {
if (stepEls[idx]) stepEls[idx].classList.add('is-done');
});
}
const hasIngChecks = Array.isArray(data.ingredients) && data.ingredients.length > 0;
const hasStepChecks = Array.isArray(data.steps) && data.steps.length > 0;
updateResetButtonsVisibility(hasIngChecks, hasStepChecks);
} catch (e) {
console.warn('Fehler beim Laden des Abhake-Status:', e);
}
}
// Interaktive Zutaten-Checkliste (zum Abhaken beim Vorbereiten)
function toggleIngredientCheck(el) {
el.classList.toggle('is-checked');
saveChecksToStorage();
}
// Interaktiver Schritt-Tracker (zum Markieren erledigter Schritte)
function toggleStepCheck(el) {
el.classList.toggle('is-done');
saveChecksToStorage();
}
function resetIngredientChecks() {
const ingEls = getCheckableIngredients();
ingEls.forEach(el => el.classList.remove('is-checked'));
saveChecksToStorage();
showToast('Zutaten zurückgesetzt');
}
function resetStepChecks() {
const stepEls = getCheckableSteps();
stepEls.forEach(el => el.classList.remove('is-done'));
saveChecksToStorage();
showToast('Schritte zurückgesetzt');
}
function resetAllChecks() {
const ingEls = getCheckableIngredients();
ingEls.forEach(el => el.classList.remove('is-checked'));
const stepEls = getCheckableSteps();
stepEls.forEach(el => el.classList.remove('is-done'));
try {
sessionStorage.removeItem(RECIPE_CHECK_STORAGE_KEY);
localStorage.removeItem(RECIPE_CHECK_STORAGE_KEY);
} catch (e) {}
updateResetButtonsVisibility(false, false);
showToast('Alles zurückgesetzt');
}
// Toast Benachrichtigung