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
+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>