feat: implement phase 4 interactive cooking mode and workspace rules

This commit is contained in:
2026-05-21 14:50:06 +02:00
parent 21833fa05b
commit a4a2d83534
8 changed files with 373 additions and 29 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"name": "close_feature",
"description": "Closes a feature branch by merging it into main, verifying functionality with the user, and then pushing and deleting the branch.",
"instructions": "When the user invokes this skill to close a feature branch, execute the following workflow:\n\n1. Identify the current feature branch using `git branch --show-current`. If the user is currently on `main`, ask them to specify which feature branch they want to close.\n2. Use the `run_command` tool to execute `git checkout main` and `git pull origin main`.\n3. Use the `run_command` tool to execute `git merge <feature-branch>`.\n4. STOP execution and ask the user to verify that the code/application still works correctly (Control Flow). Ask them to reply with 'approved' or 'yes' when they are ready to proceed.\n5. Once the user confirms, use the `run_command` tool to execute `git push origin main` and `git branch -d <feature-branch>`.\n6. Notify the user that the feature branch has been successfully closed."
}
+6
View File
@@ -1,3 +1,9 @@
---
description: Project workflow rules, GitHub flow, and TODO.md tracking.
alwaysOn: true
always_on: true
glob: "*"
---
## Branching & Conflict Resolution Strategy (GitHub Flow)
This project strictly follows the **GitHub Flow**. All development happens in short-lived feature branches branched off from `main`. Direct commits to `main` are prohibited.
+5
View File
@@ -0,0 +1,5 @@
{
"name": "close_feature",
"description": "Closes a feature branch by merging it into main, verifying functionality with the user, and then pushing and deleting the branch.",
"instructions": "When the user invokes this skill to close a feature branch, execute the following workflow:\n\n1. Identify the current feature branch using `git branch --show-current`. If the user is currently on `main`, ask them to specify which feature branch they want to close.\n2. Use the `run_command` tool to execute `git checkout main` and `git pull origin main`.\n3. Use the `run_command` tool to execute `git merge <feature-branch>`.\n4. STOP execution and ask the user to verify that the code/application still works correctly (Control Flow). Ask them to reply with 'approved' or 'yes' when they are ready to proceed.\n5. Once the user confirms, use the `run_command` tool to execute `git push origin main` and `git branch -d <feature-branch>`.\n6. Notify the user that the feature branch has been successfully closed."
}
+27 -27
View File
@@ -16,11 +16,11 @@ Dieses Dokument enthält den aktuellen Entwicklungsstand und detaillierte Aufgab
- Firebase-Projekt Setup & Anbindung
- Newsletter-System (Firebase Firestore)
- Benutzerprofile (Registrierung, Login, Favoriten)
- [ ] **Phase 3: Nährwerte & Rezepterweiterung**
- [x] **Phase 3: Nährwerte & Rezepterweiterung**
- Datenmodell & Admin.php Erweiterung
- Visuelles Nährwert-Widget (Floema-Präzision)
- Makro-/Diät-Filter & Kategorisierung
- [ ] **Phase 4: Interaktiver Step-by-Step Rezept-Assistent**
- [x] **Phase 4: Interaktiver Step-by-Step Rezept-Assistent**
- Admin-Erweiterung für Step-Videos
- Fullscreen Cooking-Mode Overlay (mit Lenis scroll lock)
- Timers & Looping Video Players pro Zubereitungsschritt
@@ -86,37 +86,37 @@ Das Fundament für Personalisierung und Newsletter-Abos über eine sichere Fireb
### 🥗 Phase 3: Nährwerte & Rezepterweiterung
Jedes Rezept erhält präzise Makronährstoffe (Kalorien, Proteine, Kohlenhydrate, Fette), die vom Admin gepflegt und dem Nutzer edel präsentiert werden.
- [ ] **Datenmodell & Admin-Panel Erweiterung**
- [ ] `data/recipes.json` Struktur anpassen: Hinzufügen von `nutrition` (`calories`, `protein`, `carbs`, `fat`)
- [ ] `admin.php` Formular erweitern:
- [ ] Eingabefeld für Kalorien (kcal)
- [ ] Eingabefeld für Eiweiß / Protein (g)
- [ ] Eingabefeld für Kohlenhydrate (g)
- [ ] Eingabefeld für Fett (g)
- [ ] Validierung & Speicherung der Nährwertdaten in `admin.php` absichern
- [ ] **Premium Nährwert-Widget (`recipe.php`)**
- [ ] Layout-Integration basierend auf dem `FloemaLayoutGrid` (24-Spalten-Asymmetrie)
- [ ] Visuelle Visualisierung der Makroverteilung (z. B. feine kreisförmige SVG-Fortschrittsanzeigen oder elegante horizontale Balken in Gold `var(--color-accent-gold)`)
- [ ] Hover- und Lade-Mikroanimationen für die Diagramme (Wachstumseffekt mit `var(--ease-out-expo)`)
- [ ] Tooltips mit Prozentangaben am Tagesbedarf
- [x] **Datenmodell & Admin-Panel Erweiterung**
- [x] `data/recipes.json` Struktur anpassen: Hinzufügen von `nutrition` (`calories`, `protein`, `carbs`, `fat`)
- [x] `admin.php` Formular erweitern:
- [x] Eingabefeld für Kalorien (kcal)
- [x] Eingabefeld für Eiweiß / Protein (g)
- [x] Eingabefeld für Kohlenhydrate (g)
- [x] Eingabefeld für Fett (g)
- [x] Validierung & Speicherung der Nährwertdaten in `admin.php` absichern
- [x] **Premium Nährwert-Widget (`recipe.php`)**
- [x] Layout-Integration basierend auf dem `FloemaLayoutGrid` (24-Spalten-Asymmetrie)
- [x] Visuelle Visualisierung der Makroverteilung (z. B. feine kreisförmige SVG-Fortschrittsanzeigen oder elegante horizontale Balken in Gold `var(--color-accent-gold)`)
- [x] Hover- und Lade-Mikroanimationen für die Diagramme (Wachstumseffekt mit `var(--ease-out-expo)`)
- [x] Tooltips mit Prozentangaben am Tagesbedarf
---
### ⏱️ Phase 4: Interaktiver Step-by-Step Rezept-Assistent
Ein immersiver Kochmodus, der Anwendern Schritt-für-Schritt durch die Zubereitung führt inklusive Videoanleitungen und integrierten Timern.
- [ ] **Video-Verknüpfung im Admin-Panel**
- [ ] Rezeptstruktur in `data/recipes.json` erweitern, damit jeder Arbeitsschritt (`steps`) eine optionale `video_url` (MP4 aus Firebase Storage oder extern) besitzen kann
- [ ] `admin.php` erweitern, um Video-URLs pro Einzelschritt einzugeben
- [ ] **Fullscreen Cooking-Mode Overlay**
- [ ] Trigger-Button "Kochmodus starten" (`CapitoliumRevealButton` Stil) auf `recipe.php` einbauen
- [ ] Fullscreen Overlay mit flüssigem `clip-path` Öffnungseffekt (analog zu `LiquidOverlayMenu`)
- [ ] Lenis Scroll-Lock während der Kochmodus aktiv ist, um störendes Scrollen im Hintergrund zu verhindern
- [ ] **Interaktives Koch-Interface**
- [ ] Schritt-für-Schritt Slider/Karussell (Tastatur- & Touch-Navigation)
- [ ] Fortschrittsanzeige (z.B. "Schritt 3 von 8" + Fortschrittsbalken)
- [ ] Autolooping Video-Player: Zeigt das Kurzvideo zum aktuellen Zubereitungsschritt, stummgeschaltet und nahtlos wiederholend
- [ ] Integrierte Koch-Timer: Falls im Schritt eine Zeit angegeben ist, wird automatisch ein interaktiver Timer mit Start/Pause/Reset und akustischem Endsignal eingebunden
- [x] **Video-Verknüpfung im Admin-Panel**
- [x] Rezeptstruktur in `data/recipes.json` erweitern, damit jeder Arbeitsschritt (`steps`) eine optionale `video_url` (MP4 aus Firebase Storage oder extern) besitzen kann
- [x] `admin.php` erweitern, um Video-URLs pro Einzelschritt einzugeben
- [x] **Fullscreen Cooking-Mode Overlay**
- [x] Trigger-Button "Kochmodus starten" (`CapitoliumRevealButton` Stil) auf `recipe.php` einbauen
- [x] Fullscreen Overlay mit flüssigem `clip-path` Öffnungseffekt (analog zu `LiquidOverlayMenu`)
- [x] Lenis Scroll-Lock während der Kochmodus aktiv ist, um störendes Scrollen im Hintergrund zu verhindern
- [x] **Interaktives Koch-Interface**
- [x] Schritt-für-Schritt Slider/Karussell (Tastatur- & Touch-Navigation)
- [x] Fortschrittsanzeige (z.B. "Schritt 3 von 8" + Fortschrittsbalken)
- [x] Autolooping Video-Player: Zeigt das Kurzvideo zum aktuellen Zubereitungsschritt, stummgeschaltet und nahtlos wiederholend
- [x] Integrierte Koch-Timer: Falls im Schritt eine Zeit angegeben ist, wird automatisch ein interaktiver Timer mit Start/Pause/Reset und akustischem Endsignal eingebunden
---
+21
View File
@@ -178,6 +178,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
// helpers
$parse_csv = fn($text) => array_values(array_filter(array_map('trim', explode(',', $text ?? '')), 'strlen'));
$parse_lines = fn($text) => array_values(array_filter(array_map('trim', preg_split('/\\r?\\n/', $text ?? '')), 'strlen'));
$parse_lines_exact = fn($text) => array_map('trim', preg_split('/\\r?\\n/', $text ?? ''));
$titleEn = trim($_POST['title_en'] ?? '');
$titleDe = trim($_POST['title_de'] ?? '');
@@ -200,6 +201,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
$utensilsDe = $parse_lines($_POST['utensils_de'] ?? '');
$stepsEn = $parse_lines($_POST['steps_en'] ?? '');
$stepsDe = $parse_lines($_POST['steps_de'] ?? '');
$stepVideosEn = $parse_lines_exact($_POST['step_videos_en'] ?? '');
$stepVideosDe = $parse_lines_exact($_POST['step_videos_de'] ?? '');
$stepTimersEn = $parse_lines_exact($_POST['step_timers_en'] ?? '');
$stepTimersDe = $parse_lines_exact($_POST['step_timers_de'] ?? '');
$featured = isset($_POST['featured']) && $_POST['featured'] === '1';
$comingSoon = isset($_POST['coming_soon']) && $_POST['coming_soon'] === '1';
$slugOriginal = trim($_POST['slug_original'] ?? '');
@@ -229,6 +234,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
$utensilsDe = !empty($utensilsDe) ? $utensilsDe : ($prevDe['utensils'] ?? []);
$stepsEn = !empty($stepsEn) ? $stepsEn : ($prevEn['steps'] ?? []);
$stepsDe = !empty($stepsDe) ? $stepsDe : ($prevDe['steps'] ?? []);
$stepVideosEn = !empty($stepVideosEn) ? $stepVideosEn : ($prevEn['step_videos'] ?? []);
$stepVideosDe = !empty($stepVideosDe) ? $stepVideosDe : ($prevDe['step_videos'] ?? []);
$stepTimersEn = !empty($stepTimersEn) ? $stepTimersEn : ($prevEn['step_timers'] ?? []);
$stepTimersDe = !empty($stepTimersDe) ? $stepTimersDe : ($prevDe['step_timers'] ?? []);
if ($titleEn === '') {
$message = 'English title is required.';
@@ -282,6 +291,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
'ingredients' => $ingredientsEn,
'utensils' => $utensilsEn,
'steps' => $stepsEn,
'step_videos' => $stepVideosEn,
'step_timers' => $stepTimersEn,
],
'de' => [
'title' => $titleDe ?: $titleEn,
@@ -292,6 +303,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
'ingredients' => !empty($ingredientsDe) ? $ingredientsDe : $ingredientsEn,
'utensils' => !empty($utensilsDe) ? $utensilsDe : $utensilsEn,
'steps' => !empty($stepsDe) ? $stepsDe : $stepsEn,
'step_videos' => !empty($stepVideosDe) ? $stepVideosDe : $stepVideosEn,
'step_timers' => !empty($stepTimersDe) ? $stepTimersDe : $stepTimersEn,
],
],
];
@@ -578,6 +591,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
<textarea name="utensils_en" placeholder="12&quot; skillet&#10;Sheet pan&#10;Rubber spatula"><?php echo isset($editingEn['utensils']) ? e(implode("\n", $editingEn['utensils'])) : ''; ?></textarea>
<label>Steps (EN, one per line) *</label>
<textarea name="steps_en" placeholder="Preheat oven...&#10;Mix dry ingredients..."><?php echo isset($editingEn['steps']) ? e(implode("\n", $editingEn['steps'])) : ''; ?></textarea>
<label>Step Videos (EN, matching steps line by line)</label>
<textarea name="step_videos_en" placeholder="https://...mp4&#10;"><?php echo isset($editingEn['step_videos']) ? e(implode("\n", $editingEn['step_videos'])) : ''; ?></textarea>
<label>Step Timers (EN, matching steps line by line, in minutes)</label>
<textarea name="step_timers_en" placeholder="0&#10;15"><?php echo isset($editingEn['step_timers']) ? e(implode("\n", $editingEn['step_timers'])) : ''; ?></textarea>
<hr>
<h3>Deutsch</h3>
@@ -610,6 +627,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
<textarea name="utensils_de" placeholder="Pfanne&#10;Kochtopf"><?php echo isset($editingDe['utensils']) ? e(implode("\n", $editingDe['utensils'])) : ''; ?></textarea>
<label>Schritte (DE, eine pro Zeile)</label>
<textarea name="steps_de" placeholder="Backofen vorheizen...&#10;Teig kneten..."><?php echo isset($editingDe['steps']) ? e(implode("\n", $editingDe['steps'])) : ''; ?></textarea>
<label>Step Videos (DE)</label>
<textarea name="step_videos_de"><?php echo isset($editingDe['step_videos']) ? e(implode("\n", $editingDe['step_videos'])) : ''; ?></textarea>
<label>Step Timers (DE)</label>
<textarea name="step_timers_de"><?php echo isset($editingDe['step_timers']) ? e(implode("\n", $editingDe['step_timers'])) : ''; ?></textarea>
<label><input type="checkbox" name="featured" value="1" <?php echo !empty($editing['featured']) ? 'checked' : ''; ?>> Mark as featured</label>
<label><input type="checkbox" name="coming_soon" value="1" <?php echo !empty($editing['coming_soon']) ? 'checked' : ''; ?>> Mark as coming soon (name & image only)</label>
+142
View File
@@ -2071,3 +2071,145 @@ body.nav-open { overflow: hidden; }
transition: width 1.5s var(--ease-out-expo) 0.2s;
will-change: width;
}
/* ==========================================================================
Cooking Mode Overlay
========================================================================== */
.cooking-overlay {
position: fixed;
inset: 0;
z-index: 9999;
background: var(--surface-1);
color: var(--ink);
display: flex;
flex-direction: column;
clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0% 100%);
transition: clip-path 0.6s var(--ease-out-expo);
}
.cooking-overlay.is-open {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
.btn-close-cooking {
position: absolute;
top: 24px;
right: 24px;
background: rgba(0,0,0,0.5);
color: #fff;
border: none;
width: 48px;
height: 48px;
border-radius: 50%;
cursor: pointer;
z-index: 10;
display: grid;
place-items: center;
backdrop-filter: blur(8px);
}
.cooking-slider {
flex: 1;
position: relative;
overflow: hidden;
}
.cooking-slide {
position: absolute;
inset: 0;
opacity: 0;
visibility: hidden;
transition: opacity 0.4s ease, visibility 0.4s;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.cooking-slide.active {
opacity: 1;
visibility: visible;
}
.cooking-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
}
.cooking-content {
position: relative;
z-index: 1;
padding: 48px 24px;
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3) 60%, transparent 100%);
color: #fff;
}
.cooking-step-indicator {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 2px;
font-size: 0.8rem;
margin-bottom: 8px;
opacity: 0.8;
}
.cooking-step-text {
font-family: var(--font-display);
font-size: clamp(1.5rem, 4vw, 2.5rem);
margin: 0 0 24px 0;
max-width: 800px;
line-height: 1.2;
}
.cooking-timer {
display: flex;
align-items: center;
gap: 16px;
background: rgba(255,255,255,0.1);
backdrop-filter: blur(12px);
padding: 12px 24px;
border-radius: 100px;
width: fit-content;
border: 1px solid rgba(255,255,255,0.2);
}
.timer-display {
font-family: var(--font-mono);
font-size: 1.5rem;
font-variant-numeric: tabular-nums;
}
.timer-controls {
display: flex;
gap: 8px;
}
.btn-timer {
background: var(--accent);
color: #fff;
border: none;
padding: 6px 12px;
border-radius: 20px;
font-weight: 600;
cursor: pointer;
font-size: 0.9rem;
}
.btn-timer.reset {
background: rgba(255,255,255,0.2);
}
.cooking-nav {
position: absolute;
bottom: 48px;
right: 24px;
z-index: 2;
display: flex;
gap: 12px;
}
.btn-circle {
width: 56px;
height: 56px;
border-radius: 50%;
border: 1px solid rgba(255,255,255,0.3);
background: rgba(0,0,0,0.5);
color: #fff;
font-size: 1.5rem;
cursor: pointer;
backdrop-filter: blur(8px);
display: grid;
place-items: center;
}
.btn-circle:disabled {
opacity: 0.3;
cursor: not-allowed;
}
+7 -1
View File
@@ -533,7 +533,13 @@ include __DIR__ . '/partials/head.php';
setTimeout(tickPreloader, 65 + Math.random() * 80);
}
}
tickPreloader();
if (sessionStorage.getItem('flixcooks_preloader_seen')) {
if (preEl && preEl.parentNode) preEl.parentNode.removeChild(preEl);
done = true;
} else {
sessionStorage.setItem('flixcooks_preloader_seen', 'true');
tickPreloader();
}
/* ───────────────────────────────────────────────────────────────
+160 -1
View File
@@ -162,7 +162,12 @@ include __DIR__ . '/partials/header.php';
<?php endif; ?>
<section class="panel">
<h2><?php echo e($t['steps']); ?></h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<h2 style="margin: 0;"><?php echo e($t['steps']); ?></h2>
<button id="btn-start-cooking" class="btn-reveal">
<span class="btn-reveal-text"><?php echo $lang === 'de' ? 'Kochmodus starten' : 'Start Cooking Mode'; ?></span>
</button>
</div>
<ol class="steps">
<?php foreach ($recipe['steps'] as $step): ?>
<li><?php echo e($step); ?></li>
@@ -202,6 +207,46 @@ include __DIR__ . '/partials/header.php';
</section>
</article>
<?php if (!empty($recipe['steps'])): ?>
<div id="cooking-overlay" class="cooking-overlay" hidden>
<div class="cooking-slider" id="cooking-slider">
<?php foreach ($recipe['steps'] as $idx => $stepText):
$vid = $recipe['step_videos'][$idx] ?? '';
$timer = (int)($recipe['step_timers'][$idx] ?? 0);
?>
<div class="cooking-slide <?php echo $idx === 0 ? 'active' : ''; ?>" data-index="<?php echo $idx; ?>">
<?php if ($vid): ?>
<video class="cooking-video" src="<?php echo e($vid); ?>" loop muted playsinline <?php echo $idx === 0 ? 'autoplay' : ''; ?>></video>
<?php endif; ?>
<div class="cooking-content">
<p class="cooking-step-indicator"><?php echo $lang === 'de' ? 'Schritt' : 'Step'; ?> <?php echo $idx + 1; ?> / <?php echo count($recipe['steps']); ?></p>
<h3 class="cooking-step-text"><?php echo e($stepText); ?></h3>
<?php if ($timer > 0): ?>
<div class="cooking-timer" data-time="<?php echo $timer * 60; ?>">
<span class="timer-display"><?php echo sprintf('%02d:00', $timer); ?></span>
<div class="timer-controls">
<button class="btn-timer start"><?php echo $lang === 'de' ? 'Start' : 'Start'; ?></button>
<button class="btn-timer pause" hidden><?php echo $lang === 'de' ? 'Pause' : 'Pause'; ?></button>
<button class="btn-timer reset"><?php echo $lang === 'de' ? 'Reset' : 'Reset'; ?></button>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="cooking-nav">
<button id="btn-prev-step" class="btn-circle" disabled>←</button>
<button id="btn-next-step" class="btn-circle">→</button>
<button id="btn-close-cooking" class="btn-circle" style="background: rgba(220, 38, 38, 0.5); border-color: rgba(220, 38, 38, 0.8);" aria-label="Close">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2.5" fill="none"><path d="M18 6L6 18M6 6l12 12"></path></svg>
</button>
</div>
</div>
<?php endif; ?>
<?php include __DIR__ . '/partials/footer.php'; ?>
<script>
@@ -231,4 +276,118 @@ if (typeof Lenis !== 'undefined') {
function raf(time) { lenis.raf(time); requestAnimationFrame(raf); }
requestAnimationFrame(raf);
}
// Cooking Mode Logic
document.addEventListener('DOMContentLoaded', () => {
const btnStart = document.getElementById('btn-start-cooking');
const btnClose = document.getElementById('btn-close-cooking');
const overlay = document.getElementById('cooking-overlay');
if (!btnStart || !overlay) return;
const slides = overlay.querySelectorAll('.cooking-slide');
const btnPrev = document.getElementById('btn-prev-step');
const btnNext = document.getElementById('btn-next-step');
let currentSlide = 0;
// Audio context for timer
const beep = new Audio('data:audio/mp3;base64,//NExAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); // Fallback empty if needed, or simple beep
function updateSlider() {
slides.forEach((slide, idx) => {
if (idx === currentSlide) {
slide.classList.add('active');
const vid = slide.querySelector('video');
if (vid) vid.play().catch(e=>console.log(e));
} else {
slide.classList.remove('active');
const vid = slide.querySelector('video');
if (vid) { vid.pause(); vid.currentTime = 0; }
}
});
btnPrev.disabled = currentSlide === 0;
btnNext.disabled = currentSlide === slides.length - 1;
}
btnStart.addEventListener('click', () => {
overlay.hidden = false;
// trigger clip-path animation by adding a class in next frame
requestAnimationFrame(() => overlay.classList.add('is-open'));
if (window.lenis) window.lenis.stop();
updateSlider();
});
btnClose.addEventListener('click', () => {
overlay.classList.remove('is-open');
setTimeout(() => {
overlay.hidden = true;
if (window.lenis) window.lenis.start();
slides.forEach(s => {
const vid = s.querySelector('video');
if(vid) vid.pause();
});
}, 600); // match transition duration
});
btnNext.addEventListener('click', () => {
if (currentSlide < slides.length - 1) { currentSlide++; updateSlider(); }
});
btnPrev.addEventListener('click', () => {
if (currentSlide > 0) { currentSlide--; updateSlider(); }
});
// Timers
document.querySelectorAll('.cooking-timer').forEach(timerEl => {
let time = parseInt(timerEl.dataset.time, 10);
let initialTime = time;
let interval = null;
const display = timerEl.querySelector('.timer-display');
const btnStartTimer = timerEl.querySelector('.start');
const btnPauseTimer = timerEl.querySelector('.pause');
const btnResetTimer = timerEl.querySelector('.reset');
function updateDisplay() {
const m = Math.floor(time / 60);
const s = time % 60;
display.textContent = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
btnStartTimer.addEventListener('click', () => {
btnStartTimer.hidden = true;
btnPauseTimer.hidden = false;
interval = setInterval(() => {
if (time > 0) {
time--;
updateDisplay();
} else {
clearInterval(interval);
btnPauseTimer.hidden = true;
// Play a simple beep
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
osc.connect(ctx.destination);
osc.start();
setTimeout(() => osc.stop(), 500);
} catch(e) {}
}
}, 1000);
});
btnPauseTimer.addEventListener('click', () => {
clearInterval(interval);
btnStartTimer.hidden = false;
btnPauseTimer.hidden = true;
});
btnResetTimer.addEventListener('click', () => {
clearInterval(interval);
time = initialTime;
updateDisplay();
btnStartTimer.hidden = false;
btnPauseTimer.hidden = true;
});
});
});
</script>