2026-03-22 22:16:44 +01:00
<? php
require __DIR__ . '/helpers.php' ;
2026-05-23 10:23:28 +02:00
try {
$allRecipes = load_recipes ();
} catch ( DatabaseUnavailableException $e ) {
handle_database_unavailable ( $e );
}
2026-03-22 22:16:44 +01:00
// Language & inputs
$lang = strtolower (( string ) ( filter_input ( INPUT_GET , 'lang' , FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?: 'en' )) === 'de' ? 'de' : 'en' ;
2026-05-21 11:52:40 +02:00
$q = trim (( string ) ( filter_input ( INPUT_GET , 'q' , FILTER_UNSAFE_RAW ) ?? '' ));
$tag = trim (( string ) ( filter_input ( INPUT_GET , 'tag' , FILTER_UNSAFE_RAW ) ?? '' ));
2026-03-22 22:16:44 +01:00
// Language copy
$copy = [
'en' => [
2026-05-21 11:52:40 +02:00
'home' => 'Home' ,
'latest' => 'Latest' ,
'basics' => 'Basics' ,
'eyebrow' => 'Seasonal · Unfussy · Ridiculously tasty.' ,
2026-03-22 22:16:44 +01:00
'search_placeholder' => 'Search for pasta, brunch, sauce...' ,
2026-05-21 11:52:40 +02:00
'search_button' => 'Search' ,
'tag_clear' => 'Clear' ,
'featured' => 'Featured' ,
'latest_drops' => 'Latest drops' ,
'count_suffix_default' => ' recipes ready to cook.' ,
2026-03-22 22:16:44 +01:00
'count_suffix_filtered' => ' recipes matching your filter.' ,
2026-05-21 11:52:40 +02:00
'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' ,
2026-03-22 22:16:44 +01:00
],
'de' => [
2026-05-21 11:52:40 +02:00
'home' => 'Start' ,
'latest' => 'Neueste' ,
'basics' => 'Basics' ,
'eyebrow' => 'Saisonal · Unkompliziert · Richtig lecker.' ,
2026-03-22 22:16:44 +01:00
'search_placeholder' => 'Suche nach Pasta, Brunch, Sauce...' ,
2026-05-21 11:52:40 +02:00
'search_button' => 'Suchen' ,
'tag_clear' => 'Zurücksetzen' ,
'featured' => 'Highlight' ,
'latest_drops' => 'Frisch dazugekommen' ,
'count_suffix_default' => ' Rezepte bereit zum Kochen.' ,
2026-03-22 22:16:44 +01:00
'count_suffix_filtered' => ' Rezepte passend zum Filter.' ,
2026-05-21 11:52:40 +02:00
'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' ,
2026-03-22 22:16:44 +01:00
],
];
$t = $copy [ $lang ];
2026-05-21 11:52:40 +02:00
// Data
$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' ])));
2026-03-22 22:16:44 +01:00
2026-05-21 11:52:40 +02:00
$comingSoon = array_values ( array_filter ( $localizedAll , fn ( $r ) => ! empty ( $r [ 'coming_soon' ])));
2026-03-22 22:16:44 +01:00
$placeholderTitle = $lang === 'de' ? 'Bald verfügbar' : 'Coming soon' ;
$comingSoonDisplay = array_pad ( $comingSoon , 5 , [
2026-05-21 11:52:40 +02:00
'hero' => '/assets/placeholder.svg' ,
2026-03-22 22:16:44 +01:00
'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 ) {
2026-05-21 11:52:40 +02:00
foreach ( $recipe [ 'tags' ] ?? [] as $tTag ) { $allTags [] = $tTag ; }
2026-03-22 22:16:44 +01:00
}
2026-05-21 11:52:40 +02:00
$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' ])));
2026-03-22 22:16:44 +01:00
$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' ,
];
2026-05-21 11:52:40 +02:00
// Meta
$pageTitle = $lang === 'de'
2026-03-22 22:16:44 +01:00
? 'FlixCooks | Moderner Foodblog für schnelle Alltagsküche'
: 'FlixCooks | Modern food blog for busy home cooks' ;
$description = $lang === 'de'
2026-05-21 11:52:40 +02:00
? 'Frische, schnelle Rezepte mit klaren Schritten und großem Geschmack.'
: 'Fresh, fast recipes with clear steps, smart prep notes, and big flavor.' ;
2026-03-22 22:16:44 +01:00
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN' ;
2026-05-21 11:52:40 +02:00
$langSwitchHref = lang_url ( $lang === 'de' ? 'en' : 'de' );
$navHome = $t [ 'home' ];
$navLatest = $t [ 'latest' ];
$navBasics = $t [ 'basics' ];
2026-03-22 22:16:44 +01:00
2026-05-21 11:52:40 +02:00
$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 );
2026-03-22 23:15:45 +01:00
2026-03-22 22:16:44 +01:00
// Small helpers for URLs
$langQuery = [ 'lang' => $lang ];
$recipeUrl = fn ( array $recipe ) => '/recipe.php?' . http_build_query ([ 'slug' => $recipe [ 'slug' ]] + $langQuery );
2026-05-21 11:52:40 +02:00
$tagUrl = fn ( string $tagValue ) => '/index.php?' . http_build_query ([ 'tag' => $tagValue ] + $langQuery );
2026-03-22 22:16:44 +01:00
include __DIR__ . '/partials/head.php' ;
?>
2026-05-21 11:52:40 +02:00
<!-- ╔══════════════════════════════════════════════════════════════╗
║ 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'; ?>
<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">
2026-03-22 22:16:44 +01:00
<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">
2026-05-21 11:52:40 +02:00
<p class="landing__eyebrow"><?php echo e($t['eyebrow']); ?></p>
2026-03-22 22:16:44 +01:00
<h1 class="landing__headline">
<?php if ($lang === 'de'): ?>
<span class="line line--1">Besser <em>kochen</em></span>
<span class="line line--2">unter der Woche.</span>
<span class="line line--3">Glänzen <em>am Wochenende.</em></span>
<?php else: ?>
<span class="line line--1">Cook <em>better</em></span>
<span class="line line--2">weeknights.</span>
<span class="line line--3">Brighter <em>weekends.</em></span>
<?php endif; ?>
</h1>
2026-05-21 11:52:40 +02:00
<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">
2026-03-22 22:16:44 +01:00
<span><?php echo $lang === 'de' ? 'Rezepte entdecken' : 'Explore recipes'; ?></span>
2026-05-21 11:52:40 +02:00
<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>
2026-03-22 22:16:44 +01:00
</a>
</div>
<div class="landing__scroll-nudge" aria-hidden="true">
<div class="scroll-line"></div>
2026-05-21 11:52:40 +02:00
<span><?php echo $lang === 'de' ? 'scrollen' : 'scroll'; ?></span>
2026-03-22 22:16:44 +01:00
</div>
</section>
2026-05-21 11:52:40 +02:00
<!-- ╔══════════════════════════════════════════════════════════════╗
║ 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'; ?>">
2026-03-22 22:16:44 +01:00
<?php if (!empty($comingSoon)): ?>
2026-05-21 11:52:40 +02:00
<div class="coming-strip reveal-target" aria-label="Coming soon recipes">
2026-03-22 22:16:44 +01:00
<div class="coming-strip__head">
2026-05-21 11:52:40 +02:00
<p class="pill pill--gold"><?php echo e($lang === 'de' ? 'Bald verfügbar' : 'Coming soon'); ?></p>
2026-03-22 22:16:44 +01:00
<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; ?>
2026-05-21 16:20:23 +02:00
<!-- Swipe Discovery Carousel -->
<section class="swipe-discovery reveal-target" id="swipe-discovery" style="display: none;" data-section-name="<?php echo $lang === 'de' ? 'Für Dich' : 'For You'; ?>" aria-label="<?php echo $lang === 'de' ? 'Persönliche Empfehlungen' : 'Personal Recommendations'; ?>">
<div class="section-header section-header--stack swipe-discovery__header">
<p class="eyebrow eyebrow-goal" id="swipeGoalEyebrow"></p>
<h2 id="swipeGoalTitle"><?php echo $lang === 'de' ? 'Empfohlen für dich' : 'Recommended for you'; ?></h2>
<p id="swipeGoalSub"><?php echo $lang === 'de' ? 'Basierend auf deinem Ernährungsziel' : 'Based on your dietary goal'; ?></p>
</div>
<div class="swipe-track" id="swipeTrack">
<!-- Cards inserted by JS -->
</div>
</section>
2026-05-21 11:52:40 +02:00
<!-- 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'; ?>">
2026-03-22 23:15:45 +01:00
<div class="discover-shell__main">
<div class="section-header section-header--stack">
2026-05-21 11:52:40 +02:00
<p class="eyebrow"><?php echo e($lang === 'de' ? 'Basics' : 'Basics'); ?></p>
2026-03-22 23:15:45 +01:00
<h2><?php echo e($t['discover_title']); ?></h2>
<p><?php echo e($t['discover_text']); ?></p>
</div>
2026-05-21 11:52:40 +02:00
<form class="search discover-search" method="get" action="/index.php" role="search">
2026-03-22 22:16:44 +01:00
<input type="hidden" name="lang" value="<?php echo e($lang); ?>">
2026-03-22 23:15:45 +01:00
<label class="sr-only" for="recipe-search"><?php echo e($t['search_placeholder']); ?></label>
2026-05-21 11:52:40 +02:00
<input id="recipe-search" type="text" name="q"
placeholder="<?php echo e($t['search_placeholder']); ?>"
value="<?php echo e($q); ?>">
2026-03-22 22:16:44 +01:00
<?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>
2026-03-22 23:15:45 +01:00
<div class="discover-stats" aria-label="Recipe overview">
<div class="discover-stat">
2026-05-21 11:52:40 +02:00
<strong><?php echo e((string)$recipeCount); ?></strong>
2026-03-22 23:15:45 +01:00
<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">
2026-05-21 11:52:40 +02:00
<strong><?php echo e((string)$tagCount); ?></strong>
2026-03-22 23:15:45 +01:00
<span><?php echo e($t['stats_tags']); ?></span>
</div>
2026-03-22 22:16:44 +01:00
</div>
2026-03-22 23:15:45 +01:00
<div class="tag-panel">
<div class="tag-panel__head">
<h3><?php echo e($t['browse_tags']); ?></h3>
<?php if ($activeTag): ?>
2026-03-23 10:52:13 +01:00
<a class="tag clear" href="/index.php?lang=<?php echo e($lang); ?>#basics"><?php echo e($t['tag_clear']); ?></a>
2026-03-22 23:15:45 +01:00
<?php endif; ?>
</div>
<div class="tag-row tag-row--panel">
<?php foreach ($allTags as $tTag): ?>
2026-05-21 11:52:40 +02:00
<a class="tag<?php echo strtolower($tTag) === strtolower($activeTag ?? '') ? ' active' : ''; ?>"
href="<?php echo e($tagUrl($tTag)); ?>#basics">#<?php echo e($tTag); ?></a>
2026-03-22 23:15:45 +01:00
<?php endforeach; ?>
2026-03-22 22:16:44 +01:00
</div>
</div>
</div>
2026-03-22 23:15:45 +01:00
<?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">
2026-05-21 11:52:40 +02:00
<p class="pill pill--gold"><?php echo e($t['featured']); ?></p>
2026-05-21 14:14:10 +02:00
<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>
2026-03-22 23:15:45 +01:00
<p><?php echo e($featured['description']); ?></p>
<div class="meta">
2026-05-21 11:52:40 +02:00
<span>⏱ <?php echo format_minutes((int)($featured['total_time'] ?? 0)); ?></span>
2026-03-22 23:15:45 +01:00
<span>🍽 <?php echo e($featured['servings']); ?> <?php echo e($t['servings']); ?></span>
</div>
2026-05-21 11:52:40 +02:00
<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>
2026-03-22 23:15:45 +01:00
</div>
</aside>
2026-03-22 22:16:44 +01:00
<?php endif; ?>
</section>
2026-05-21 11:52:40 +02:00
<!-- 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'; ?>">
2026-03-22 23:15:45 +01:00
<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>
2026-05-21 11:52:40 +02:00
<?php if (!empty($recipes)): ?>
2026-03-22 23:15:45 +01:00
<div class="grid">
2026-05-21 11:52:40 +02:00
<?php foreach ($recipes as $recipe): ?>
<article class="card reveal-target">
2026-03-22 23:15:45 +01:00
<a href="<?php echo e($recipeUrl($recipe)); ?>" class="card__image">
2026-05-21 11:52:40 +02:00
<img src="<?php echo e($recipe['hero']); ?>" alt="<?php echo e($recipe['title']); ?>" loading="lazy">
2026-03-22 23:15:45 +01:00
<?php if (!empty($recipe['category'])): ?>
<span class="pill pill--ghost"><?php echo e($recipe['category']); ?></span>
<?php endif; ?>
2026-05-21 14:14:10 +02:00
<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>
2026-03-22 23:15:45 +01:00
</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">
2026-05-21 11:52:40 +02:00
<span>⏱ <?php echo format_minutes((int)($recipe['total_time'] ?? 0)); ?></span>
2026-03-22 23:15:45 +01:00
<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>
2026-05-21 11:52:40 +02:00
<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>
2026-03-22 23:15:45 +01:00
</article>
<?php endif; ?>
</section>
2026-05-21 11:52:40 +02:00
<!-- Coming Features -->
<section class="coming-features reveal-target" id="coming-features" data-section-name="<?php echo $lang === 'de' ? 'Demnächst' : 'Soon'; ?>">
2026-03-22 23:15:45 +01:00
<div class="section-header section-header--stack">
2026-05-21 11:52:40 +02:00
<p class="eyebrow"><?php echo $lang === 'de' ? 'Roadmap' : 'Roadmap'; ?></p>
2026-03-22 22:16:44 +01:00
<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): ?>
2026-05-21 11:52:40 +02:00
<article class="feature-card reveal-target">
2026-03-22 22:16:44 +01:00
<div class="feature-icon" aria-hidden="true">✨</div>
<p><?php echo e($feature); ?></p>
</article>
<?php endforeach; ?>
</div>
</section>
</div><!-- /#recipes-start -->
2026-05-21 11:52:40 +02:00
2026-03-22 22:16:44 +01:00
<?php include __DIR__ . '/partials/footer.php'; ?>
2026-05-21 11:52:40 +02:00
2026-05-21 16:20:23 +02:00
<?php
// Expose recipe data for JS filtering
$recipesJson = json_encode(array_values(array_map(function($r) use ($lang) {
return [
'slug' => $r['slug'],
'title' => $r['title'],
'hero' => $r['hero'],
'difficulty' => $r['difficulty'],
'total_time' => (int)($r['total_time'] ?? 0),
'url' => '/recipe.php?slug=' . urlencode($r['slug']) . '&lang=' . $lang,
'tags' => $r['tags'] ?? [],
'category' => $r['category'] ?? ''
];
}, $localizedAll)));
?>
<script>
const indexRecipeBank = <?php echo $recipesJson; ?>;
const i18nGoal = {
'weight_loss': '<?php echo $lang === "de" ? "Abnehmen / Low-Carb" : "Weight Loss / Low-Carb"; ?>',
'muscle_gain': '<?php echo $lang === "de" ? "Muskelaufbau / High-Protein" : "Muscle Gain / High-Protein"; ?>',
'healthy': '<?php echo $lang === "de" ? "Gesund & Ausgewogen" : "Healthy & Balanced"; ?>'
};
</script>
2026-05-21 11:52:40 +02:00
<!-- ╔══════════════════════════════════════════════════════════════╗
║ 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);
}
}
2026-05-21 14:50:06 +02:00
if (sessionStorage.getItem('flixcooks_preloader_seen')) {
if (preEl && preEl.parentNode) preEl.parentNode.removeChild(preEl);
done = true;
} else {
sessionStorage.setItem('flixcooks_preloader_seen', 'true');
tickPreloader();
}
2026-05-21 11:52:40 +02:00
/* ───────────────────────────────────────────────────────────────
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 0– 1) 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' });
}
});
}
2026-05-21 16:20:23 +02:00
/* ───────────────────────────────────────────────────────────────
7. SWIPE DISCOVERY CAROUSEL
─────────────────────────────────────────────────────────────── */
var swipeSec = document.getElementById('swipe-discovery');
var swipeTrack = document.getElementById('swipeTrack');
var swipeEyebrow = document.getElementById('swipeGoalEyebrow');
function renderSwipeCarousel(recipes, goalLabel) {
if (!swipeSec || !swipeTrack || recipes.length === 0) return;
swipeSec.style.display = 'block';
if (goalLabel) {
swipeEyebrow.textContent = goalLabel;
} else {
swipeEyebrow.textContent = '<?php echo $lang === "de" ? "Entdecken" : "Discover"; ?>';
}
swipeTrack.innerHTML = '';
recipes.slice(0, 8).forEach(function(recipe) {
var card = document.createElement('article');
card.className = 'swipe-card';
card.innerHTML = `
<a href="${recipe.url}" class="swipe-card__image">
<img src="${recipe.hero}" alt="${recipe.title}" loading="lazy">
<button class="btn-favorite" data-slug="${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="swipe-card__body">
<div class="swipe-card__badge">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
<?php echo $lang === "de" ? "Für dich empfohlen" : "Recommended for you"; ?>
</div>
<h3><a href="${recipe.url}">${recipe.title}</a></h3>
<div class="meta">
<span>⏱ ${recipe.total_time} min</span>
<span>🙂 ${recipe.difficulty}</span>
</div>
</div>
`;
swipeTrack.appendChild(card);
});
// Re-bind favorite states using existing global userFavSlugs sync
if (typeof updateHeartStates === 'function') {
updateHeartStates();
}
// Add horizontal scroll mouse drag
let isDown = false;
let startX;
let scrollLeft;
swipeTrack.addEventListener('mousedown', (e) => {
isDown = true;
swipeTrack.classList.add('active');
startX = e.pageX - swipeTrack.offsetLeft;
scrollLeft = swipeTrack.scrollLeft;
});
swipeTrack.addEventListener('mouseleave', () => {
isDown = false;
swipeTrack.classList.remove('active');
});
swipeTrack.addEventListener('mouseup', () => {
isDown = false;
swipeTrack.classList.remove('active');
});
swipeTrack.addEventListener('mousemove', (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - swipeTrack.offsetLeft;
const walk = (x - startX) * 2;
swipeTrack.scrollLeft = scrollLeft - walk;
});
}
2026-05-23 10:23:28 +02:00
function filterRecipesByGoal(goal) {
var filtered = indexRecipeBank;
if (goal === 'weight_loss') {
filtered = indexRecipeBank.filter(function(r) {
return r.tags.some(function(t) {
var lower = t.toLowerCase();
return lower.includes('low-carb') || lower.includes('diet');
}) || (r.category && r.category.toLowerCase().includes('salad'));
2026-05-21 16:20:23 +02:00
});
2026-05-23 10:23:28 +02:00
} else if (goal === 'muscle_gain') {
filtered = indexRecipeBank.filter(function(r) {
return r.tags.some(function(t) {
var lower = t.toLowerCase();
return lower.includes('high-protein') || lower.includes('meat') || lower.includes('fleisch');
});
});
} else if (goal === 'healthy') {
filtered = indexRecipeBank.filter(function(r) {
return r.tags.some(function(t) {
var lower = t.toLowerCase();
return lower.includes('vegetarian') || lower.includes('healthy');
});
});
}
if (filtered.length === 0) filtered = indexRecipeBank;
return filtered;
}
window.addEventListener('DOMContentLoaded', function() {
var goal = window.fcLocal ? window.fcLocal.getGoal() : '';
if (goal) {
renderSwipeCarousel(filterRecipesByGoal(goal), i18nGoal[goal] || null);
2026-05-21 16:20:23 +02:00
} else {
renderSwipeCarousel(indexRecipeBank, null);
}
});
2026-05-21 11:52:40 +02:00
})();
</script>