Files
flixcooks-website/index.php
T

812 lines
36 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require __DIR__ . '/helpers.php';
// Language & inputs
$lang = strtolower((string) (filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?: 'en')) === 'de' ? 'de' : 'en';
$q = trim((string) (filter_input(INPUT_GET, 'q', FILTER_UNSAFE_RAW) ?? ''));
$tag = trim((string) (filter_input(INPUT_GET, 'tag', FILTER_UNSAFE_RAW) ?? ''));
// Language copy
$copy = [
'en' => [
'home' => 'Home',
'latest' => 'Latest',
'basics' => 'Basics',
'eyebrow' => 'Seasonal · Unfussy · Ridiculously tasty.',
'search_placeholder' => 'Search for pasta, brunch, sauce...',
'search_button' => 'Search',
'tag_clear' => 'Clear',
'featured' => 'Featured',
'latest_drops' => 'Latest drops',
'count_suffix_default' => ' recipes ready to cook.',
'count_suffix_filtered' => ' recipes matching your filter.',
'kitchen_basics' => 'Kitchen basics',
'basics_sub' => 'Quick wins that upgrade everything else.',
'servings' => 'servings',
'cook_it' => 'Cook it',
'coming_features' => 'Coming soon',
'coming_features_sub'=> 'Features I\'m shipping next.',
'discover_title' => 'Find your next easy favorite.',
'discover_text' => 'Search, filter by tag, and jump into a recipe.',
'browse_tags' => 'Browse tags',
'quick_start_title' => 'Quick start',
'library_title' => 'Recipe library',
'library_text' => 'Every recipe, searchable and filterable.',
'stats_recipes' => 'live recipes',
'stats_average' => 'avg. total time',
'stats_tags' => 'browseable tags',
'empty_title' => 'No recipes found',
'empty_text' => 'Try another search term or clear the active tag.',
'view_recipe' => 'View recipe',
'stage_eyebrow' => 'Our Recipes',
'stage_title_1' => 'Handcrafted',
'stage_title_2' => 'dishes.',
'stage_scroll' => 'scroll',
'stage_of' => 'of',
],
'de' => [
'home' => 'Start',
'latest' => 'Neueste',
'basics' => 'Basics',
'eyebrow' => 'Saisonal · Unkompliziert · Richtig lecker.',
'search_placeholder' => 'Suche nach Pasta, Brunch, Sauce...',
'search_button' => 'Suchen',
'tag_clear' => 'Zurücksetzen',
'featured' => 'Highlight',
'latest_drops' => 'Frisch dazugekommen',
'count_suffix_default' => ' Rezepte bereit zum Kochen.',
'count_suffix_filtered' => ' Rezepte passend zum Filter.',
'kitchen_basics' => 'Küchenbasics',
'basics_sub' => 'Kleine Tricks, die alles besser machen.',
'servings' => 'Portionen',
'cook_it' => 'Nachkochen',
'coming_features' => 'Demnächst',
'coming_features_sub'=> 'Features, an denen ich gerade baue.',
'discover_title' => 'Finde schnell dein nächstes Lieblingsrezept.',
'discover_text' => 'Suchen, nach Tag filtern und ins Rezept springen.',
'browse_tags' => 'Tags entdecken',
'quick_start_title' => 'Schnell starten',
'library_title' => 'Rezeptübersicht',
'library_text' => 'Alle Rezepte, durchsuchbar und filterbar.',
'stats_recipes' => 'Rezepte online',
'stats_average' => 'Ø Gesamtzeit',
'stats_tags' => 'durchsuchbare Tags',
'empty_title' => 'Keine Rezepte gefunden',
'empty_text' => 'Probiere einen anderen Suchbegriff oder entferne den aktiven Tag.',
'view_recipe' => 'Rezept ansehen',
'stage_eyebrow' => 'Unsere Rezepte',
'stage_title_1' => 'Handgefertigte',
'stage_title_2' => 'Gerichte.',
'stage_scroll' => 'scrollen',
'stage_of' => 'von',
],
];
$t = $copy[$lang];
// Data
$allRecipes = load_recipes();
$localizedAll = localize_recipes($allRecipes, $lang);
$recipes = filter_recipes($allRecipes, $q ?: null, $tag ?: null, $lang);
$recipes = array_values(array_filter($recipes, fn($r) => empty($r['coming_soon'])));
$comingSoon = array_values(array_filter($localizedAll, fn($r) => !empty($r['coming_soon'])));
$placeholderTitle = $lang === 'de' ? 'Bald verfügbar' : 'Coming soon';
$comingSoonDisplay = array_pad($comingSoon, 5, [
'hero' => '/assets/placeholder.svg',
'title' => $placeholderTitle,
]);
$featured = null;
foreach ($allRecipes as $recipe) {
if (!empty($recipe['featured']) && empty($recipe['coming_soon'])) {
$featured = $recipe;
break;
}
}
$featured = $featured ?? ($allRecipes[0] ?? null);
$featured = $featured ? localize_recipe($featured, $lang) : null;
// Build tag cloud
$allTags = [];
foreach ($localizedAll as $recipe) {
foreach ($recipe['tags'] ?? [] as $tTag) { $allTags[] = $tTag; }
}
$allTags = array_values(array_unique($allTags));
// Stage recipes (only fully published, non-coming-soon)
$stageRecipes = array_values(array_filter($localizedAll, fn($r) => empty($r['coming_soon']) && !empty($r['title'])));
$comingFeatures = $lang === 'de'
? [
'Nährwerte & Rezepte Vorschlag individuell angepasst auf Ernährungsziel',
'Einkaufsliste ans Handy senden',
'Step-by-Step Rezepte Assistent mit Videos',
]
: [
'Nutrition info & recipe suggestions tailored to your goals',
'Send your grocery list straight to your phone',
'Step-by-step recipe assistant with videos',
];
// Meta
$pageTitle = $lang === 'de'
? 'FlixCooks | Moderner Foodblog für schnelle Alltagsküche'
: 'FlixCooks | Modern food blog for busy home cooks';
$description = $lang === 'de'
? 'Frische, schnelle Rezepte mit klaren Schritten und großem Geschmack.'
: 'Fresh, fast recipes with clear steps, smart prep notes, and big flavor.';
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
$navHome = $t['home'];
$navLatest = $t['latest'];
$navBasics = $t['basics'];
$recipeCount = count($recipes);
$totalMinutes = array_sum(array_map(fn($r) => (int)($r['total_time'] ?? 0), $recipes));
$averageMinutes = $recipeCount > 0 ? (int)ceil($totalMinutes / $recipeCount) : 0;
$activeTag = $tag !== '' ? $tag : null;
$tagCount = count($allTags);
// Small helpers for URLs
$langQuery = ['lang' => $lang];
$recipeUrl = fn(array $recipe) => '/recipe.php?' . http_build_query(['slug' => $recipe['slug']] + $langQuery);
$tagUrl = fn(string $tagValue) => '/index.php?' . http_build_query(['tag' => $tagValue] + $langQuery);
include __DIR__ . '/partials/head.php';
?>
<!-- ╔══════════════════════════════════════════════════════════════╗
║ CAPITOLIUM PRELOADER ║
╚══════════════════════════════════════════════════════════════╝ -->
<div class="preloader" id="preloader" aria-hidden="true" role="presentation">
<div class="preloader-content">
<p class="preloader-logo">Flix<span>Cooks</span></p>
<div class="preloader-bar">
<div class="preloader-fill" id="preloaderFill"></div>
</div>
<p class="preloader-text">
<?php echo $lang === 'de' ? 'Wird zubereitet' : 'Preparing'; ?> &nbsp;
<span class="preloader-perc" id="preloaderPerc">0</span>%
</p>
</div>
</div>
<?php include __DIR__ . '/partials/header.php'; ?>
<!-- ╔══════════════════════════════════════════════════════════════╗
║ INDEX SECTION INDICATOR ║
╚══════════════════════════════════════════════════════════════╝ -->
<div class="section-indicator" id="sectionIndicator" aria-hidden="true">
<div class="indicator-index" id="indicatorIndex">01</div>
<div class="indicator-line-wrapper">
<div class="indicator-line-fill" id="indicatorLineFill"></div>
</div>
<div class="indicator-name" id="indicatorName"><?php echo $lang === 'de' ? 'Start' : 'Start'; ?></div>
</div>
<!-- ╔══════════════════════════════════════════════════════════════╗
║ LANDING SECTION ║
╚══════════════════════════════════════════════════════════════╝ -->
<section class="landing" id="landing" data-section-name="<?php echo $lang === 'de' ? 'Start' : 'Home'; ?>" aria-label="Landing">
<div class="landing__blobs" aria-hidden="true">
<div class="blob blob--a"></div>
<div class="blob blob--b"></div>
<div class="blob blob--c"></div>
</div>
<div class="landing__chips" aria-hidden="true">
<div class="chip chip--1">
<img src="/assets/pasta-tomato.jpg" alt="">
<span><?php echo $lang === 'de' ? 'Pasta' : 'Pasta'; ?></span>
</div>
<div class="chip chip--2">
<img src="/assets/steak.jpg" alt="">
<span><?php echo $lang === 'de' ? 'Steak' : 'Steak'; ?></span>
</div>
<div class="chip chip--3">
<img src="/assets/pancakes.jpg" alt="">
<span><?php echo $lang === 'de' ? 'Pfannkuchen' : 'Pancakes'; ?></span>
</div>
<div class="chip chip--4">
<img src="/assets/haferkuchen.JPEG" alt="">
<span><?php echo $lang === 'de' ? 'Kuchen' : 'Cake'; ?></span>
</div>
</div>
<div class="landing__copy">
<p class="landing__eyebrow"><?php echo e($t['eyebrow']); ?></p>
<h1 class="landing__headline">
<?php if ($lang === 'de'): ?>
<span class="line line--1">Besser&nbsp;<em>kochen</em></span>
<span class="line line--2">unter der Woche.</span>
<span class="line line--3">Glänzen&nbsp;<em>am Wochenende.</em></span>
<?php else: ?>
<span class="line line--1">Cook&nbsp;<em>better</em></span>
<span class="line line--2">weeknights.</span>
<span class="line line--3">Brighter&nbsp;<em>weekends.</em></span>
<?php endif; ?>
</h1>
<p class="landing__sub"><?php echo $lang === 'de' ? 'Suche nach einem Gericht, filtere nach Stimmung oder entdecke das Highlight der Woche.' : 'Search for a craving, filter by vibe, or jump into this week\'s featured recipe.'; ?></p>
<a href="#recipe-stage" class="landing__cta" id="landingCta">
<span><?php echo $lang === 'de' ? 'Rezepte entdecken' : 'Explore recipes'; ?></span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12l7 7 7-7"/></svg>
</a>
</div>
<div class="landing__scroll-nudge" aria-hidden="true">
<div class="scroll-line"></div>
<span><?php echo $lang === 'de' ? 'scrollen' : 'scroll'; ?></span>
</div>
</section>
<!-- ╔══════════════════════════════════════════════════════════════╗
║ RECIPE SHOWCASE STAGE (GSAP pinned ScrollTrigger) ║
╚══════════════════════════════════════════════════════════════╝ -->
<?php if (!empty($stageRecipes)): ?>
<section class="recipe-stage" id="recipe-stage" data-section-name="<?php echo $lang === 'de' ? 'Rezepte' : 'Recipes'; ?>" aria-label="<?php echo $lang === 'de' ? 'Rezept-Showcase' : 'Recipe showcase'; ?>">
<!-- Stage intro (visible before first slide enters) -->
<div class="recipe-stage__intro" id="stageIntro">
<p class="recipe-stage__intro-eyebrow"><?php echo e($t['stage_eyebrow']); ?></p>
<h2 class="recipe-stage__intro-title">
<?php echo e($t['stage_title_1']); ?><br><em><?php echo e($t['stage_title_2']); ?></em>
</h2>
<div class="recipe-stage__intro-line"></div>
</div>
<!-- Slides -->
<div class="recipe-stage__slides" id="stageSlides">
<?php foreach ($stageRecipes as $i => $sr): ?>
<div class="recipe-stage__slide" id="stageSlide<?php echo $i; ?>" aria-label="<?php echo e($sr['title']); ?>">
<div class="recipe-stage__slide-image">
<img src="<?php echo e($sr['hero']); ?>" alt="<?php echo e($sr['title']); ?>" loading="<?php echo $i === 0 ? 'eager' : 'lazy'; ?>">
</div>
<div class="recipe-stage__slide-copy">
<p class="recipe-stage__slide-num">
<?php printf('%02d %s %02d', $i + 1, '/', count($stageRecipes)); ?>
</p>
<?php if (!empty($sr['category'])): ?>
<p class="recipe-stage__slide-category"><?php echo e($sr['category']); ?></p>
<?php elseif (!empty($sr['tags'][0])): ?>
<p class="recipe-stage__slide-category"><?php echo e($sr['tags'][0]); ?></p>
<?php endif; ?>
<h3 class="recipe-stage__slide-title"><?php echo e($sr['title']); ?></h3>
<?php if (!empty($sr['description'])): ?>
<p class="recipe-stage__slide-desc"><?php echo e(mb_substr($sr['description'], 0, 160)); ?><?php echo mb_strlen($sr['description']) > 160 ? '…' : ''; ?></p>
<?php endif; ?>
<div class="recipe-stage__slide-meta">
<?php if (!empty($sr['total_time'])): ?>
<span class="recipe-stage__slide-meta-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<strong><?php echo format_minutes((int)$sr['total_time']); ?></strong>
</span>
<?php endif; ?>
<?php if (!empty($sr['servings'])): ?>
<span class="recipe-stage__slide-meta-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<strong><?php echo e($sr['servings']); ?></strong> <?php echo $lang === 'de' ? 'Port.' : 'serv.'; ?>
</span>
<?php endif; ?>
<?php if (!empty($sr['difficulty'])): ?>
<span class="recipe-stage__slide-meta-item">
<strong><?php echo e($sr['difficulty']); ?></strong>
</span>
<?php endif; ?>
</div>
<a href="<?php echo e($recipeUrl($sr)); ?>" class="recipe-stage__slide-cta">
<span><?php echo $lang === 'de' ? 'Zum Rezept' : 'View recipe'; ?></span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Progress dots -->
<div class="recipe-stage__progress" aria-hidden="true" id="stageProgress">
<?php foreach ($stageRecipes as $i => $sr): ?>
<div class="recipe-stage__progress-dot<?php echo $i === 0 ? ' active' : ''; ?>" data-index="<?php echo $i; ?>"></div>
<?php endforeach; ?>
</div>
<!-- Scroll hint -->
<div class="recipe-stage__hint" aria-hidden="true">
<span><?php echo $lang === 'de' ? 'scrollen' : 'scroll'; ?></span>
<div class="recipe-stage__hint-line"></div>
</div>
</section>
<?php endif; ?>
<!-- ╔══════════════════════════════════════════════════════════════╗
║ MAIN CONTENT (Search, Featured, Grid) ║
╚══════════════════════════════════════════════════════════════╝ -->
<div id="recipes-start" data-section-name="<?php echo $lang === 'de' ? 'Suche' : 'Search'; ?>">
<?php if (!empty($comingSoon)): ?>
<div class="coming-strip reveal-target" aria-label="Coming soon recipes">
<div class="coming-strip__head">
<p class="pill pill--gold"><?php echo e($lang === 'de' ? 'Bald verfügbar' : 'Coming soon'); ?></p>
<small><?php echo count($comingSoon); ?> <?php echo $lang === 'de' ? 'in Arbeit' : 'in progress'; ?></small>
</div>
<div class="coming-strip__row">
<?php foreach (array_slice($comingSoonDisplay, 0, 5) as $cs): ?>
<div class="coming-chip">
<div class="coming-chip__image">
<img src="<?php echo e($cs['hero'] ?? '/assets/placeholder.svg'); ?>" alt="<?php echo e($cs['title'] ?? 'Coming soon'); ?>">
</div>
<div class="coming-chip__title"><?php echo e($cs['title'] ?? 'New recipe'); ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Discover Shell: Search + Stats + Tags | Featured -->
<section class="discover-shell reveal-target" id="basics" data-section-name="<?php echo $lang === 'de' ? 'Entdecken' : 'Discover'; ?>" aria-label="<?php echo $lang === 'de' ? 'Rezepte entdecken' : 'Discover recipes'; ?>">
<div class="discover-shell__main">
<div class="section-header section-header--stack">
<p class="eyebrow"><?php echo e($lang === 'de' ? 'Basics' : 'Basics'); ?></p>
<h2><?php echo e($t['discover_title']); ?></h2>
<p><?php echo e($t['discover_text']); ?></p>
</div>
<form class="search discover-search" method="get" action="/index.php" role="search">
<input type="hidden" name="lang" value="<?php echo e($lang); ?>">
<label class="sr-only" for="recipe-search"><?php echo e($t['search_placeholder']); ?></label>
<input id="recipe-search" type="text" name="q"
placeholder="<?php echo e($t['search_placeholder']); ?>"
value="<?php echo e($q); ?>">
<?php if ($tag): ?>
<input type="hidden" name="tag" value="<?php echo e($tag); ?>">
<?php endif; ?>
<button type="submit"><?php echo e($t['search_button']); ?></button>
</form>
<div class="discover-stats" aria-label="Recipe overview">
<div class="discover-stat">
<strong><?php echo e((string)$recipeCount); ?></strong>
<span><?php echo e($t['stats_recipes']); ?></span>
</div>
<div class="discover-stat">
<strong><?php echo e(format_minutes($averageMinutes)); ?></strong>
<span><?php echo e($t['stats_average']); ?></span>
</div>
<div class="discover-stat">
<strong><?php echo e((string)$tagCount); ?></strong>
<span><?php echo e($t['stats_tags']); ?></span>
</div>
</div>
<div class="tag-panel">
<div class="tag-panel__head">
<h3><?php echo e($t['browse_tags']); ?></h3>
<?php if ($activeTag): ?>
<a class="tag clear" href="/index.php?lang=<?php echo e($lang); ?>#basics"><?php echo e($t['tag_clear']); ?></a>
<?php endif; ?>
</div>
<div class="tag-row tag-row--panel">
<?php foreach ($allTags as $tTag): ?>
<a class="tag<?php echo strtolower($tTag) === strtolower($activeTag ?? '') ? ' active' : ''; ?>"
href="<?php echo e($tagUrl($tTag)); ?>#basics">#<?php echo e($tTag); ?></a>
<?php endforeach; ?>
</div>
</div>
</div>
<?php if ($featured): ?>
<aside class="discover-shell__feature hero-card">
<img src="<?php echo e($featured['hero']); ?>" alt="<?php echo e($featured['title']); ?>">
<div class="hero-card__body">
<p class="pill pill--gold"><?php echo e($t['featured']); ?></p>
<div class="recipe__title-row">
<h3><?php echo e($featured['title']); ?></h3>
<button class="btn-favorite" data-slug="<?php echo e($featured['slug']); ?>" aria-label="Save to favorites">
<svg viewBox="0 0 24 24" stroke="currentColor" fill="none">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
</button>
</div>
<p><?php echo e($featured['description']); ?></p>
<div class="meta">
<span>⏱ <?php echo format_minutes((int)($featured['total_time'] ?? 0)); ?></span>
<span>🍽 <?php echo e($featured['servings']); ?> <?php echo e($t['servings']); ?></span>
</div>
<a class="btn-reveal" href="<?php echo e($recipeUrl($featured)); ?>">
<span class="btn-reveal-text"><?php echo e($t['cook_it']); ?></span>
<span class="btn-reveal-arrow">→</span>
</a>
</div>
</aside>
<?php endif; ?>
</section>
<!-- Recipe Grid -->
<section class="latest reveal-target" id="latest" data-section-name="<?php echo $lang === 'de' ? 'Rezepte' : 'Recipes'; ?>" aria-label="<?php echo $lang === 'de' ? 'Alle Rezepte' : 'All recipes'; ?>">
<div class="section-header section-header--stack">
<h2><?php echo e($t['library_title']); ?></h2>
<p><?php echo $recipeCount; ?><?php echo $q || $tag ? $t['count_suffix_filtered'] : $t['count_suffix_default']; ?></p>
</div>
<?php if (!empty($recipes)): ?>
<div class="grid">
<?php foreach ($recipes as $recipe): ?>
<article class="card reveal-target">
<a href="<?php echo e($recipeUrl($recipe)); ?>" class="card__image">
<img src="<?php echo e($recipe['hero']); ?>" alt="<?php echo e($recipe['title']); ?>" loading="lazy">
<?php if (!empty($recipe['category'])): ?>
<span class="pill pill--ghost"><?php echo e($recipe['category']); ?></span>
<?php endif; ?>
<button class="btn-favorite" data-slug="<?php echo e($recipe['slug']); ?>" aria-label="Save to favorites" onclick="event.preventDefault(); event.stopPropagation();">
<svg viewBox="0 0 24 24" stroke="currentColor" fill="none">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
</button>
</a>
<div class="card__body">
<h3><a href="<?php echo e($recipeUrl($recipe)); ?>"><?php echo e($recipe['title']); ?></a></h3>
<p><?php echo e($recipe['description']); ?></p>
<div class="meta">
<span>⏱ <?php echo format_minutes((int)($recipe['total_time'] ?? 0)); ?></span>
<span>🙂 <?php echo e($recipe['difficulty']); ?></span>
</div>
<?php if (!empty($recipe['tags'])): ?>
<div class="tags">
<?php foreach ($recipe['tags'] as $tTag): ?>
<a href="<?php echo e($tagUrl($tTag)); ?>" class="tag">#<?php echo e($tTag); ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else: ?>
<article class="empty-state">
<h3><?php echo e($t['empty_title']); ?></h3>
<p><?php echo e($t['empty_text']); ?></p>
<a class="btn-reveal" href="/index.php?lang=<?php echo e($lang); ?>">
<span class="btn-reveal-text"><?php echo e($t['tag_clear']); ?></span>
<span class="btn-reveal-arrow">→</span>
</a>
</article>
<?php endif; ?>
</section>
<!-- Coming Features -->
<section class="coming-features reveal-target" id="coming-features" data-section-name="<?php echo $lang === 'de' ? 'Demnächst' : 'Soon'; ?>">
<div class="section-header section-header--stack">
<p class="eyebrow"><?php echo $lang === 'de' ? 'Roadmap' : 'Roadmap'; ?></p>
<h2><?php echo e($t['coming_features']); ?></h2>
<p><?php echo e($t['coming_features_sub']); ?></p>
</div>
<div class="feature-grid">
<?php foreach ($comingFeatures as $feature): ?>
<article class="feature-card reveal-target">
<div class="feature-icon" aria-hidden="true">✨</div>
<p><?php echo e($feature); ?></p>
</article>
<?php endforeach; ?>
</div>
</section>
</div><!-- /#recipes-start -->
<?php include __DIR__ . '/partials/footer.php'; ?>
<!-- ╔══════════════════════════════════════════════════════════════╗
║ JAVASCRIPT — Preloader, Lenis, GSAP Stage, Section Indicator ║
╚══════════════════════════════════════════════════════════════╝ -->
<script>
(function() {
'use strict';
/* ───────────────────────────────────────────────────────────────
1. CAPITOLIUM PRELOADER
─────────────────────────────────────────────────────────────── */
var fillEl = document.getElementById('preloaderFill');
var percEl = document.getElementById('preloaderPerc');
var preEl = document.getElementById('preloader');
var progress = 0;
var done = false;
function tickPreloader() {
if (done) return;
var step = Math.floor(Math.random() * 14) + 6;
progress = Math.min(100, progress + step);
if (fillEl) fillEl.style.width = progress + '%';
if (percEl) percEl.textContent = progress;
if (progress >= 100) {
done = true;
setTimeout(function() {
if (preEl) {
preEl.classList.add('loaded');
setTimeout(function() {
if (preEl && preEl.parentNode) preEl.parentNode.removeChild(preEl);
}, 1200);
}
}, 280);
} else {
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();
}
/* ───────────────────────────────────────────────────────────────
2. LENIS SMOOTH SCROLL
─────────────────────────────────────────────────────────────── */
var lenis;
if (typeof Lenis !== 'undefined') {
lenis = new Lenis({
duration: 1.25,
easing: function(t) { return Math.min(1, 1.001 - Math.pow(2, -10 * t)); },
direction: 'vertical',
gestureDirection: 'vertical',
smooth: true,
smoothTouch: false,
touchMultiplier: 2,
});
window.lenis = lenis;
// Sync GSAP ticker
if (typeof gsap !== 'undefined') {
gsap.ticker.add(function(time) { lenis.raf(time * 1000); });
gsap.ticker.lagSmoothing(0);
} else {
function rafLoop(time) {
lenis.raf(time);
requestAnimationFrame(rafLoop);
}
requestAnimationFrame(rafLoop);
}
}
/* ───────────────────────────────────────────────────────────────
3. GSAP RECIPE SHOWCASE STAGE
─────────────────────────────────────────────────────────────── */
<?php if (!empty($stageRecipes)): ?>
if (typeof gsap !== 'undefined' && typeof ScrollTrigger !== 'undefined') {
gsap.registerPlugin(ScrollTrigger);
// If lenis is active, feed its scroll to ScrollTrigger
if (lenis) {
lenis.on('scroll', ScrollTrigger.update);
}
var stageEl = document.getElementById('recipe-stage');
var introEl = document.getElementById('stageIntro');
var slidesEl = document.getElementById('stageSlides');
var progressEl = document.getElementById('stageProgress');
var dots = progressEl ? progressEl.querySelectorAll('.recipe-stage__progress-dot') : [];
var slides = slidesEl ? slidesEl.querySelectorAll('.recipe-stage__slide') : [];
var numSlides = slides.length;
if (stageEl && numSlides > 0) {
// Initial state: slides hidden (off-screen right), intro visible
gsap.set(slides, { xPercent: 110, opacity: 0 });
gsap.set(introEl, { opacity: 1, y: 0 });
// We express pin length in pixels
var pinLength = (numSlides + 1.5) * window.innerHeight;
// Segment boundaries (fractional 01) per slide for dot tracking
var introEnd = 1 / (numSlides + 1.5);
var segBoundaries = [];
for (var si = 0; si < numSlides; si++) {
segBoundaries.push({
start: introEnd + si * (1 - introEnd) / numSlides,
end: introEnd + (si + 1) * (1 - introEnd) / numSlides,
});
}
var stageTl = gsap.timeline({
scrollTrigger: {
trigger: stageEl,
start: 'top top',
end: '+=' + pinLength,
pin: true,
scrub: 1.2,
anticipatePin: 1,
onUpdate: function(self) {
// Update section indicator progress
updateIndicatorProgress(self.progress);
// Update progress dots based on scroll progress
var p = self.progress;
var activeIdx = -1;
for (var si = 0; si < segBoundaries.length; si++) {
var seg = segBoundaries[si];
var midpoint = (seg.start + seg.end) / 2;
// Slide is "active" from its enter midpoint to the next midpoint
var nextMid = si < segBoundaries.length - 1
? (segBoundaries[si].start + segBoundaries[si+1].end) / 2
: 1;
if (p >= seg.start && p < seg.end) {
activeIdx = si;
break;
}
}
if (activeIdx === -1 && p < introEnd) {
// Still in intro
} else if (activeIdx === -1) {
activeIdx = numSlides - 1;
}
dots.forEach(function(dot, di) {
dot.classList.toggle('active', di === activeIdx);
});
},
}
});
stageTl.to(introEl, {
opacity: 0,
y: -40,
duration: introEnd,
ease: 'power2.in',
}, 0);
// Phase per slide
slides.forEach(function(slide, idx) {
var segStart = segBoundaries[idx].start;
var segEnd = segBoundaries[idx].end;
var enterDur = (segEnd - segStart) * 0.28;
var exitDur = (segEnd - segStart) * 0.27;
var img = slide.querySelector('img');
// Enter: slide in from RIGHT
stageTl.fromTo(slide,
{ xPercent: 105, opacity: 0 },
{ xPercent: 0, opacity: 1, duration: enterDur, ease: 'power3.out' },
segStart
);
// Inner image parallax on enter
if (img) {
stageTl.fromTo(img,
{ scale: 1.12, xPercent: 8 },
{ scale: 1, xPercent: 0, duration: enterDur, ease: 'power2.out' },
segStart
);
}
// Exit: slide out to LEFT
stageTl.to(slide,
{ xPercent: -110, opacity: 0, duration: exitDur, ease: 'power3.in' },
segEnd - exitDur
);
if (img) {
stageTl.to(img,
{ scale: 1.08, xPercent: -6, duration: exitDur, ease: 'power2.in' },
segEnd - exitDur
);
}
});
}
}
<?php endif; ?>
/* ───────────────────────────────────────────────────────────────
4. INDEX SECTION INDICATOR
─────────────────────────────────────────────────────────────── */
var indicatorEl = document.getElementById('sectionIndicator');
var indicatorIndex = document.getElementById('indicatorIndex');
var indicatorFill = document.getElementById('indicatorLineFill');
var indicatorName = document.getElementById('indicatorName');
// Collect all sections with data-section-name
var sections = Array.from(document.querySelectorAll('[data-section-name]'));
function updateIndicatorFromScroll() {
var scrollY = window.scrollY;
var viewMid = scrollY + window.innerHeight * 0.4;
var active = null;
var activeIdx = 0;
sections.forEach(function(sec, idx) {
var top = sec.getBoundingClientRect().top + scrollY;
var bottom = top + sec.offsetHeight;
if (viewMid >= top && viewMid < bottom) {
active = sec;
activeIdx = idx;
}
});
if (!active && sections.length > 0) {
active = sections[0];
activeIdx = 0;
}
// Show / hide indicator
if (scrollY > 80) {
indicatorEl && indicatorEl.classList.add('visible');
} else {
indicatorEl && indicatorEl.classList.remove('visible');
}
// Update content
if (active && indicatorEl) {
var name = active.getAttribute('data-section-name') || '';
var numStr = String(activeIdx + 1).padStart(2, '0');
var pct = sections.length > 1
? Math.round((activeIdx / (sections.length - 1)) * 100)
: 100;
if (indicatorIndex) indicatorIndex.textContent = numStr;
if (indicatorName) indicatorName.textContent = name;
if (indicatorFill) indicatorFill.style.height = pct + '%';
}
}
// Global helper for GSAP onUpdate
window.updateIndicatorProgress = function(progress) {
if (indicatorFill) {
indicatorFill.style.height = Math.round(progress * 100) + '%';
}
};
// Listen
if (lenis) {
lenis.on('scroll', updateIndicatorFromScroll);
} else {
window.addEventListener('scroll', updateIndicatorFromScroll, { passive: true });
}
updateIndicatorFromScroll();
/* ───────────────────────────────────────────────────────────────
5. SCROLL REVEAL (for .reveal-target elements)
─────────────────────────────────────────────────────────────── */
if ('IntersectionObserver' in window) {
var revealObs = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
revealObs.unobserve(entry.target);
}
});
}, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' });
document.querySelectorAll('.reveal-target').forEach(function(el) {
revealObs.observe(el);
});
} else {
// Fallback: show all immediately
document.querySelectorAll('.reveal-target').forEach(function(el) {
el.classList.add('revealed');
});
}
/* ───────────────────────────────────────────────────────────────
6. LANDING CTA smooth scroll
─────────────────────────────────────────────────────────────── */
var landingCta = document.getElementById('landingCta');
if (landingCta) {
landingCta.addEventListener('click', function(e) {
e.preventDefault();
var target = document.getElementById('recipe-stage');
if (!target) target = document.getElementById('recipes-start');
if (!target) return;
if (window.lenis) {
window.lenis.scrollTo(target, { offset: 0, duration: 1.6 });
} else {
target.scrollIntoView({ behavior: 'smooth' });
}
});
}
})();
</script>