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) ## 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. 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 - Firebase-Projekt Setup & Anbindung
- Newsletter-System (Firebase Firestore) - Newsletter-System (Firebase Firestore)
- Benutzerprofile (Registrierung, Login, Favoriten) - Benutzerprofile (Registrierung, Login, Favoriten)
- [ ] **Phase 3: Nährwerte & Rezepterweiterung** - [x] **Phase 3: Nährwerte & Rezepterweiterung**
- Datenmodell & Admin.php Erweiterung - Datenmodell & Admin.php Erweiterung
- Visuelles Nährwert-Widget (Floema-Präzision) - Visuelles Nährwert-Widget (Floema-Präzision)
- Makro-/Diät-Filter & Kategorisierung - 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 - Admin-Erweiterung für Step-Videos
- Fullscreen Cooking-Mode Overlay (mit Lenis scroll lock) - Fullscreen Cooking-Mode Overlay (mit Lenis scroll lock)
- Timers & Looping Video Players pro Zubereitungsschritt - 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 ### 🥗 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. 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** - [x] **Datenmodell & Admin-Panel Erweiterung**
- [ ] `data/recipes.json` Struktur anpassen: Hinzufügen von `nutrition` (`calories`, `protein`, `carbs`, `fat`) - [x] `data/recipes.json` Struktur anpassen: Hinzufügen von `nutrition` (`calories`, `protein`, `carbs`, `fat`)
- [ ] `admin.php` Formular erweitern: - [x] `admin.php` Formular erweitern:
- [ ] Eingabefeld für Kalorien (kcal) - [x] Eingabefeld für Kalorien (kcal)
- [ ] Eingabefeld für Eiweiß / Protein (g) - [x] Eingabefeld für Eiweiß / Protein (g)
- [ ] Eingabefeld für Kohlenhydrate (g) - [x] Eingabefeld für Kohlenhydrate (g)
- [ ] Eingabefeld für Fett (g) - [x] Eingabefeld für Fett (g)
- [ ] Validierung & Speicherung der Nährwertdaten in `admin.php` absichern - [x] Validierung & Speicherung der Nährwertdaten in `admin.php` absichern
- [ ] **Premium Nährwert-Widget (`recipe.php`)** - [x] **Premium Nährwert-Widget (`recipe.php`)**
- [ ] Layout-Integration basierend auf dem `FloemaLayoutGrid` (24-Spalten-Asymmetrie) - [x] 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)`) - [x] 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)`) - [x] Hover- und Lade-Mikroanimationen für die Diagramme (Wachstumseffekt mit `var(--ease-out-expo)`)
- [ ] Tooltips mit Prozentangaben am Tagesbedarf - [x] Tooltips mit Prozentangaben am Tagesbedarf
--- ---
### ⏱️ Phase 4: Interaktiver Step-by-Step Rezept-Assistent ### ⏱️ 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. 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** - [x] **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 - [x] 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 - [x] `admin.php` erweitern, um Video-URLs pro Einzelschritt einzugeben
- [ ] **Fullscreen Cooking-Mode Overlay** - [x] **Fullscreen Cooking-Mode Overlay**
- [ ] Trigger-Button "Kochmodus starten" (`CapitoliumRevealButton` Stil) auf `recipe.php` einbauen - [x] Trigger-Button "Kochmodus starten" (`CapitoliumRevealButton` Stil) auf `recipe.php` einbauen
- [ ] Fullscreen Overlay mit flüssigem `clip-path` Öffnungseffekt (analog zu `LiquidOverlayMenu`) - [x] 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 - [x] Lenis Scroll-Lock während der Kochmodus aktiv ist, um störendes Scrollen im Hintergrund zu verhindern
- [ ] **Interaktives Koch-Interface** - [x] **Interaktives Koch-Interface**
- [ ] Schritt-für-Schritt Slider/Karussell (Tastatur- & Touch-Navigation) - [x] Schritt-für-Schritt Slider/Karussell (Tastatur- & Touch-Navigation)
- [ ] Fortschrittsanzeige (z.B. "Schritt 3 von 8" + Fortschrittsbalken) - [x] Fortschrittsanzeige (z.B. "Schritt 3 von 8" + Fortschrittsbalken)
- [ ] Autolooping Video-Player: Zeigt das Kurzvideo zum aktuellen Zubereitungsschritt, stummgeschaltet und nahtlos wiederholend - [x] 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] 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 // helpers
$parse_csv = fn($text) => array_values(array_filter(array_map('trim', explode(',', $text ?? '')), 'strlen')); $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 = 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'] ?? ''); $titleEn = trim($_POST['title_en'] ?? '');
$titleDe = trim($_POST['title_de'] ?? ''); $titleDe = trim($_POST['title_de'] ?? '');
@@ -200,6 +201,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
$utensilsDe = $parse_lines($_POST['utensils_de'] ?? ''); $utensilsDe = $parse_lines($_POST['utensils_de'] ?? '');
$stepsEn = $parse_lines($_POST['steps_en'] ?? ''); $stepsEn = $parse_lines($_POST['steps_en'] ?? '');
$stepsDe = $parse_lines($_POST['steps_de'] ?? ''); $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'; $featured = isset($_POST['featured']) && $_POST['featured'] === '1';
$comingSoon = isset($_POST['coming_soon']) && $_POST['coming_soon'] === '1'; $comingSoon = isset($_POST['coming_soon']) && $_POST['coming_soon'] === '1';
$slugOriginal = trim($_POST['slug_original'] ?? ''); $slugOriginal = trim($_POST['slug_original'] ?? '');
@@ -229,6 +234,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
$utensilsDe = !empty($utensilsDe) ? $utensilsDe : ($prevDe['utensils'] ?? []); $utensilsDe = !empty($utensilsDe) ? $utensilsDe : ($prevDe['utensils'] ?? []);
$stepsEn = !empty($stepsEn) ? $stepsEn : ($prevEn['steps'] ?? []); $stepsEn = !empty($stepsEn) ? $stepsEn : ($prevEn['steps'] ?? []);
$stepsDe = !empty($stepsDe) ? $stepsDe : ($prevDe['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 === '') { if ($titleEn === '') {
$message = 'English title is required.'; $message = 'English title is required.';
@@ -282,6 +291,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
'ingredients' => $ingredientsEn, 'ingredients' => $ingredientsEn,
'utensils' => $utensilsEn, 'utensils' => $utensilsEn,
'steps' => $stepsEn, 'steps' => $stepsEn,
'step_videos' => $stepVideosEn,
'step_timers' => $stepTimersEn,
], ],
'de' => [ 'de' => [
'title' => $titleDe ?: $titleEn, 'title' => $titleDe ?: $titleEn,
@@ -292,6 +303,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
'ingredients' => !empty($ingredientsDe) ? $ingredientsDe : $ingredientsEn, 'ingredients' => !empty($ingredientsDe) ? $ingredientsDe : $ingredientsEn,
'utensils' => !empty($utensilsDe) ? $utensilsDe : $utensilsEn, 'utensils' => !empty($utensilsDe) ? $utensilsDe : $utensilsEn,
'steps' => !empty($stepsDe) ? $stepsDe : $stepsEn, '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> <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> <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> <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> <hr>
<h3>Deutsch</h3> <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> <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> <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> <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="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> <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; transition: width 1.5s var(--ease-out-expo) 0.2s;
will-change: width; 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;
}
+6
View File
@@ -533,7 +533,13 @@ include __DIR__ . '/partials/head.php';
setTimeout(tickPreloader, 65 + Math.random() * 80); setTimeout(tickPreloader, 65 + Math.random() * 80);
} }
} }
if (sessionStorage.getItem('flixcooks_preloader_seen')) {
if (preEl && preEl.parentNode) preEl.parentNode.removeChild(preEl);
done = true;
} else {
sessionStorage.setItem('flixcooks_preloader_seen', 'true');
tickPreloader(); tickPreloader();
}
/* ─────────────────────────────────────────────────────────────── /* ───────────────────────────────────────────────────────────────
+160 -1
View File
@@ -162,7 +162,12 @@ include __DIR__ . '/partials/header.php';
<?php endif; ?> <?php endif; ?>
<section class="panel"> <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"> <ol class="steps">
<?php foreach ($recipe['steps'] as $step): ?> <?php foreach ($recipe['steps'] as $step): ?>
<li><?php echo e($step); ?></li> <li><?php echo e($step); ?></li>
@@ -202,6 +207,46 @@ include __DIR__ . '/partials/header.php';
</section> </section>
</article> </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'; ?> <?php include __DIR__ . '/partials/footer.php'; ?>
<script> <script>
@@ -231,4 +276,118 @@ if (typeof Lenis !== 'undefined') {
function raf(time) { lenis.raf(time); requestAnimationFrame(raf); } function raf(time) { lenis.raf(time); requestAnimationFrame(raf); }
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> </script>