feat(phase-1): Premium design system, CapitoliumPreloader, IndexSectionIndicator, Recipe Showcase Stage
- Full CSS rewrite: Warm Ivory/Gold/Steinkohle design tokens, Floema grid, easing library - AuraMarbleBackground: fixed grain texture layer - CapitoliumPreloader: fullscreen counter animation with slide-up exit - LiquidOverlayMenu: fixed hamburger → fullscreen clip-path overlay with SlideUpTextHover nav links - IndexSectionIndicator: fixed left-side section tracker with animated fill line - Recipe Showcase Stage: GSAP ScrollTrigger pinned stage (images enter from right, exit left) with parallax inner-image effect, intro title, progress dots, hint line - LenisSmoothScroll: CDN integration synced with GSAP ticker - CapitoliumRevealButton: scaleY fill reveal on hover for all CTAs - SlideUpTextHover: overlay nav links - UnderlineExpandHover: footer + overlay footer links - head.php: GSAP 3.12.5 + ScrollTrigger + Lenis 1.0.42 CDN; Playfair Display + Manrope fonts - recipe.php: updated to new design system (btn-reveal, pill--gold, Lenis init) - impressum.php / datenschutz.php: migrated to shared head.php partial
This commit is contained in:
+1262
-822
File diff suppressed because it is too large
Load Diff
+18
-31
@@ -1,15 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/helpers.php';
|
require __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
$lang = strtolower((string) (filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?: 'en')) === 'de' ? 'de' : 'en';
|
$lang = strtolower((string)(filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?: 'en')) === 'de' ? 'de' : 'en';
|
||||||
$settings = load_site_settings();
|
$settings = load_site_settings();
|
||||||
$privacy = $settings['privacy'][$lang] ?? ($settings['privacy']['en'] ?? []);
|
$privacy = $settings['privacy'][$lang] ?? ($settings['privacy']['en'] ?? []);
|
||||||
|
|
||||||
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
|
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
|
||||||
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
|
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
|
||||||
$navHome = $lang === 'de' ? 'Start' : 'Home';
|
|
||||||
$navLatest = $lang === 'de' ? 'Neueste' : 'Latest';
|
|
||||||
$navBasics = $lang === 'de' ? 'Basics' : 'Basics';
|
|
||||||
|
|
||||||
$t = [
|
$t = [
|
||||||
'title' => $lang === 'de' ? 'Datenschutzerklärung' : 'Privacy Policy',
|
'title' => $lang === 'de' ? 'Datenschutzerklärung' : 'Privacy Policy',
|
||||||
@@ -26,9 +23,7 @@ $t = [
|
|||||||
|
|
||||||
function privacy_item(string $label, ?string $value): void {
|
function privacy_item(string $label, ?string $value): void {
|
||||||
$value = trim((string) $value);
|
$value = trim((string) $value);
|
||||||
if ($value === '') {
|
if ($value === '') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="legal-item"><span class="legal-label">' . e($label) . '</span><span>' . nl2br(e($value)) . '</span></div>';
|
echo '<div class="legal-item"><span class="legal-label">' . e($label) . '</span><span>' . nl2br(e($value)) . '</span></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,29 +38,15 @@ if (!empty($privacy['log_retention_days'])) {
|
|||||||
: 'Server logs (IP address, timestamp, URL, user agent) are kept for security and deleted after ')
|
: 'Server logs (IP address, timestamp, URL, user agent) are kept for security and deleted after ')
|
||||||
. intval($privacy['log_retention_days']) . ($lang === 'de' ? ' Tagen gelöscht.' : ' days.');
|
. intval($privacy['log_retention_days']) . ($lang === 'de' ? ' Tagen gelöscht.' : ' days.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pageTitle = e($t['title']) . ' | FlixCooks';
|
||||||
|
$description = $lang === 'de'
|
||||||
|
? 'Informationen gemäß DSGVO und TKG über die Verarbeitung personenbezogener Daten.'
|
||||||
|
: 'Information per GDPR and TKG on how personal data is processed.';
|
||||||
|
|
||||||
|
include __DIR__ . '/partials/head.php';
|
||||||
|
include __DIR__ . '/partials/header.php';
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
|
||||||
<html lang="<?php echo e($lang); ?>">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title><?php echo e($t['title']); ?> | FlixCooks</title>
|
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
|
||||||
<style>
|
|
||||||
.legal-page { max-width: 900px; margin: 40px auto 80px; padding: 0 16px; }
|
|
||||||
.legal-card { background: var(--surface-2); border-radius: 16px; padding: 24px; box-shadow: var(--shadow); border: 1px solid rgba(255,255,255,0.08); }
|
|
||||||
.legal-item { display: grid; grid-template-columns: 220px 1fr; gap: 10px; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.08); }
|
|
||||||
.legal-item:last-child { border-bottom: none; }
|
|
||||||
.legal-label { font-weight: 700; color: var(--ink); }
|
|
||||||
.legal-section { margin-top: 22px; }
|
|
||||||
.legal-section h2 { margin: 0 0 8px; }
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.legal-item { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<?php include __DIR__ . '/partials/header.php'; ?>
|
|
||||||
<main class="legal-page">
|
<main class="legal-page">
|
||||||
<h1><?php echo e($t['title']); ?></h1>
|
<h1><?php echo e($t['title']); ?></h1>
|
||||||
<p><?php echo $lang === 'de'
|
<p><?php echo $lang === 'de'
|
||||||
@@ -88,5 +69,11 @@ if (!empty($privacy['log_retention_days'])) {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<?php include __DIR__ . '/partials/footer.php'; ?>
|
<?php include __DIR__ . '/partials/footer.php'; ?>
|
||||||
</body>
|
<script>
|
||||||
</html>
|
if (typeof Lenis !== 'undefined') {
|
||||||
|
var lenis = new Lenis({ duration: 1.2, smooth: true, smoothTouch: false });
|
||||||
|
window.lenis = lenis;
|
||||||
|
function raf(t) { lenis.raf(t); requestAnimationFrame(raf); }
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
+19
-32
@@ -1,15 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/helpers.php';
|
require __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
$lang = strtolower((string) (filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?: 'en')) === 'de' ? 'de' : 'en';
|
$lang = strtolower((string)(filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?: 'en')) === 'de' ? 'de' : 'en';
|
||||||
$settings = load_site_settings();
|
$settings = load_site_settings();
|
||||||
$imprint = $settings['imprint'][$lang] ?? ($settings['imprint']['en'] ?? []);
|
$imprint = $settings['imprint'][$lang] ?? ($settings['imprint']['en'] ?? []);
|
||||||
|
|
||||||
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
|
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
|
||||||
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
|
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
|
||||||
$navHome = $lang === 'de' ? 'Start' : 'Home';
|
|
||||||
$navLatest = $lang === 'de' ? 'Neueste' : 'Latest';
|
|
||||||
$navBasics = $lang === 'de' ? 'Basics' : 'Basics';
|
|
||||||
|
|
||||||
$t = [
|
$t = [
|
||||||
'title' => $lang === 'de' ? 'Impressum' : 'Imprint',
|
'title' => $lang === 'de' ? 'Impressum' : 'Imprint',
|
||||||
@@ -27,34 +24,18 @@ $t = [
|
|||||||
|
|
||||||
function legal_item(string $label, ?string $value): void {
|
function legal_item(string $label, ?string $value): void {
|
||||||
$value = trim((string) $value);
|
$value = trim((string) $value);
|
||||||
if ($value === '') {
|
if ($value === '') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="legal-item"><span class="legal-label">' . e($label) . '</span><span>' . nl2br(e($value)) . '</span></div>';
|
echo '<div class="legal-item"><span class="legal-label">' . e($label) . '</span><span>' . nl2br(e($value)) . '</span></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pageTitle = e($t['title']) . ' | FlixCooks';
|
||||||
|
$description = $lang === 'de'
|
||||||
|
? 'Pflichtangaben für diese Website gemäß ECG, MedienG und GewO.'
|
||||||
|
: 'Mandatory website information in line with Austrian ECG, Media Act and Trade Regulation.';
|
||||||
|
|
||||||
|
include __DIR__ . '/partials/head.php';
|
||||||
|
include __DIR__ . '/partials/header.php';
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
|
||||||
<html lang="<?php echo e($lang); ?>">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title><?php echo e($t['title']); ?> | FlixCooks</title>
|
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
|
||||||
<style>
|
|
||||||
.legal-page { max-width: 900px; margin: 40px auto 80px; padding: 0 16px; }
|
|
||||||
.legal-card { background: var(--surface-2); border-radius: 16px; padding: 24px; box-shadow: var(--shadow); border: 1px solid rgba(255,255,255,0.08); }
|
|
||||||
.legal-item { display: grid; grid-template-columns: 220px 1fr; gap: 10px; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.08); }
|
|
||||||
.legal-item:last-child { border-bottom: none; }
|
|
||||||
.legal-label { font-weight: 700; color: var(--ink); }
|
|
||||||
.legal-section { margin-top: 22px; }
|
|
||||||
.legal-section h2 { margin: 0 0 8px; }
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.legal-item { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<?php include __DIR__ . '/partials/header.php'; ?>
|
|
||||||
<main class="legal-page">
|
<main class="legal-page">
|
||||||
<h1><?php echo e($t['title']); ?></h1>
|
<h1><?php echo e($t['title']); ?></h1>
|
||||||
<p><?php echo $lang === 'de'
|
<p><?php echo $lang === 'de'
|
||||||
@@ -76,12 +57,18 @@ function legal_item(string $label, ?string $value): void {
|
|||||||
$odrText = $lang === 'de'
|
$odrText = $lang === 'de'
|
||||||
? 'Die EU-ODR-Plattform wurde am 20. Juli 2025 eingestellt. Informationen zur Verbraucherstreitbeilegung finden Sie hier:'
|
? 'Die EU-ODR-Plattform wurde am 20. Juli 2025 eingestellt. Informationen zur Verbraucherstreitbeilegung finden Sie hier:'
|
||||||
: 'The EU ODR platform was discontinued on 20 July 2025. Consumer dispute resolution information:';
|
: 'The EU ODR platform was discontinued on 20 July 2025. Consumer dispute resolution information:';
|
||||||
echo '<div class="legal-item"><span class="legal-label">' . e($t['odr']) . '</span><span>' . e($odrText) . ' <a href="' . e($imprint['odr']) . '" target="_blank" rel="noopener">consumer-redress.ec.europa.eu</a></span></div>';
|
echo '<div class="legal-item"><span class="legal-label">' . e($t['odr']) . '</span><span>' . e($odrText) . ' <a href="' . e($imprint['odr']) . '" target="_blank" rel="noopener" class="underline-link">consumer-redress.ec.europa.eu</a></span></div>';
|
||||||
}
|
}
|
||||||
legal_item($t['last_updated'], $imprint['last_updated'] ?? '');
|
legal_item($t['last_updated'], $imprint['last_updated'] ?? '');
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<?php include __DIR__ . '/partials/footer.php'; ?>
|
<?php include __DIR__ . '/partials/footer.php'; ?>
|
||||||
</body>
|
<script>
|
||||||
</html>
|
if (typeof Lenis !== 'undefined') {
|
||||||
|
var lenis = new Lenis({ duration: 1.2, smooth: true, smoothTouch: false });
|
||||||
|
window.lenis = lenis;
|
||||||
|
function raf(t) { lenis.raf(t); requestAnimationFrame(raf); }
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ $copy = [
|
|||||||
'home' => 'Home',
|
'home' => 'Home',
|
||||||
'latest' => 'Latest',
|
'latest' => 'Latest',
|
||||||
'basics' => 'Basics',
|
'basics' => 'Basics',
|
||||||
'eyebrow' => 'Seasonal, unfussy, ridiculously tasty.',
|
'eyebrow' => 'Seasonal · Unfussy · Ridiculously tasty.',
|
||||||
'headline' => 'Cook better <span>weeknights</span> and brighter <span>weekends</span>.',
|
|
||||||
'subhead' => 'Search for a craving, filter by vibe, or jump into this week\'s featured recipe.',
|
|
||||||
'search_placeholder' => 'Search for pasta, brunch, sauce...',
|
'search_placeholder' => 'Search for pasta, brunch, sauce...',
|
||||||
'search_button' => 'Search',
|
'search_button' => 'Search',
|
||||||
'tag_clear' => 'Clear',
|
'tag_clear' => 'Clear',
|
||||||
@@ -27,15 +25,30 @@ $copy = [
|
|||||||
'servings' => 'servings',
|
'servings' => 'servings',
|
||||||
'cook_it' => 'Cook it',
|
'cook_it' => 'Cook it',
|
||||||
'coming_features' => 'Coming soon',
|
'coming_features' => 'Coming soon',
|
||||||
'coming_features_sub' => 'Features I’m shipping next.',
|
'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' => [
|
'de' => [
|
||||||
'home' => 'Start',
|
'home' => 'Start',
|
||||||
'latest' => 'Neueste',
|
'latest' => 'Neueste',
|
||||||
'basics' => 'Basics',
|
'basics' => 'Basics',
|
||||||
'eyebrow' => 'Saisonal, unkompliziert, richtig lecker.',
|
'eyebrow' => 'Saisonal · Unkompliziert · Richtig lecker.',
|
||||||
'headline' => 'Besser kochen unter der Woche, glänzen am Wochenende.',
|
|
||||||
'subhead' => 'Suche nach einem Gericht, filtere nach Stimmung oder starte mit dem Highlight der Woche.',
|
|
||||||
'search_placeholder' => 'Suche nach Pasta, Brunch, Sauce...',
|
'search_placeholder' => 'Suche nach Pasta, Brunch, Sauce...',
|
||||||
'search_button' => 'Suchen',
|
'search_button' => 'Suchen',
|
||||||
'tag_clear' => 'Zurücksetzen',
|
'tag_clear' => 'Zurücksetzen',
|
||||||
@@ -48,15 +61,33 @@ $copy = [
|
|||||||
'servings' => 'Portionen',
|
'servings' => 'Portionen',
|
||||||
'cook_it' => 'Nachkochen',
|
'cook_it' => 'Nachkochen',
|
||||||
'coming_features' => 'Demnächst',
|
'coming_features' => 'Demnächst',
|
||||||
'coming_features_sub' => 'Features, an denen ich gerade baue.',
|
'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];
|
$t = $copy[$lang];
|
||||||
|
|
||||||
|
// Data
|
||||||
$allRecipes = load_recipes();
|
$allRecipes = load_recipes();
|
||||||
$localizedAll = localize_recipes($allRecipes, $lang);
|
$localizedAll = localize_recipes($allRecipes, $lang);
|
||||||
$recipes = filter_recipes($allRecipes, $q ?: null, $tag ?: null, $lang);
|
$recipes = filter_recipes($allRecipes, $q ?: null, $tag ?: null, $lang);
|
||||||
$recipes = array_values(array_filter($recipes, fn($r) => empty($r['coming_soon']))); // hide coming soon from main list
|
$recipes = array_values(array_filter($recipes, fn($r) => empty($r['coming_soon'])));
|
||||||
|
|
||||||
$comingSoon = array_values(array_filter($localizedAll, 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';
|
$placeholderTitle = $lang === 'de' ? 'Bald verfügbar' : 'Coming soon';
|
||||||
@@ -78,12 +109,13 @@ $featured = $featured ? localize_recipe($featured, $lang) : null;
|
|||||||
// Build tag cloud
|
// Build tag cloud
|
||||||
$allTags = [];
|
$allTags = [];
|
||||||
foreach ($localizedAll as $recipe) {
|
foreach ($localizedAll as $recipe) {
|
||||||
foreach ($recipe['tags'] ?? [] as $tTag) {
|
foreach ($recipe['tags'] ?? [] as $tTag) { $allTags[] = $tTag; }
|
||||||
$allTags[] = $tTag;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$allTags = array_values(array_unique($allTags));
|
$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'
|
$comingFeatures = $lang === 'de'
|
||||||
? [
|
? [
|
||||||
'Nährwerte & Rezepte Vorschlag individuell angepasst auf Ernährungsziel',
|
'Nährwerte & Rezepte Vorschlag individuell angepasst auf Ernährungsziel',
|
||||||
@@ -96,58 +128,25 @@ $comingFeatures = $lang === 'de'
|
|||||||
'Step-by-step recipe assistant with videos',
|
'Step-by-step recipe assistant with videos',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Meta
|
||||||
$pageTitle = $lang === 'de'
|
$pageTitle = $lang === 'de'
|
||||||
? 'FlixCooks | Moderner Foodblog für schnelle Alltagsküche'
|
? 'FlixCooks | Moderner Foodblog für schnelle Alltagsküche'
|
||||||
: 'FlixCooks | Modern food blog for busy home cooks';
|
: 'FlixCooks | Modern food blog for busy home cooks';
|
||||||
$description = $lang === 'de'
|
$description = $lang === 'de'
|
||||||
? 'Frische, schnelle Rezepte mit klaren Schritten und großem Geschmack. Gebaut für Hetzner Webhosting in einfachem PHP.'
|
? 'Frische, schnelle Rezepte mit klaren Schritten und großem Geschmack.'
|
||||||
: 'Fresh, fast recipes with clear steps, smart prep notes, and big flavor. Built for Hetzner web hosting in plain PHP.';
|
: 'Fresh, fast recipes with clear steps, smart prep notes, and big flavor.';
|
||||||
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
|
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
|
||||||
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
|
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
|
||||||
$navHome = $t['home'];
|
$navHome = $t['home'];
|
||||||
$navLatest = $t['latest'];
|
$navLatest = $t['latest'];
|
||||||
$navBasics = $t['basics'];
|
$navBasics = $t['basics'];
|
||||||
|
|
||||||
$quickStartRecipes = array_slice($recipes, 0, 3);
|
|
||||||
$latestRecipes = array_slice($recipes, 0, 6);
|
|
||||||
$recipeCount = count($recipes);
|
$recipeCount = count($recipes);
|
||||||
$totalMinutes = array_sum(array_map(fn($recipe) => (int) ($recipe['total_time'] ?? 0), $recipes));
|
$totalMinutes = array_sum(array_map(fn($r) => (int)($r['total_time'] ?? 0), $recipes));
|
||||||
$averageMinutes = $recipeCount > 0 ? (int) ceil($totalMinutes / $recipeCount) : 0;
|
$averageMinutes = $recipeCount > 0 ? (int)ceil($totalMinutes / $recipeCount) : 0;
|
||||||
$activeTag = $tag !== '' ? $tag : null;
|
$activeTag = $tag !== '' ? $tag : null;
|
||||||
$tagCount = count($allTags);
|
$tagCount = count($allTags);
|
||||||
|
|
||||||
$copy['en'] += [
|
|
||||||
'discover_title' => 'Find your next easy favorite.',
|
|
||||||
'discover_text' => 'Everything below the landing area is simplified for smaller screens: search, tap a tag, and jump into a recipe without cramped text blocks.',
|
|
||||||
'browse_tags' => 'Browse tags',
|
|
||||||
'quick_start_title' => 'Quick start',
|
|
||||||
'quick_start_text' => 'Open one of the recipes with the shortest path from craving to plate.',
|
|
||||||
'library_title' => 'Recipe library',
|
|
||||||
'library_text' => 'Clean cards, shorter descriptions, and tap-friendly actions for mobile browsing.',
|
|
||||||
'stats_recipes' => 'live recipes',
|
|
||||||
'stats_average' => 'avg. total time',
|
|
||||||
'stats_tags' => 'browseable tags',
|
|
||||||
'empty_title' => 'No recipes found yet',
|
|
||||||
'empty_text' => 'Try another search term or clear the active tag to see every recipe again.',
|
|
||||||
'view_recipe' => 'View recipe',
|
|
||||||
];
|
|
||||||
$copy['de'] += [
|
|
||||||
'discover_title' => 'Finde schnell dein nächstes Lieblingsrezept.',
|
|
||||||
'discover_text' => 'Alles unterhalb der Landingpage ist für kleine Screens vereinfacht: suchen, Tag antippen und ohne gequetschte Textblöcke ins Rezept springen.',
|
|
||||||
'browse_tags' => 'Tags entdecken',
|
|
||||||
'quick_start_title' => 'Schnell starten',
|
|
||||||
'quick_start_text' => 'Öffne eines der Rezepte mit dem kürzesten Weg von Appetit zu Teller.',
|
|
||||||
'library_title' => 'Rezeptübersicht',
|
|
||||||
'library_text' => 'Saubere Karten, kürzere Beschreibungen und gut tappbare Aktionen für Mobile.',
|
|
||||||
'stats_recipes' => 'Rezepte online',
|
|
||||||
'stats_average' => 'Ø Gesamtzeit',
|
|
||||||
'stats_tags' => 'durchsuchbare Tags',
|
|
||||||
'empty_title' => 'Noch keine Rezepte gefunden',
|
|
||||||
'empty_text' => 'Probiere einen anderen Suchbegriff oder entferne den aktiven Tag, um wieder alle Rezepte zu sehen.',
|
|
||||||
'view_recipe' => 'Rezept ansehen',
|
|
||||||
];
|
|
||||||
$t = $copy[$lang];
|
|
||||||
|
|
||||||
// Small helpers for URLs
|
// Small helpers for URLs
|
||||||
$langQuery = ['lang' => $lang];
|
$langQuery = ['lang' => $lang];
|
||||||
$recipeUrl = fn(array $recipe) => '/recipe.php?' . http_build_query(['slug' => $recipe['slug']] + $langQuery);
|
$recipeUrl = fn(array $recipe) => '/recipe.php?' . http_build_query(['slug' => $recipe['slug']] + $langQuery);
|
||||||
@@ -156,8 +155,39 @@ $tagUrl = fn(string $tagValue) => '/index.php?' . http_build_query(['tag' => $ta
|
|||||||
include __DIR__ . '/partials/head.php';
|
include __DIR__ . '/partials/head.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php // ── LANDING SECTION ─────────────────────────────────────────────────── ?>
|
<!-- ╔══════════════════════════════════════════════════════════════╗
|
||||||
<section class="landing" id="landing" aria-label="Landing">
|
║ 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">
|
||||||
<div class="landing__blobs" aria-hidden="true">
|
<div class="landing__blobs" aria-hidden="true">
|
||||||
<div class="blob blob--a"></div>
|
<div class="blob blob--a"></div>
|
||||||
<div class="blob blob--b"></div>
|
<div class="blob blob--b"></div>
|
||||||
@@ -182,11 +212,7 @@ include __DIR__ . '/partials/head.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="landing__copy">
|
<div class="landing__copy">
|
||||||
<p class="landing__eyebrow">
|
<p class="landing__eyebrow"><?php echo e($t['eyebrow']); ?></p>
|
||||||
<?php echo $lang === 'de'
|
|
||||||
? 'Saisonal · Unkompliziert · Richtig lecker'
|
|
||||||
: 'Seasonal · Unfussy · Ridiculously tasty'; ?>
|
|
||||||
</p>
|
|
||||||
<h1 class="landing__headline">
|
<h1 class="landing__headline">
|
||||||
<?php if ($lang === 'de'): ?>
|
<?php if ($lang === 'de'): ?>
|
||||||
<span class="line line--1">Besser <em>kochen</em></span>
|
<span class="line line--1">Besser <em>kochen</em></span>
|
||||||
@@ -198,30 +224,108 @@ include __DIR__ . '/partials/head.php';
|
|||||||
<span class="line line--3">Brighter <em>weekends.</em></span>
|
<span class="line line--3">Brighter <em>weekends.</em></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="landing__sub"><?php echo e($t['subhead']); ?></p>
|
<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="#recipes-start" class="landing__cta" id="landingCta">
|
<a href="#recipe-stage" class="landing__cta" id="landingCta">
|
||||||
<span><?php echo $lang === 'de' ? 'Rezepte entdecken' : 'Explore recipes'; ?></span>
|
<span><?php echo $lang === 'de' ? 'Rezepte entdecken' : 'Explore recipes'; ?></span>
|
||||||
<svg width="18" height="18" 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>
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="landing__scroll-nudge" aria-hidden="true">
|
<div class="landing__scroll-nudge" aria-hidden="true">
|
||||||
<div class="scroll-line"></div>
|
<div class="scroll-line"></div>
|
||||||
<span>scroll</span>
|
<span><?php echo $lang === 'de' ? 'scrollen' : 'scroll'; ?></span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<?php include __DIR__ . '/partials/header.php'; ?>
|
|
||||||
<div id="recipes-start">
|
|
||||||
<script>
|
<!-- ╔══════════════════════════════════════════════════════════════╗
|
||||||
document.getElementById('landingCta').addEventListener('click', function(e){
|
║ RECIPE SHOWCASE STAGE (GSAP pinned ScrollTrigger) ║
|
||||||
e.preventDefault();
|
╚══════════════════════════════════════════════════════════════╝ -->
|
||||||
document.getElementById('recipes-start').scrollIntoView({ behavior: 'smooth' });
|
<?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'; ?>">
|
||||||
</script>
|
|
||||||
|
<!-- 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)): ?>
|
<?php if (!empty($comingSoon)): ?>
|
||||||
<div class="coming-strip" aria-label="Coming soon recipes">
|
<div class="coming-strip reveal-target" aria-label="Coming soon recipes">
|
||||||
<div class="coming-strip__head">
|
<div class="coming-strip__head">
|
||||||
<p class="pill pill--ghost"><?php echo e($lang === 'de' ? 'Bald verfügbar' : 'Coming soon'); ?></p>
|
<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>
|
<small><?php echo count($comingSoon); ?> <?php echo $lang === 'de' ? 'in Arbeit' : 'in progress'; ?></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="coming-strip__row">
|
<div class="coming-strip__row">
|
||||||
@@ -237,18 +341,21 @@ document.getElementById('landingCta').addEventListener('click', function(e){
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<section class="discover-shell" id="basics">
|
<!-- 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="discover-shell__main">
|
||||||
<div class="section-header section-header--stack">
|
<div class="section-header section-header--stack">
|
||||||
<p class="eyebrow"><?php echo e($t['basics']); ?></p>
|
<p class="eyebrow"><?php echo e($lang === 'de' ? 'Basics' : 'Basics'); ?></p>
|
||||||
<h2><?php echo e($t['discover_title']); ?></h2>
|
<h2><?php echo e($t['discover_title']); ?></h2>
|
||||||
<p><?php echo e($t['discover_text']); ?></p>
|
<p><?php echo e($t['discover_text']); ?></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="search discover-search" method="get" action="/index.php">
|
<form class="search discover-search" method="get" action="/index.php" role="search">
|
||||||
<input type="hidden" name="lang" value="<?php echo e($lang); ?>">
|
<input type="hidden" name="lang" value="<?php echo e($lang); ?>">
|
||||||
<label class="sr-only" for="recipe-search"><?php echo e($t['search_placeholder']); ?></label>
|
<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); ?>">
|
<input id="recipe-search" type="text" name="q"
|
||||||
|
placeholder="<?php echo e($t['search_placeholder']); ?>"
|
||||||
|
value="<?php echo e($q); ?>">
|
||||||
<?php if ($tag): ?>
|
<?php if ($tag): ?>
|
||||||
<input type="hidden" name="tag" value="<?php echo e($tag); ?>">
|
<input type="hidden" name="tag" value="<?php echo e($tag); ?>">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -257,7 +364,7 @@ document.getElementById('landingCta').addEventListener('click', function(e){
|
|||||||
|
|
||||||
<div class="discover-stats" aria-label="Recipe overview">
|
<div class="discover-stats" aria-label="Recipe overview">
|
||||||
<div class="discover-stat">
|
<div class="discover-stat">
|
||||||
<strong><?php echo e((string) $recipeCount); ?></strong>
|
<strong><?php echo e((string)$recipeCount); ?></strong>
|
||||||
<span><?php echo e($t['stats_recipes']); ?></span>
|
<span><?php echo e($t['stats_recipes']); ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="discover-stat">
|
<div class="discover-stat">
|
||||||
@@ -265,7 +372,7 @@ document.getElementById('landingCta').addEventListener('click', function(e){
|
|||||||
<span><?php echo e($t['stats_average']); ?></span>
|
<span><?php echo e($t['stats_average']); ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="discover-stat">
|
<div class="discover-stat">
|
||||||
<strong><?php echo e((string) $tagCount); ?></strong>
|
<strong><?php echo e((string)$tagCount); ?></strong>
|
||||||
<span><?php echo e($t['stats_tags']); ?></span>
|
<span><?php echo e($t['stats_tags']); ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -279,7 +386,8 @@ document.getElementById('landingCta').addEventListener('click', function(e){
|
|||||||
</div>
|
</div>
|
||||||
<div class="tag-row tag-row--panel">
|
<div class="tag-row tag-row--panel">
|
||||||
<?php foreach ($allTags as $tTag): ?>
|
<?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>
|
<a class="tag<?php echo strtolower($tTag) === strtolower($activeTag ?? '') ? ' active' : ''; ?>"
|
||||||
|
href="<?php echo e($tagUrl($tTag)); ?>#basics">#<?php echo e($tTag); ?></a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,31 +397,36 @@ document.getElementById('landingCta').addEventListener('click', function(e){
|
|||||||
<aside class="discover-shell__feature hero-card">
|
<aside class="discover-shell__feature hero-card">
|
||||||
<img src="<?php echo e($featured['hero']); ?>" alt="<?php echo e($featured['title']); ?>">
|
<img src="<?php echo e($featured['hero']); ?>" alt="<?php echo e($featured['title']); ?>">
|
||||||
<div class="hero-card__body">
|
<div class="hero-card__body">
|
||||||
<p class="pill"><?php echo e($t['featured']); ?></p>
|
<p class="pill pill--gold"><?php echo e($t['featured']); ?></p>
|
||||||
<h3><?php echo e($featured['title']); ?></h3>
|
<h3><?php echo e($featured['title']); ?></h3>
|
||||||
<p><?php echo e($featured['description']); ?></p>
|
<p><?php echo e($featured['description']); ?></p>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span>⏱ <?php echo format_minutes((int) ($featured['total_time'] ?? 0)); ?></span>
|
<span>⏱ <?php echo format_minutes((int)($featured['total_time'] ?? 0)); ?></span>
|
||||||
<span>🍽 <?php echo e($featured['servings']); ?> <?php echo e($t['servings']); ?></span>
|
<span>🍽 <?php echo e($featured['servings']); ?> <?php echo e($t['servings']); ?></span>
|
||||||
</div>
|
</div>
|
||||||
<a class="button" href="<?php echo e($recipeUrl($featured)); ?>"><?php echo e($t['cook_it']); ?></a>
|
<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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="latest" id="latest">
|
|
||||||
|
<!-- 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">
|
<div class="section-header section-header--stack">
|
||||||
<h2><?php echo e($t['library_title']); ?></h2>
|
<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>
|
<p><?php echo $recipeCount; ?><?php echo $q || $tag ? $t['count_suffix_filtered'] : $t['count_suffix_default']; ?></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($latestRecipes)): ?>
|
<?php if (!empty($recipes)): ?>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<?php foreach ($latestRecipes as $recipe): ?>
|
<?php foreach ($recipes as $recipe): ?>
|
||||||
<article class="card">
|
<article class="card reveal-target">
|
||||||
<a href="<?php echo e($recipeUrl($recipe)); ?>" class="card__image">
|
<a href="<?php echo e($recipeUrl($recipe)); ?>" class="card__image">
|
||||||
<img src="<?php echo e($recipe['hero']); ?>" alt="<?php echo e($recipe['title']); ?>">
|
<img src="<?php echo e($recipe['hero']); ?>" alt="<?php echo e($recipe['title']); ?>" loading="lazy">
|
||||||
<?php if (!empty($recipe['category'])): ?>
|
<?php if (!empty($recipe['category'])): ?>
|
||||||
<span class="pill pill--ghost"><?php echo e($recipe['category']); ?></span>
|
<span class="pill pill--ghost"><?php echo e($recipe['category']); ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -322,7 +435,7 @@ document.getElementById('landingCta').addEventListener('click', function(e){
|
|||||||
<h3><a href="<?php echo e($recipeUrl($recipe)); ?>"><?php echo e($recipe['title']); ?></a></h3>
|
<h3><a href="<?php echo e($recipeUrl($recipe)); ?>"><?php echo e($recipe['title']); ?></a></h3>
|
||||||
<p><?php echo e($recipe['description']); ?></p>
|
<p><?php echo e($recipe['description']); ?></p>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span>⏱ <?php echo format_minutes((int) ($recipe['total_time'] ?? 0)); ?></span>
|
<span>⏱ <?php echo format_minutes((int)($recipe['total_time'] ?? 0)); ?></span>
|
||||||
<span>🙂 <?php echo e($recipe['difficulty']); ?></span>
|
<span>🙂 <?php echo e($recipe['difficulty']); ?></span>
|
||||||
</div>
|
</div>
|
||||||
<?php if (!empty($recipe['tags'])): ?>
|
<?php if (!empty($recipe['tags'])): ?>
|
||||||
@@ -340,19 +453,25 @@ document.getElementById('landingCta').addEventListener('click', function(e){
|
|||||||
<article class="empty-state">
|
<article class="empty-state">
|
||||||
<h3><?php echo e($t['empty_title']); ?></h3>
|
<h3><?php echo e($t['empty_title']); ?></h3>
|
||||||
<p><?php echo e($t['empty_text']); ?></p>
|
<p><?php echo e($t['empty_text']); ?></p>
|
||||||
<a class="button" href="/index.php?lang=<?php echo e($lang); ?>"><?php echo e($t['tag_clear']); ?></a>
|
<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>
|
</article>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="coming-features" id="coming-features">
|
|
||||||
|
<!-- 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">
|
<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>
|
<h2><?php echo e($t['coming_features']); ?></h2>
|
||||||
<p><?php echo e($t['coming_features_sub']); ?></p>
|
<p><?php echo e($t['coming_features_sub']); ?></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-grid">
|
<div class="feature-grid">
|
||||||
<?php foreach ($comingFeatures as $feature): ?>
|
<?php foreach ($comingFeatures as $feature): ?>
|
||||||
<article class="feature-card">
|
<article class="feature-card reveal-target">
|
||||||
<div class="feature-icon" aria-hidden="true">✨</div>
|
<div class="feature-icon" aria-hidden="true">✨</div>
|
||||||
<p><?php echo e($feature); ?></p>
|
<p><?php echo e($feature); ?></p>
|
||||||
</article>
|
</article>
|
||||||
@@ -361,4 +480,314 @@ document.getElementById('landingCta').addEventListener('click', function(e){
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div><!-- /#recipes-start -->
|
</div><!-- /#recipes-start -->
|
||||||
|
|
||||||
<?php include __DIR__ . '/partials/footer.php'; ?>
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|||||||
+32
-33
@@ -4,76 +4,75 @@
|
|||||||
$settings = function_exists('load_site_settings') ? load_site_settings() : null;
|
$settings = function_exists('load_site_settings') ? load_site_settings() : null;
|
||||||
$contactEmail = $settings['imprint'][$langCode]['email'] ?? 'contact@flixcooks.at';
|
$contactEmail = $settings['imprint'][$langCode]['email'] ?? 'contact@flixcooks.at';
|
||||||
?>
|
?>
|
||||||
<footer class="site-footer">
|
<footer class="site-footer" role="contentinfo">
|
||||||
<div class="footer-meta">
|
<div class="footer-meta">
|
||||||
<p>Website designed by Felix Brockers</p>
|
<p><?php echo $langCode === 'de' ? 'Designed & entwickelt von Felix Brockers' : 'Designed & built by Felix Brockers'; ?></p>
|
||||||
<a class="contact-link" href="mailto:<?php echo e($contactEmail); ?>">Contact me</a>
|
<a class="contact-link underline-link" href="mailto:<?php echo e($contactEmail); ?>">
|
||||||
|
<?php echo e($contactEmail); ?>
|
||||||
|
</a>
|
||||||
<small>© <?php echo date('Y'); ?> FlixCooks</small>
|
<small>© <?php echo date('Y'); ?> FlixCooks</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/impressum.php<?php echo $langSuffix; ?>"><?php echo $langCode === 'de' ? 'Impressum' : 'Imprint'; ?></a>
|
<a class="underline-link" href="/impressum.php<?php echo $langSuffix; ?>">
|
||||||
<a href="/datenschutz.php<?php echo $langSuffix; ?>"><?php echo $langCode === 'de' ? 'Datenschutz' : 'Privacy'; ?></a>
|
<?php echo $langCode === 'de' ? 'Impressum' : 'Imprint'; ?>
|
||||||
|
</a>
|
||||||
|
<a class="underline-link" href="/datenschutz.php<?php echo $langSuffix; ?>">
|
||||||
|
<?php echo $langCode === 'de' ? 'Datenschutz' : 'Privacy'; ?>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<button id="backToTop" aria-label="Back to top">↑ Back to top</button>
|
<button id="backToTop" aria-label="<?php echo $langCode === 'de' ? 'Nach oben' : 'Back to top'; ?>">↑</button>
|
||||||
|
|
||||||
<!-- Fixed dark/light toggle — always visible, scrolls with page -->
|
<!-- Fixed dark/light toggle -->
|
||||||
<button class="theme-toggle" data-theme-toggle aria-label="Switch to dark mode">
|
<button class="theme-toggle" data-theme-toggle aria-label="Switch to dark mode">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ── Back to top ───────────────────────────────────────────────────────────────
|
// ── Back to top ────────────────────────────────────────────────
|
||||||
(function(){
|
(function(){
|
||||||
const btn = document.getElementById('backToTop');
|
var btn = document.getElementById('backToTop');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
const check = () => btn.classList.toggle('show', window.scrollY > 240);
|
var check = function() { btn.classList.toggle('show', window.scrollY > 280); };
|
||||||
window.addEventListener('scroll', check, { passive: true });
|
window.addEventListener('scroll', check, { passive: true });
|
||||||
btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
btn.addEventListener('click', function() {
|
||||||
|
if (window.lenis) {
|
||||||
|
window.lenis.scrollTo(0, { duration: 1.4 });
|
||||||
|
} else {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
});
|
||||||
check();
|
check();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ── Dark / light mode toggle ─────────────────────────────────────────────────
|
// ── Dark / light mode toggle ───────────────────────────────────
|
||||||
(function(){
|
(function(){
|
||||||
const r = document.documentElement;
|
var r = document.documentElement;
|
||||||
let d = r.getAttribute('data-theme') || 'light';
|
var d = localStorage.getItem('fc-theme') || r.getAttribute('data-theme') || 'light';
|
||||||
r.setAttribute('data-theme', d);
|
r.setAttribute('data-theme', d);
|
||||||
|
|
||||||
const SUN = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
|
var SUN = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
|
||||||
const MOON = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
var MOON = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
||||||
|
|
||||||
function setIcon() {
|
function setIcon() {
|
||||||
const icon = d === 'dark' ? SUN : MOON;
|
var icon = d === 'dark' ? SUN : MOON;
|
||||||
const label = 'Switch to ' + (d === 'dark' ? 'light' : 'dark') + ' mode';
|
var label = 'Switch to ' + (d === 'dark' ? 'light' : 'dark') + ' mode';
|
||||||
document.querySelectorAll('[data-theme-toggle]').forEach(t => {
|
document.querySelectorAll('[data-theme-toggle]').forEach(function(t) {
|
||||||
t.innerHTML = icon;
|
t.innerHTML = icon;
|
||||||
t.setAttribute('aria-label', label);
|
t.setAttribute('aria-label', label);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use event delegation so it works regardless of DOM order
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
if (e.target.closest('[data-theme-toggle]')) {
|
if (e.target.closest('[data-theme-toggle]')) {
|
||||||
d = d === 'dark' ? 'light' : 'dark';
|
d = d === 'dark' ? 'light' : 'dark';
|
||||||
r.setAttribute('data-theme', d);
|
r.setAttribute('data-theme', d);
|
||||||
|
localStorage.setItem('fc-theme', d);
|
||||||
setIcon();
|
setIcon();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle adapts visually when scrolled off landing section
|
|
||||||
(function(){
|
|
||||||
const landing = document.getElementById('landing');
|
|
||||||
const toggle = document.querySelector('.theme-toggle');
|
|
||||||
if (!toggle) return;
|
|
||||||
function check() {
|
|
||||||
const past = !landing || window.scrollY > (landing.offsetHeight - 80);
|
|
||||||
toggle.classList.toggle('on-site', past);
|
|
||||||
}
|
|
||||||
window.addEventListener('scroll', check, { passive: true });
|
|
||||||
check();
|
|
||||||
})();
|
|
||||||
|
|
||||||
setIcon();
|
setIcon();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+26
-3
@@ -11,11 +11,34 @@ $description = $description ?? 'Seasonal recipes, tested tips, and approachable
|
|||||||
<title><?php echo e($pageTitle); ?></title>
|
<title><?php echo e($pageTitle); ?></title>
|
||||||
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
||||||
<link rel="alternate icon" type="image/x-icon" sizes="16x16 32x32" href="/favicon.ico">
|
<link rel="alternate icon" type="image/x-icon" sizes="16x16 32x32" href="/favicon.ico">
|
||||||
|
|
||||||
|
<!-- Google Fonts: Playfair Display + Manrope -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,500;0,600;0,700;1,500;1,600&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Manrope:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Main stylesheet -->
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
<!-- Always start light -->
|
|
||||||
<script>document.documentElement.setAttribute('data-theme','light');</script>
|
<!-- GSAP + ScrollTrigger (CDN) -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Lenis smooth scroll (CDN) -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@studio-freight/lenis@1.0.42/bundled/lenis.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Set theme instantly to avoid flash -->
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var t = localStorage.getItem('fc-theme') || 'light';
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
<!-- ── AuraMarbleBackground (fixed atmospheric grain layer) ── -->
|
||||||
|
<div class="bg-marble-container" aria-hidden="true">
|
||||||
|
<div class="bg-marble-grain"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|||||||
+108
-27
@@ -1,32 +1,113 @@
|
|||||||
<header class="site-header">
|
<?php
|
||||||
<div class="site-header__top">
|
// Determine current page for active nav state
|
||||||
<a class="logo" href="/index.php<?php echo $lang === 'de' ? '?lang=de' : ''; ?>">
|
$currentPage = basename($_SERVER['PHP_SELF'] ?? 'index.php');
|
||||||
<img class="logo--light" src="/assets/logo-white.png" alt="FlixCooks">
|
$langParam = isset($lang) && $lang === 'de' ? '?lang=de' : '';
|
||||||
<img class="logo--dark" src="/assets/logo-black.png" alt="FlixCooks">
|
$langParamAmp = isset($lang) && $lang === 'de' ? '&lang=de' : '';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- ── LiquidOverlayMenu ─────────────────────────────────────── -->
|
||||||
|
|
||||||
|
<!-- Fixed Logo (top-left) -->
|
||||||
|
<a class="nav-logo" href="/index.php<?php echo $langParam; ?>" aria-label="FlixCooks – Home">
|
||||||
|
<img class="logo--light" src="/assets/logo-black.png" alt="FlixCooks">
|
||||||
|
<img class="logo--dark" src="/assets/logo-white.png" alt="FlixCooks">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Fixed Hamburger Button (top-right) -->
|
||||||
|
<button class="nav-trigger" id="navMenuBtn" type="button" aria-label="<?php echo isset($lang) && $lang === 'de' ? 'Menü öffnen' : 'Open menu'; ?>" aria-expanded="false" aria-controls="navOverlay">
|
||||||
|
<span class="nav-burger-lines" aria-hidden="true">
|
||||||
|
<span class="nav-line nav-line--top"></span>
|
||||||
|
<span class="nav-line nav-line--mid"></span>
|
||||||
|
<span class="nav-line nav-line--bot"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Full-screen Overlay -->
|
||||||
|
<div class="nav-overlay" id="navOverlay" aria-hidden="true" role="dialog" aria-label="Navigation">
|
||||||
|
<div class="nav-overlay-inner">
|
||||||
|
<nav>
|
||||||
|
<ul class="nav-menu-list" role="list">
|
||||||
|
<li>
|
||||||
|
<a href="/index.php<?php echo $langParam; ?>" class="slide-hover-link" <?php echo $currentPage === 'index.php' ? 'aria-current="page"' : ''; ?>>
|
||||||
|
<span class="slide-hover-wrapper">
|
||||||
|
<span class="slide-text-main"><?php echo isset($lang) && $lang === 'de' ? 'Startseite' : 'Home'; ?></span>
|
||||||
|
<span class="slide-text-clone" aria-hidden="true"><?php echo isset($lang) && $lang === 'de' ? 'Startseite' : 'Home'; ?></span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="header-actions">
|
</li>
|
||||||
<a class="lang-toggle" href="<?php echo e($langSwitchHref ?? '#'); ?>"><?php echo e($langSwitchLabel ?? 'DE/EN'); ?></a>
|
<li>
|
||||||
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="siteNav">
|
<a href="/index.php<?php echo $langParam; ?>#recipes-start" class="slide-hover-link">
|
||||||
<span class="menu-toggle__icon" aria-hidden="true">☰</span>
|
<span class="slide-hover-wrapper">
|
||||||
<span class="menu-toggle__label"><?php echo $lang === 'de' ? 'Menü' : 'Menu'; ?></span>
|
<span class="slide-text-main"><?php echo isset($lang) && $lang === 'de' ? 'Rezepte' : 'Recipes'; ?></span>
|
||||||
</button>
|
<span class="slide-text-clone" aria-hidden="true"><?php echo isset($lang) && $lang === 'de' ? 'Rezepte' : 'Recipes'; ?></span>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
<nav id="siteNav" class="site-nav" data-collapsed="true">
|
</li>
|
||||||
<a href="/index.php<?php echo $lang === 'de' ? '?lang=de' : ''; ?>"><?php echo e($navHome ?? 'Home'); ?></a>
|
<li>
|
||||||
<a href="/index.php<?php echo $lang === 'de' ? '?lang=de' : ''; ?>#latest"><?php echo e($navLatest ?? 'Latest'); ?></a>
|
<a href="/impressum.php<?php echo $langParam; ?>" class="slide-hover-link" <?php echo $currentPage === 'impressum.php' ? 'aria-current="page"' : ''; ?>>
|
||||||
<a href="/index.php<?php echo $lang === 'de' ? '?lang=de' : ''; ?>#basics"><?php echo e($navBasics ?? 'Basics'); ?></a>
|
<span class="slide-hover-wrapper">
|
||||||
|
<span class="slide-text-main"><?php echo isset($lang) && $lang === 'de' ? 'Impressum' : 'Imprint'; ?></span>
|
||||||
|
<span class="slide-text-clone" aria-hidden="true"><?php echo isset($lang) && $lang === 'de' ? 'Impressum' : 'Imprint'; ?></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
|
||||||
|
<!-- Language & Footer links inside overlay -->
|
||||||
|
<div class="nav-overlay-footer">
|
||||||
|
<a href="<?php echo e($langSwitchHref ?? '#'); ?>" class="underline-link">
|
||||||
|
<?php echo isset($lang) && $lang === 'de' ? 'English' : 'Deutsch'; ?>
|
||||||
|
</a>
|
||||||
|
<a href="/datenschutz.php<?php echo $langParam; ?>" class="underline-link">
|
||||||
|
<?php echo isset($lang) && $lang === 'de' ? 'Datenschutz' : 'Privacy'; ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(function() {
|
||||||
const toggle = document.currentScript.previousElementSibling.querySelector('.menu-toggle');
|
var btn = document.getElementById('navMenuBtn');
|
||||||
const nav = document.getElementById('siteNav');
|
var overlay = document.getElementById('navOverlay');
|
||||||
if (!toggle || !nav) return;
|
if (!btn || !overlay) return;
|
||||||
toggle.addEventListener('click', () => {
|
|
||||||
const isCollapsed = nav.getAttribute('data-collapsed') === 'true';
|
function openMenu() {
|
||||||
nav.setAttribute('data-collapsed', String(!isCollapsed));
|
btn.classList.add('active');
|
||||||
toggle.setAttribute('aria-expanded', String(isCollapsed));
|
overlay.classList.add('open');
|
||||||
|
overlay.setAttribute('aria-hidden', 'false');
|
||||||
|
btn.setAttribute('aria-expanded', 'true');
|
||||||
|
document.body.classList.add('nav-open');
|
||||||
|
}
|
||||||
|
function closeMenu() {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
overlay.classList.remove('open');
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
btn.setAttribute('aria-expanded', 'false');
|
||||||
|
document.body.classList.remove('nav-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
if (btn.classList.contains('active')) {
|
||||||
|
closeMenu();
|
||||||
|
} else {
|
||||||
|
openMenu();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
// Close on Escape key
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape' && overlay.classList.contains('open')) {
|
||||||
|
closeMenu();
|
||||||
|
btn.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on overlay link click
|
||||||
|
overlay.querySelectorAll('a').forEach(function(link) {
|
||||||
|
link.addEventListener('click', function() {
|
||||||
|
closeMenu();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+45
-22
@@ -4,9 +4,6 @@ require __DIR__ . '/helpers.php';
|
|||||||
$lang = (isset($_GET['lang']) && strtolower($_GET['lang']) === 'de') ? 'de' : 'en';
|
$lang = (isset($_GET['lang']) && strtolower($_GET['lang']) === 'de') ? 'de' : 'en';
|
||||||
$copy = [
|
$copy = [
|
||||||
'en' => [
|
'en' => [
|
||||||
'home' => 'Home',
|
|
||||||
'latest' => 'Latest',
|
|
||||||
'basics' => 'Basics',
|
|
||||||
'not_found' => 'Recipe not found',
|
'not_found' => 'Recipe not found',
|
||||||
'not_found_sub' => 'Try searching for something else.',
|
'not_found_sub' => 'Try searching for something else.',
|
||||||
'back_home' => '← Back to all recipes',
|
'back_home' => '← Back to all recipes',
|
||||||
@@ -22,9 +19,6 @@ $copy = [
|
|||||||
'level' => 'Level',
|
'level' => 'Level',
|
||||||
],
|
],
|
||||||
'de' => [
|
'de' => [
|
||||||
'home' => 'Start',
|
|
||||||
'latest' => 'Neueste',
|
|
||||||
'basics' => 'Basics',
|
|
||||||
'not_found' => 'Rezept nicht gefunden',
|
'not_found' => 'Rezept nicht gefunden',
|
||||||
'not_found_sub' => 'Versuche eine andere Suche.',
|
'not_found_sub' => 'Versuche eine andere Suche.',
|
||||||
'back_home' => '← Zurück zu allen Rezepten',
|
'back_home' => '← Zurück zu allen Rezepten',
|
||||||
@@ -46,13 +40,10 @@ $allRecipes = load_recipes();
|
|||||||
$slug = isset($_GET['slug']) ? trim($_GET['slug']) : '';
|
$slug = isset($_GET['slug']) ? trim($_GET['slug']) : '';
|
||||||
$recipeRaw = $slug ? find_recipe_by_slug($allRecipes, $slug) : null;
|
$recipeRaw = $slug ? find_recipe_by_slug($allRecipes, $slug) : null;
|
||||||
$recipe = $recipeRaw ? localize_recipe($recipeRaw, $lang) : null;
|
$recipe = $recipeRaw ? localize_recipe($recipeRaw, $lang) : null;
|
||||||
$allRecipesLocalized = localize_recipes($allRecipes, $lang);
|
$allRecipesLocal = localize_recipes($allRecipes, $lang);
|
||||||
|
|
||||||
$langSwitchLabel = $lang === 'de' ? 'EN' : 'DE';
|
$langSwitchLabel = $lang === 'de' ? 'EN' : 'DE';
|
||||||
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
|
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
|
||||||
$navHome = $t['home'];
|
|
||||||
$navLatest = $t['latest'];
|
|
||||||
$navBasics = $t['basics'];
|
|
||||||
|
|
||||||
if (!$recipe) {
|
if (!$recipe) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
@@ -64,7 +55,10 @@ if (!$recipe) {
|
|||||||
<main class="not-found">
|
<main class="not-found">
|
||||||
<h1><?php echo e($t['not_found']); ?></h1>
|
<h1><?php echo e($t['not_found']); ?></h1>
|
||||||
<p><?php echo e($t['not_found_sub']); ?></p>
|
<p><?php echo e($t['not_found_sub']); ?></p>
|
||||||
<a class="button" href="/index.php?lang=<?php echo e($lang); ?>#recipes-start"><?php echo e($t['back_home']); ?></a>
|
<a class="btn-reveal" href="/index.php?lang=<?php echo e($lang); ?>#recipes-start">
|
||||||
|
<span class="btn-reveal-text"><?php echo e($t['back_home']); ?></span>
|
||||||
|
<span class="btn-reveal-arrow">→</span>
|
||||||
|
</a>
|
||||||
</main>
|
</main>
|
||||||
<?php
|
<?php
|
||||||
include __DIR__ . '/partials/footer.php';
|
include __DIR__ . '/partials/footer.php';
|
||||||
@@ -81,17 +75,22 @@ include __DIR__ . '/partials/header.php';
|
|||||||
<div class="recipe__hero">
|
<div class="recipe__hero">
|
||||||
<img src="<?php echo e($recipe['hero']); ?>" alt="<?php echo e($recipe['title']); ?>">
|
<img src="<?php echo e($recipe['hero']); ?>" alt="<?php echo e($recipe['title']); ?>">
|
||||||
<div class="recipe__hero__copy">
|
<div class="recipe__hero__copy">
|
||||||
<p class="pill pill--ghost"><?php echo e($recipe['category']); ?></p>
|
<?php if (!empty($recipe['category'])): ?>
|
||||||
|
<p class="pill pill--gold"><?php echo e($recipe['category']); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
<h1><?php echo e($recipe['title']); ?></h1>
|
<h1><?php echo e($recipe['title']); ?></h1>
|
||||||
<p class="lede"><?php echo e($recipe['description']); ?></p>
|
<p class="lede"><?php echo e($recipe['description']); ?></p>
|
||||||
<div class="meta meta--row">
|
<div class="meta meta--row">
|
||||||
<span><?php echo e($t['prep']); ?>: <?php echo format_minutes((int) $recipe['prep_time']); ?></span>
|
<span><?php echo e($t['prep']); ?>: <?php echo format_minutes((int)$recipe['prep_time']); ?></span>
|
||||||
<span><?php echo e($t['cook']); ?>: <?php echo format_minutes((int) $recipe['cook_time']); ?></span>
|
<span><?php echo e($t['cook']); ?>: <?php echo format_minutes((int)$recipe['cook_time']); ?></span>
|
||||||
<span><?php echo e($t['total']); ?>: <?php echo format_minutes((int) $recipe['total_time']); ?></span>
|
<span><?php echo e($t['total']); ?>: <?php echo format_minutes((int)$recipe['total_time']); ?></span>
|
||||||
<span><?php echo e($t['serves']); ?>: <?php echo e($recipe['servings']); ?></span>
|
<span><?php echo e($t['serves']); ?>: <?php echo e($recipe['servings']); ?></span>
|
||||||
<span><?php echo e($t['level']); ?>: <?php echo e($recipe['difficulty']); ?></span>
|
<span><?php echo e($t['level']); ?>: <?php echo e($recipe['difficulty']); ?></span>
|
||||||
</div>
|
</div>
|
||||||
<a class="button" href="/index.php?lang=<?php echo e($lang); ?>#recipes-start"><?php echo e($t['back_home']); ?></a>
|
<a class="btn-reveal" href="/index.php?lang=<?php echo e($lang); ?>#recipes-start">
|
||||||
|
<span class="btn-reveal-text"><?php echo e($t['back_home']); ?></span>
|
||||||
|
<span class="btn-reveal-arrow">←</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -127,20 +126,25 @@ include __DIR__ . '/partials/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="more">
|
<section class="more">
|
||||||
<div class="section-header">
|
<div class="section-header section-header--stack">
|
||||||
<h3><?php echo e($t['also_tasty']); ?></h3>
|
<p class="eyebrow"><?php echo e($t['also_tasty']); ?></p>
|
||||||
<p><?php echo e($t['more_dishes']); ?></p>
|
<h3><?php echo e($t['more_dishes']); ?></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid--tight">
|
<div class="grid grid--tight">
|
||||||
<?php foreach ($allRecipesLocalized as $other): ?>
|
<?php foreach ($allRecipesLocal as $other): ?>
|
||||||
<?php if ($other['slug'] === $recipe['slug'] || !empty($other['coming_soon'])) continue; ?>
|
<?php if ($other['slug'] === $recipe['slug'] || !empty($other['coming_soon'])) continue; ?>
|
||||||
<article class="card card--bare">
|
<article class="card card--bare reveal-target">
|
||||||
|
<a href="/recipe.php?<?php echo http_build_query(['slug' => $other['slug'], 'lang' => $lang]); ?>" class="card__image">
|
||||||
|
<img src="<?php echo e($other['hero']); ?>" alt="<?php echo e($other['title']); ?>" loading="lazy">
|
||||||
|
</a>
|
||||||
|
<div class="card__body">
|
||||||
<h4><a href="/recipe.php?<?php echo http_build_query(['slug' => $other['slug'], 'lang' => $lang]); ?>"><?php echo e($other['title']); ?></a></h4>
|
<h4><a href="/recipe.php?<?php echo http_build_query(['slug' => $other['slug'], 'lang' => $lang]); ?>"><?php echo e($other['title']); ?></a></h4>
|
||||||
<p><?php echo e($other['description']); ?></p>
|
<p><?php echo e($other['description']); ?></p>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span>⏱ <?php echo format_minutes((int) ($other['total_time'] ?? 0)); ?></span>
|
<span>⏱ <?php echo format_minutes((int)($other['total_time'] ?? 0)); ?></span>
|
||||||
<span>🙂 <?php echo e($other['difficulty']); ?></span>
|
<span>🙂 <?php echo e($other['difficulty']); ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,3 +152,22 @@ include __DIR__ . '/partials/header.php';
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<?php include __DIR__ . '/partials/footer.php'; ?>
|
<?php include __DIR__ . '/partials/footer.php'; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Scroll reveal on recipe page
|
||||||
|
if ('IntersectionObserver' in window) {
|
||||||
|
var obs = new IntersectionObserver(function(entries) {
|
||||||
|
entries.forEach(function(e) {
|
||||||
|
if (e.isIntersecting) { e.target.classList.add('revealed'); obs.unobserve(e.target); }
|
||||||
|
});
|
||||||
|
}, { threshold: 0.08 });
|
||||||
|
document.querySelectorAll('.reveal-target').forEach(function(el) { obs.observe(el); });
|
||||||
|
}
|
||||||
|
// Lenis init for recipe page
|
||||||
|
if (typeof Lenis !== 'undefined') {
|
||||||
|
var lenis = new Lenis({ duration: 1.2, smooth: true, smoothTouch: false });
|
||||||
|
window.lenis = lenis;
|
||||||
|
function raf(time) { lenis.raf(time); requestAnimationFrame(raf); }
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user