feat: implement phase 4 interactive cooking mode and workspace rules
This commit is contained in:
@@ -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."
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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" skillet Sheet pan 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... 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 "><?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 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 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... 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user