diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..de15895 --- /dev/null +++ b/.htaccess @@ -0,0 +1,4 @@ +#KONSOLEH AREA START - PLEASE DO NOT EDIT MANUALLY BETWEEN THESE LINES +DirectoryIndex index.php +#KONSOLEH AREA END - PLEASE ADD MANUAL CHANGES BELOW +SetEnv FLIXCOOKS_ADMIN_KEY "REWnqLYwQtZZDgXcNxnt" \ No newline at end of file diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..2b8f5ce --- /dev/null +++ b/AGENT.md @@ -0,0 +1,5 @@ +## Workflow + +- After every chat response that includes code or content changes, make a git commit describing the work. +- Keep commits small and scoped to the edits made in that conversation. +- Do not defer commits; commit as soon as the edits are complete. diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..1d4621e --- /dev/null +++ b/admin.php @@ -0,0 +1,393 @@ + + + + + + + FlixCooks Admin Login + + + + +
+

Admin

+

Enter your password to manage recipes.

+

+
+ + +
+
+ + + $r) { + if (($r['slug'] ?? '') === $slugEdit) { + $editing = $r; + $editIndex = $idx; + $editingEn = $r['i18n']['en'] ?? []; + $editingDe = $r['i18n']['de'] ?? []; + break; + } + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete') { + $slugDel = trim($_POST['slug'] ?? ''); + $before = count($allRecipes); + $allRecipes = array_values(array_filter($allRecipes, fn($r) => ($r['slug'] ?? '') !== $slugDel)); + if ($before !== count($allRecipes) && save_recipes($allRecipes)) { + $message = 'Recipe deleted.'; + } else { + $message = 'Could not delete. Check permissions.'; + } + $editing = null; + $editIndex = null; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'save') { + // helpers + $parse_csv = fn($text) => array_values(array_filter(array_map('trim', explode(',', $text ?? '')), 'strlen')); + $parse_lines = fn($text) => array_values(array_filter(array_map('trim', preg_split('/\\r?\\n/', $text ?? '')), 'strlen')); + + $titleEn = trim($_POST['title_en'] ?? ''); + $titleDe = trim($_POST['title_de'] ?? ''); + $descriptionEn = trim($_POST['description_en'] ?? ''); + $descriptionDe = trim($_POST['description_de'] ?? ''); + $hero = trim($_POST['hero'] ?? ''); + $categoryEn = trim($_POST['category_en'] ?? ''); + $categoryDe = trim($_POST['category_de'] ?? ''); + $tagsEn = $parse_csv($_POST['tags_en'] ?? ''); + $tagsDe = $parse_csv($_POST['tags_de'] ?? ''); + $prep = (int) ($_POST['prep_time'] ?? 0); + $cook = (int) ($_POST['cook_time'] ?? 0); + $total = (int) ($_POST['total_time'] ?? ($prep + $cook)); + $servings = (int) ($_POST['servings'] ?? 0); + $difficultyEn = trim($_POST['difficulty_en'] ?? 'Easy'); + $difficultyDe = trim($_POST['difficulty_de'] ?? 'Einfach'); + $ingredientsEn = $parse_lines($_POST['ingredients_en'] ?? ''); + $ingredientsDe = $parse_lines($_POST['ingredients_de'] ?? ''); + $utensilsEn = $parse_lines($_POST['utensils_en'] ?? ''); + $utensilsDe = $parse_lines($_POST['utensils_de'] ?? ''); + $stepsEn = $parse_lines($_POST['steps_en'] ?? ''); + $stepsDe = $parse_lines($_POST['steps_de'] ?? ''); + $featured = isset($_POST['featured']) && $_POST['featured'] === '1'; + $comingSoon = isset($_POST['coming_soon']) && $_POST['coming_soon'] === '1'; + $slugOriginal = trim($_POST['slug_original'] ?? ''); + + // keep previous language data if fields left blank while editing + $prevEn = $editing['i18n']['en'] ?? []; + $prevDe = $editing['i18n']['de'] ?? []; + + $titleEn = $titleEn !== '' ? $titleEn : ($prevEn['title'] ?? ''); + $titleDe = $titleDe !== '' ? $titleDe : ($prevDe['title'] ?? ''); + $descriptionEn = $descriptionEn !== '' ? $descriptionEn : ($prevEn['description'] ?? ''); + $descriptionDe = $descriptionDe !== '' ? $descriptionDe : ($prevDe['description'] ?? ''); + $categoryEn = $categoryEn !== '' ? $categoryEn : ($prevEn['category'] ?? ''); + $categoryDe = $categoryDe !== '' ? $categoryDe : ($prevDe['category'] ?? ''); + $difficultyEn = $difficultyEn !== '' ? $difficultyEn : ($prevEn['difficulty'] ?? 'Easy'); + $difficultyDe = $difficultyDe !== '' ? $difficultyDe : ($prevDe['difficulty'] ?? 'Einfach'); + $tagsEn = !empty($tagsEn) ? $tagsEn : ($prevEn['tags'] ?? []); + $tagsDe = !empty($tagsDe) ? $tagsDe : ($prevDe['tags'] ?? []); + $ingredientsEn = !empty($ingredientsEn) ? $ingredientsEn : ($prevEn['ingredients'] ?? []); + $ingredientsDe = !empty($ingredientsDe) ? $ingredientsDe : ($prevDe['ingredients'] ?? []); + $utensilsEn = !empty($utensilsEn) ? $utensilsEn : ($prevEn['utensils'] ?? []); + $utensilsDe = !empty($utensilsDe) ? $utensilsDe : ($prevDe['utensils'] ?? []); + $stepsEn = !empty($stepsEn) ? $stepsEn : ($prevEn['steps'] ?? []); + $stepsDe = !empty($stepsDe) ? $stepsDe : ($prevDe['steps'] ?? []); + + if ($titleEn === '') { + $message = 'English title is required.'; + } elseif (!$comingSoon && (empty($ingredientsEn) || empty($stepsEn))) { + $message = 'Ingredients and steps are required unless marked coming soon.'; + } else { + // determine slug + if ($slugOriginal) { + $slug = $slugOriginal; + } else { + $slug = slugify($titleEn ?: $titleDe); + // ensure unique slug + $existingSlugs = array_map(fn($r) => $r['slug'] ?? '', $allRecipes); + $base = $slug; + $i = 1; + while (in_array($slug, $existingSlugs, true)) { + $slug = $base . '-' . $i; + $i++; + } + } + + if ($featured) { + foreach ($allRecipes as &$r) { + $r['featured'] = false; + } + unset($r); + } + + $record = [ + 'slug' => $slug, + 'hero' => $hero, + 'prep_time' => $prep, + 'cook_time' => $cook, + 'total_time' => $total, + 'servings' => $servings, + 'featured' => $featured, + 'coming_soon' => $comingSoon, + 'i18n' => [ + 'en' => [ + 'title' => $titleEn, + 'description' => $descriptionEn, + 'category' => $categoryEn, + 'difficulty' => $difficultyEn, + 'tags' => $tagsEn, + 'ingredients' => $ingredientsEn, + 'utensils' => $utensilsEn, + 'steps' => $stepsEn, + ], + 'de' => [ + 'title' => $titleDe ?: $titleEn, + 'description' => $descriptionDe ?: $descriptionEn, + 'category' => $categoryDe ?: $categoryEn, + 'difficulty' => $difficultyDe, + 'tags' => !empty($tagsDe) ? $tagsDe : $tagsEn, + 'ingredients' => !empty($ingredientsDe) ? $ingredientsDe : $ingredientsEn, + 'utensils' => !empty($utensilsDe) ? $utensilsDe : $utensilsEn, + 'steps' => !empty($stepsDe) ? $stepsDe : $stepsEn, + ], + ], + ]; + + if ($slugOriginal && $editIndex !== null) { + $allRecipes[$editIndex] = $record; + } else { + $allRecipes[] = $record; + } + + if (save_recipes($allRecipes)) { + $message = $slugOriginal ? 'Saved changes.' : 'Saved! New recipe added.'; + } else { + $message = 'Could not save the file. Check permissions on data/recipes.json.'; + } + } +} +?> + + + + + + FlixCooks Admin + + + + +
+

FlixCooks Admin

+
+ View site + + Back + +
+
+ + +
+ + +
+ + + + + + +

Accepts full URLs or relative paths (e.g. /assets/images/hero.jpg).

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

English

+
+
+ + +
+
+ + +
+
+ + + +
+
+ + + + + + + + + + + +
+

Deutsch

+
+
+ + +
+
+ + +
+
+ + + +
+
+ + + + + + + + + + + + + + + +
+ +
+

Existing recipes ()

+ +

No recipes yet.

+ + + +
+
+ + + Featured + Coming soon +
+
+ + Edit +
+ + + +
+
+
+ + +
+ + diff --git a/assets/favicon.png b/assets/favicon.png new file mode 100644 index 0000000..ec59e56 Binary files /dev/null and b/assets/favicon.png differ diff --git a/assets/haferkuchen.JPEG b/assets/haferkuchen.JPEG new file mode 100644 index 0000000..c002784 Binary files /dev/null and b/assets/haferkuchen.JPEG differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..c93e949 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/pancakes.jpg b/assets/pancakes.jpg new file mode 100644 index 0000000..e88d54c Binary files /dev/null and b/assets/pancakes.jpg differ diff --git a/assets/pasta-tomato.jpg b/assets/pasta-tomato.jpg new file mode 100644 index 0000000..7d634c4 Binary files /dev/null and b/assets/pasta-tomato.jpg differ diff --git a/assets/placeholder.svg b/assets/placeholder.svg new file mode 100644 index 0000000..0886e83 --- /dev/null +++ b/assets/placeholder.svg @@ -0,0 +1,16 @@ + + Coming soon placeholder + A simple placeholder card for upcoming recipes. + + + + + + + + + + + + Coming soon + diff --git a/assets/steak.JPEG b/assets/steak.JPEG new file mode 100644 index 0000000..921f588 Binary files /dev/null and b/assets/steak.JPEG differ diff --git a/assets/steak.jpg b/assets/steak.jpg new file mode 100644 index 0000000..8245a97 Binary files /dev/null and b/assets/steak.jpg differ diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..21b9807 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,1024 @@ +/* ═══════════════════════════════════════════════════════════════ + FlixCooks — Design System + Landing Page + ═══════════════════════════════════════════════════════════════ */ + +/* ── Tokens ─────────────────────────────────────────────────────── */ +:root { + --bg: #f0f2f7; + --bg-2: #e5e8ef; + --surface: #ffffff; + --surface-2: #f7f8fb; + --panel: #f9fafc; + --panel-2: #f2f4f8; + --stroke: #d9deea; + --ghost: rgba(107,118,140,0.14); + --ink: #1c1f29; + --muted: #5c6477; + --accent: #6b768c; + --accent-soft: rgba(107,118,140,0.12); + --accent-2: #8ea0c3; + --shadow: 0 10px 28px rgba(12,16,28,0.12); + --radius: 12px; + + /* Landing-specific */ + --land-bg: #0d0e11; + --land-text: #f0f2f7; + --land-muted: rgba(240,242,247,0.55); + --land-accent: #e8c87a; + --land-accent2: #c87a5a; +} + +[data-theme="dark"] { + --bg: #131519; + --bg-2: #0f1114; + --surface: #1a1d24; + --surface-2: #1e2029; + --panel: #22252e; + --panel-2: #1c1f28; + --stroke: #2e3240; + --ghost: rgba(180,190,220,0.08); + --ink: #dde0ea; + --muted: #8892a6; + --accent: #8ea0c3; + --accent-soft: rgba(142,160,195,0.15); + --accent-2: #6b80a4; + --shadow: 0 10px 28px rgba(0,0,0,0.35); +} + +/* ── Base reset ──────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { + scroll-behavior: smooth; + scroll-padding-top: 5rem; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: 'Manrope', system-ui, -apple-system, sans-serif; + color: var(--ink); + background: + radial-gradient(circle at 25% 20%, rgba(107,118,140,0.08), transparent 28%), + radial-gradient(circle at 78% 4%, rgba(142,160,195,0.10), transparent 32%), + linear-gradient(140deg, var(--bg), var(--bg-2)); + min-height: 100vh; + line-height: 1.6; +} + +h1, h2, h3, h4 { + font-family: 'Playfair Display', 'Times New Roman', serif; + margin: 0; + color: var(--ink); + line-height: 1.15; + text-wrap: balance; +} +h1 span, h2 span { color: var(--accent); } +p { color: var(--muted); line-height: 1.6; } +a { color: var(--ink); text-decoration: none; transition: color 0.18s; } +a:hover { color: var(--accent); } +img, picture { display: block; max-width: 100%; height: auto; } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} + +/* ══════════════════════════════════════════════════════════════════ + LANDING SECTION + ══════════════════════════════════════════════════════════════════ */ + +.landing { + position: relative; + width: 100%; + min-height: 100dvh; + background: var(--land-bg); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +/* Animated gradient blobs */ +.landing__blobs { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 0; +} +.blob { + position: absolute; + border-radius: 50%; + filter: blur(80px); + opacity: 0.55; + will-change: transform; +} +.blob--a { + width: clamp(340px, 50vw, 700px); + height: clamp(340px, 50vw, 700px); + background: radial-gradient(circle, #4a3a6e 0%, transparent 70%); + top: -15%; + left: -12%; + animation: blobA 18s ease-in-out infinite alternate; +} +.blob--b { + width: clamp(280px, 40vw, 600px); + height: clamp(280px, 40vw, 600px); + background: radial-gradient(circle, #6b3a2a 0%, transparent 70%); + bottom: -10%; + right: -8%; + animation: blobB 22s ease-in-out infinite alternate; +} +.blob--c { + width: clamp(200px, 30vw, 450px); + height: clamp(200px, 30vw, 450px); + background: radial-gradient(circle, #1a3a5a 0%, transparent 70%); + top: 40%; + left: 55%; + animation: blobC 15s ease-in-out infinite alternate; +} +@keyframes blobA { + from { transform: translate(0,0) scale(1); } + to { transform: translate(6%, 8%) scale(1.12); } +} +@keyframes blobB { + from { transform: translate(0,0) scale(1.05); } + to { transform: translate(-5%, -6%) scale(0.92); } +} +@keyframes blobC { + from { transform: translate(0,0) scale(0.95); } + to { transform: translate(4%, 10%) scale(1.08); } +} + +/* Fine grain overlay */ +.landing::before { + content: ''; + position: absolute; + inset: 0; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E"); + opacity: 0.03; + pointer-events: none; + z-index: 1; +} + +/* Floating food chips */ +.landing__chips { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 2; +} +.chip { + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + background: rgba(255,255,255,0.07); + border: 1px solid rgba(255,255,255,0.12); + backdrop-filter: blur(14px); + border-radius: 16px; + padding: 10px; + width: clamp(80px, 10vw, 120px); + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; + animation-direction: alternate; + opacity: 0; + animation-fill-mode: both; +} +.chip img { + width: 100%; + aspect-ratio: 1; + object-fit: cover; + border-radius: 10px; +} +.chip span { + color: rgba(240,242,247,0.75); + font-size: clamp(10px, 1.2vw, 13px); + font-weight: 700; + letter-spacing: 0.04em; +} + +.chip--1 { + top: 12%; + left: clamp(4%, 6vw, 8%); + animation-name: floatA; + animation-duration: 8s; + animation-delay: 0.6s; +} +.chip--2 { + bottom: 18%; + left: clamp(3%, 5vw, 7%); + animation-name: floatB; + animation-duration: 10s; + animation-delay: 0.9s; +} +.chip--3 { + top: 10%; + right: clamp(4%, 6vw, 8%); + animation-name: floatC; + animation-duration: 9s; + animation-delay: 1.2s; +} +.chip--4 { + bottom: 14%; + right: clamp(3%, 5vw, 7%); + animation-name: floatA; + animation-duration: 11s; + animation-delay: 0.4s; +} + +@keyframes floatA { + from { transform: translateY(0px) rotate(-1deg); opacity: 0.85; } + to { transform: translateY(-18px) rotate(1deg); opacity: 1; } +} +@keyframes floatB { + from { transform: translateY(0px) rotate(1.5deg); opacity: 0.8; } + to { transform: translateY(-14px) rotate(-1.5deg); opacity: 1; } +} +@keyframes floatC { + from { transform: translateY(0px) rotate(-2deg); opacity: 0.9; } + to { transform: translateY(-22px) rotate(0.5deg); opacity: 1; } +} + +/* Main copy */ +.landing__copy { + position: relative; + z-index: 3; + text-align: center; + padding: clamp(1.5rem, 4vw, 3rem) clamp(1rem, 5vw, 2rem); + max-width: 760px; + animation: fadeUp 0.9s cubic-bezier(0.16,1,0.3,1) both; + animation-delay: 0.15s; +} + +.landing__eyebrow { + color: var(--land-muted); + font-size: clamp(0.7rem, 1.2vw, 0.85rem); + font-weight: 700; + letter-spacing: 0.2em; + text-transform: uppercase; + margin-bottom: 1.25rem; +} + +.landing__headline { + font-family: 'Playfair Display', serif; + font-size: clamp(2.6rem, 7.5vw, 7rem); + font-weight: 600; + line-height: 1.05; + color: var(--land-text); + display: flex; + flex-direction: column; + gap: 0.05em; + margin-bottom: 1.5rem; +} +.landing__headline em { + font-style: italic; + color: var(--land-accent); +} +.landing__headline .line { + display: block; + opacity: 0; + animation: lineIn 0.7s cubic-bezier(0.16,1,0.3,1) both; +} +.landing__headline .line--1 { animation-delay: 0.35s; } +.landing__headline .line--2 { animation-delay: 0.5s; } +.landing__headline .line--3 { animation-delay: 0.65s; } + +.landing__sub { + color: var(--land-muted); + font-size: clamp(0.95rem, 1.8vw, 1.15rem); + max-width: 520px; + margin: 0 auto 2rem; + opacity: 0; + animation: fadeUp 0.7s 0.8s cubic-bezier(0.16,1,0.3,1) both; +} + +.landing__cta { + display: inline-flex; + align-items: center; + gap: 8px; + padding: clamp(12px,2vw,16px) clamp(22px,3vw,32px); + border-radius: 999px; + background: var(--land-accent); + color: #1a1208; + font-weight: 800; + font-size: clamp(0.875rem,1.4vw,1rem); + letter-spacing: 0.02em; + transition: transform 0.18s, box-shadow 0.18s, background 0.18s; + opacity: 0; + animation: fadeUp 0.7s 0.95s cubic-bezier(0.16,1,0.3,1) both; + text-decoration: none; +} +.landing__cta:hover { + background: #f0d490; + color: #1a1208; + transform: translateY(-2px); + box-shadow: 0 8px 28px rgba(232,200,122,0.35); +} +.landing__cta svg { + transition: transform 0.2s; +} +.landing__cta:hover svg { transform: translateY(3px); } + +/* Scroll nudge */ +.landing__scroll-nudge { + position: absolute; + bottom: 2.5rem; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + z-index: 3; + opacity: 0; + animation: fadeUp 0.7s 1.3s ease both; +} +.scroll-line { + width: 1px; + height: 48px; + background: linear-gradient(to bottom, transparent, rgba(240,242,247,0.5)); + animation: scrollPulse 2.5s ease-in-out infinite; +} +.landing__scroll-nudge span { + color: rgba(240,242,247,0.4); + font-size: 0.65rem; + letter-spacing: 0.2em; + text-transform: uppercase; +} +@keyframes scrollPulse { + 0%, 100% { opacity: 0.4; transform: scaleY(0.7); transform-origin: top; } + 50% { opacity: 1; transform: scaleY(1); } +} + +/* Theme toggle — fixed top-right, always visible */ +.theme-toggle { + position: fixed; + top: 1.25rem; + right: 1.5rem; + z-index: 1000; + width: 40px; + height: 40px; + border-radius: 50%; + border: 1px solid var(--land-toggle-border); + background: var(--land-toggle-bg); + color: var(--land-toggle-color); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + backdrop-filter: blur(10px); + transition: background 0.18s, color 0.18s, border-color 0.18s; +} +.theme-toggle:hover { + background: var(--land-chip-border); + color: var(--land-text); +} +/* Once scrolled past landing, adapt to site theme */ +.theme-toggle.on-site { + border-color: var(--stroke); + background: var(--surface); + color: var(--ink); +} +.theme-toggle.on-site:hover { + background: var(--panel-2); + color: var(--accent); +} + +/* Keyframes shared */ +@keyframes fadeUp { + from { opacity: 0; transform: translateY(24px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes lineIn { + from { opacity: 0; transform: translateY(20px) skewY(1deg); } + to { opacity: 1; transform: translateY(0) skewY(0deg); } +} + +/* ══════════════════════════════════════════════════════════════════ + SCROLL REVEAL (for cards below landing) + ══════════════════════════════════════════════════════════════════ */ +.reveal-target { + opacity: 0; + transform: translateY(28px); + transition: opacity 0.55s cubic-bezier(0.16,1,0.3,1), + transform 0.55s cubic-bezier(0.16,1,0.3,1); +} +.reveal-target.revealed { + opacity: 1; + transform: translateY(0); +} + +/* ══════════════════════════════════════════════════════════════════ + EXISTING SITE STYLES (unchanged from original) + ══════════════════════════════════════════════════════════════════ */ + +#recipes { + padding: 0 24px 64px; +} + +.site-header { + max-width: 1200px; + margin: 24px auto 12px; + padding: 14px 20px; + border: 1px solid rgba(28,32,52,0.08); + border-radius: 14px; + background: rgba(255,255,255,0.9); + backdrop-filter: blur(12px); + display: flex; + flex-direction: column; + gap: 12px; + box-shadow: var(--shadow); +} +[data-theme="dark"] .site-header { + background: rgba(26,29,36,0.92); + border-color: rgba(255,255,255,0.06); +} +.site-header__top { + display: flex; + align-items: center; + gap: 12px; +} +.site-header .logo { display: inline-flex; align-items: center; } +.site-header .logo img { display: block; height: clamp(28px,5vw,48px); width: auto; } +.header-actions { margin-left: auto; display: inline-flex; align-items: center; gap: 10px; } +.lang-toggle { + padding: 8px 12px; + border-radius: 12px; + background: rgba(28,32,52,0.06); + font-weight: 700; + letter-spacing: 0.04em; + color: var(--ink); +} +.lang-toggle:hover { background: var(--accent-soft); color: var(--ink); } +.menu-toggle { + display: none !important; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 12px; + border: 1px solid rgba(28,32,52,0.08); + background: transparent; + color: var(--ink); + font-weight: 700; + cursor: pointer; +} +.menu-toggle__label { display: none; } +.menu-toggle:hover { background: var(--accent-soft); } +.site-nav { display: flex; gap: 12px; flex-wrap: wrap; } +.site-nav a { padding: 8px 12px; border-radius: 12px; transition: background 0.18s; } +.site-nav a:hover { background: var(--accent-soft); } + +.hero { + max-width: 1200px; + margin: 24px auto 56px; + display: grid; + grid-template-columns: minmax(320px,1fr) minmax(280px,0.9fr); + gap: 24px; + align-items: stretch; +} + +.coming-strip { + max-width: 1200px; + margin: 18px auto 12px; + padding: 14px 16px; + background: var(--surface); + border: 1px solid var(--stroke); + border-radius: 14px; + display: grid; + gap: 10px; +} +.coming-strip__head { display: flex; align-items: center; gap: 10px; } +.coming-strip__head small { color: var(--muted); } +.coming-strip__row { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(120px,1fr); + gap: 12px; + overflow-x: auto; + padding-bottom: 6px; + scrollbar-color: #4f5664 transparent; +} +.coming-strip__row::-webkit-scrollbar { height: 8px; } +.coming-strip__row::-webkit-scrollbar-track { background: transparent; } +.coming-strip__row::-webkit-scrollbar-thumb { background: #4f5664; border-radius: 999px; } +.coming-chip { + background: var(--panel); + border: 1px solid var(--stroke); + border-radius: 12px; + padding: 8px; + display: grid; + gap: 8px; + box-shadow: 0 6px 14px rgba(12,16,28,0.06); +} +.coming-chip__image img { + width: 100%; + aspect-ratio: 9/16; + object-fit: cover; + border-radius: 10px; + background: var(--panel-2); + display: block; +} +.coming-chip__title { font-weight: 700; color: var(--ink); font-size: 14px; } + +.coming-features { + max-width: 1200px; + margin: 40px auto 56px; + padding: 22px; + background: var(--surface); + border: 1px solid var(--stroke); + border-radius: var(--radius); + box-shadow: var(--shadow); + display: grid; + gap: 18px; +} +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); + gap: 14px; +} +.feature-card { + padding: 14px 16px; + background: var(--panel); + border: 1px solid var(--stroke); + border-radius: 12px; + display: grid; + gap: 10px; + align-items: center; +} +.feature-icon { + width: 40px; + height: 40px; + display: grid; + place-items: center; + border-radius: 10px; + background: var(--accent-soft); + font-size: 18px; +} + +.hero-copy { padding: 12px; order: 1; } +.hero-copy h1 { font-size: clamp(32px,4vw,48px); line-height: 1.1; margin: 8px 0 12px; } +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.18em; + font-weight: 600; + color: var(--muted); + font-size: 12px; +} +.search { margin: 18px 0 8px; display: flex; gap: 8px; } +.search input { + flex: 1; + padding: 14px 16px; + border: 1px solid var(--stroke); + border-radius: 14px; + font-size: 15px; + background: var(--surface); + color: var(--ink); + font-family: inherit; +} +.search input:focus { outline: 2px solid var(--accent-2); outline-offset: 2px; } +.search button { + padding: 14px 18px; + border: 1px solid var(--stroke); + border-radius: 14px; + background: var(--panel); + color: var(--ink); + font-weight: 700; + cursor: pointer; + font-family: inherit; + transition: background 0.18s; +} +.search button:hover { background: var(--panel-2); } +.tag-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } +.tag { + padding: 8px 12px; + border-radius: 12px; + background: var(--panel); + color: var(--ink); + font-weight: 600; + font-size: 13px; + border: 1px solid var(--stroke); + transition: background 0.18s; +} +.tag:hover { background: var(--panel-2); color: var(--ink); } +.tag.active { background: var(--accent); color: white; } +.tag.clear { background: transparent; border: 1px dashed var(--stroke); color: var(--muted); } + +.hero-card { + background: var(--surface); + border-radius: var(--radius); + overflow: hidden; + box-shadow: 0 10px 24px rgba(12,16,28,0.08); + border: 1px solid var(--stroke); + display: grid; + grid-template-rows: auto 1fr; + order: 2; +} +.hero-card img { + width: 100%; + aspect-ratio: 9/16; + object-fit: contain; + background: var(--surface); + max-height: 520px; +} +.hero-card__body { padding: 18px; display: grid; gap: 10px; } + +.section-header { + max-width: 1200px; + margin: 0 auto 12px; + display: flex; + align-items: baseline; + gap: 12px; +} +.section-header h2, .section-header h3 { margin: 0; } +.section-header p { margin: 0; color: var(--muted); } + +.latest, .basics, .more { + max-width: 1200px; + margin: 0 auto 56px; +} +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px,1fr)); + gap: 18px; +} +.grid--tight { grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); } + +.card { + background: var(--surface); + border-radius: var(--radius); + overflow: hidden; + box-shadow: 0 8px 20px rgba(12,16,28,0.08); + border: 1px solid var(--stroke); + display: flex; + flex-direction: column; + transition: transform 0.18s, box-shadow 0.18s; +} +.card:hover { transform: translateY(-3px); box-shadow: 0 14px 32px rgba(12,16,28,0.14); } +.card__image { position: relative; display: block; } +.card__image img { + width: 100%; + aspect-ratio: 9/16; + max-height: 260px; + object-fit: contain; + object-position: right center; + display: block; + background: var(--surface); +} +.pill { + display: inline-block; + padding: 6px 10px; + border-radius: 999px; + background: var(--accent-soft); + color: var(--ink); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.02em; +} +.pill--ghost { background: var(--ghost); color: var(--muted); } +.card__body { padding: 16px; display: grid; gap: 10px; } +.card h3 { margin: 0; font-size: 20px; } +.meta { display: flex; flex-wrap: wrap; gap: 12px; color: var(--muted); font-size: 13px; } +.meta--row { gap: 16px; } +.tags { display: flex; flex-wrap: wrap; gap: 8px; } +.card--bare { border: 1px solid var(--stroke); box-shadow: none; background: var(--panel); } + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 16px; + border-radius: 12px; + background: transparent; + color: var(--ink); + font-weight: 700; + text-decoration: none; + border: 1px solid var(--stroke); + cursor: pointer; + transition: background 0.18s, border-color 0.18s; +} +.button:hover { background: var(--panel-2); border-color: #c6cede; color: var(--ink); } + +/* ── Recipe page ─────────────────────────────────────────────────── */ +.recipe { + max-width: 1100px; + margin: 0 auto 64px; + background: var(--surface-2); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; +} +.recipe__hero { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px,1fr)); + align-items: stretch; +} +.recipe__hero img { + width: 100%; + aspect-ratio: 9/16; + max-height: 640px; + object-fit: contain; + background: var(--surface); +} +.recipe__hero__copy { + padding: 24px; + display: grid; + gap: 12px; + background: linear-gradient(140deg, var(--panel), var(--panel-2)); +} +.lede { font-size: 18px; line-height: 1.6; } +.recipe__body { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px,1fr)); + gap: 14px; + padding: 22px; +} +.panel { + padding: 18px; + background: var(--panel); + border-radius: 12px; + border: 1px solid var(--stroke); +} +.ingredients { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 8px; +} +.ingredients li { + padding: 8px 10px; + background: var(--panel-2); + border-radius: 10px; + border: 1px solid var(--stroke); +} +.steps { padding-left: 20px; display: grid; gap: 10px; } +.steps li { padding: 6px 0; } +.more { padding: 0 12px 24px; } + +/* ── Footer ──────────────────────────────────────────────────────── */ +.site-footer { + max-width: 1100px; + margin: 0 auto 24px; + padding: 24px; + background: var(--surface-2); + border-radius: var(--radius); + box-shadow: var(--shadow); + display: grid; + gap: 12px; + border: 1px solid var(--stroke); +} +.footer-meta { + color: var(--muted); + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} +.contact-link { font-weight: 700; color: var(--ink); } +.contact-link:hover { color: var(--accent); } + +.not-found { + max-width: 800px; + margin: 120px auto; + text-align: center; + display: grid; + gap: 12px; +} + +/* ── Back to top ─────────────────────────────────────────────────── */ +#backToTop { + position: fixed; + right: 18px; + bottom: 18px; + min-width: 44px; + height: 44px; + padding: 0 14px; + border-radius: 999px; + border: 1px solid var(--stroke); + background: var(--surface); + color: var(--ink); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-weight: 800; + font-size: 14px; + white-space: nowrap; + cursor: pointer; + box-shadow: 0 10px 24px rgba(0,0,0,0.2); + opacity: 0; + transform: translateY(12px); + transition: opacity 0.25s ease, transform 0.25s ease; + z-index: 999; +} +#backToTop.show { opacity: 1; transform: translateY(0); } +#backToTop:hover { background: var(--panel-2); } + +/* ══════════════════════════════════════════════════════════════════ + RESPONSIVE + ══════════════════════════════════════════════════════════════════ */ + +/* Hide chips on small screens to avoid clutter */ +@media (max-width: 600px) { + .landing__chips { display: none; } + .landing__headline { font-size: clamp(2.2rem, 12vw, 4rem); } +} + +@media (max-width: 900px) { + #recipes { padding: 0 14px 40px; } + .site-header { padding: 12px 14px; gap: 10px; border-radius: 18px; } + .site-header .logo img { height: clamp(32px,12vw,56px); } + .menu-toggle { display: inline-flex !important; } + .menu-toggle__label { display: inline; } + .site-nav { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px,1fr)); + gap: 8px; + max-height: 0; + overflow: hidden; + transition: max-height 0.25s ease; + } + .site-nav[data-collapsed="false"] { max-height: 200px; } + .site-nav a { background: var(--panel); } + + .coming-strip { padding: 12px; gap: 8px; } + .coming-strip__row { + grid-auto-columns: minmax(132px,46vw); + gap: 10px; + scroll-snap-type: x proximity; + } + .coming-chip { scroll-snap-align: start; } + .coming-chip__image img { aspect-ratio: 4/3; } + .coming-chip__title { font-size: 13px; } + .coming-features { padding: 18px; margin: 32px auto 44px; } + .feature-grid { grid-template-columns: repeat(auto-fit, minmax(180px,1fr)); } + + .hero { + grid-template-columns: 1fr; + gap: 18px; + margin: 20px auto 40px; + } + .hero-card, .hero-copy { order: unset; } + .hero-copy { padding: 0 2px; } + .hero-copy h1 { font-size: clamp(30px,9vw,40px); } + .search { flex-direction: column; gap: 10px; } + .search button { width: 100%; } + .tag-row { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; } + .tag-row .tag { flex: 0 0 auto; } + .hero-card img { max-height: 240px; object-fit: cover; object-position: center; } + .section-header { flex-direction: column; align-items: flex-start; gap: 6px; } + .section-header p { font-size: 14px; } + .grid, .grid--tight, .recipe__body { grid-template-columns: 1fr; } + .recipe { margin: 0 auto 40px; } + .recipe__body { padding: 16px; } + .card__image img { max-height: 220px; object-fit: cover; object-position: center; } + #backToTop { min-width: 44px; width: 44px; padding: 0; font-size: 0; gap: 0; } + #backToTop::before { content: "\2191"; font-size: 18px; line-height: 1; } +} + +@media (max-width: 480px) { + #recipes { padding: 0 12px 32px; } + .hero { gap: 16px; } + .coming-strip__row { grid-auto-columns: 128px; } + .coming-chip__image img { aspect-ratio: 1/1; } + .hero-card img { max-height: 220px; } + .card__body { padding: 14px; } + .recipe__hero { grid-template-columns: 1fr; } + .recipe__hero img { max-height: 260px; object-fit: cover; object-position: center; order: 1; } + .recipe__hero__copy { order: 2; } +} + +/* ══════════════════════════════════════════════════════════════════ + LANDING — LIGHT MODE ADAPTATION + ══════════════════════════════════════════════════════════════════ */ + +/* Light mode landing tokens */ +:root, +[data-theme="light"] { + --land-bg: #f4f1eb; + --land-text: #1c1f29; + --land-muted: rgba(28,31,41,0.55); + --land-accent: #b07d2a; + --land-accent2: #8a4a20; + --land-blob-a: #c9b8e8; + --land-blob-b: #e8c0a0; + --land-blob-c: #a8c4d8; + --land-chip-bg: rgba(28,31,41,0.06); + --land-chip-border: rgba(28,31,41,0.12); + --land-chip-text: rgba(28,31,41,0.7); + --land-toggle-border: rgba(28,31,41,0.15); + --land-toggle-bg: rgba(28,31,41,0.06); + --land-toggle-color: rgba(28,31,41,0.6); + --land-scroll-line: rgba(28,31,41,0.35); + --land-scroll-text: rgba(28,31,41,0.35); + --land-grain: 0.04; +} + +[data-theme="dark"] { + --land-bg: #0d0e11; + --land-text: #f0f2f7; + --land-muted: rgba(240,242,247,0.55); + --land-accent: #e8c87a; + --land-accent2: #c87a5a; + --land-blob-a: #4a3a6e; + --land-blob-b: #6b3a2a; + --land-blob-c: #1a3a5a; + --land-chip-bg: rgba(255,255,255,0.07); + --land-chip-border: rgba(255,255,255,0.12); + --land-chip-text: rgba(240,242,247,0.75); + --land-toggle-border: rgba(255,255,255,0.15); + --land-toggle-bg: rgba(255,255,255,0.06); + --land-toggle-color: rgba(240,242,247,0.7); + --land-scroll-line: rgba(240,242,247,0.5); + --land-scroll-text: rgba(240,242,247,0.4); + --land-grain: 0.03; +} + +/* Smooth theme transition on landing */ +.landing { + transition: background-color 0.4s ease; +} + +/* Use variables everywhere in landing */ +.landing { + background: var(--land-bg); +} + +.landing__eyebrow { + color: var(--land-muted); +} + +.landing__headline { + color: var(--land-text); +} + +.landing__sub { + color: var(--land-muted); +} + +.landing__cta { + background: var(--land-accent); + color: var(--land-bg); +} + +[data-theme="light"] .landing__cta { + color: #fff8ee; +} + +.landing__cta:hover { + filter: brightness(1.08); + background: var(--land-accent); + color: inherit; +} + +/* Blob colors via variables */ +.blob--a { + background: radial-gradient(circle, var(--land-blob-a, #4a3a6e) 0%, transparent 70%); +} +.blob--b { + background: radial-gradient(circle, var(--land-blob-b, #6b3a2a) 0%, transparent 70%); +} +.blob--c { + background: radial-gradient(circle, var(--land-blob-c, #1a3a5a) 0%, transparent 70%); +} + +/* Light mode blobs slightly more opaque */ +[data-theme="light"] .blob { + opacity: 0.45; + filter: blur(90px); +} + +/* Chip styles via variables */ +.chip { + background: var(--land-chip-bg); + border-color: var(--land-chip-border); + transition: background 0.3s, border-color 0.3s; +} +.chip span { + color: var(--land-chip-text); +} + +/* Scroll nudge */ +.scroll-line { + background: linear-gradient(to bottom, transparent, var(--land-scroll-line)); +} +.landing__scroll-nudge span { + color: var(--land-scroll-text); +} + +/* Theme toggle */ +.theme-toggle { + border-color: var(--land-toggle-border); + background: var(--land-toggle-bg); + color: var(--land-toggle-color); + transition: background 0.18s, color 0.18s, border-color 0.18s; +} +.theme-toggle:hover { + background: var(--land-chip-border); + color: var(--land-text); +} diff --git a/data/recipes.json b/data/recipes.json new file mode 100644 index 0000000..700a0a1 --- /dev/null +++ b/data/recipes.json @@ -0,0 +1,240 @@ +[ + { + "slug": "frische-tagliatelle-mit-cremiger-tomatensauce", + "hero": "/assets/pasta-tomato.jpg", + "prep_time": 50, + "cook_time": 10, + "total_time": 60, + "servings": 2, + "featured": false, + "coming_soon": false, + "i18n": { + "en": { + "title": "Tagliatelle with tomato sauce", + "description": "Simple but tasty, short cooking time but self made. The root dish to start in a cozy evening and satisfy the carbs cravings without any hidden additives. Noodles with Tomato Sauce, more hearty isnt possible.", + "category": "Pasta", + "difficulty": "Easy", + "tags": [ + "Pasta", + "Fast", + "Vegetarian" + ], + "ingredients": [ + "300 g flour", + "3 eggs", + "10 cherry tomatoes", + "1/2 clove garlic", + "1 tbsp olive oil", + "80 ml cream", + "Fresh basil", + "Parmesan", + "Salt and pepper" + ], + "utensils": [ + "Rolling pin", + "Optional: Pasta machine", + "Optional: Blender" + ], + "steps": [ + "Pile the flour on your counter, press a well in the center, crack in the eggs. Gradually pull flour into the eggs, then knead 5\u201310 minutes until smooth.", + "Roll the dough to 1\u20132 mm thickness (rolling pin or pasta machine) and cut into tagliatelle.", + "Bring salted water to a boil. Meanwhile, halve tomatoes and sear in olive oil with a pinch of salt until lightly charred.", + "Toast the garlic briefly, then blend tomatoes with cream (or crush in the pan) and simmer to thicken slightly.", + "Boil tagliatelle for 2\u20133 minutes, drain, toss with the sauce, and finish with basil and Parmesan." + ] + }, + "de": { + "title": "Tagliatelle mit Tomatensauce", + "description": "Einfach, aber richtig lecker; kurze Kochzeit und trotzdem hausgemacht. Das Basisgericht f\u00fcr einen gem\u00fctlichen Abend, stillt den Kohlenhydrat-Hunger ohne versteckte Zus\u00e4tze. Nudeln mit Tomatensauce \u2013 herzhafter geht es kaum.", + "category": "Pasta", + "difficulty": "Einfach", + "tags": [ + "Pasta", + "Schnell", + "Vegetarisch" + ], + "ingredients": [ + "300 g Mehl", + "3 Eier", + "10 Cherry-Tomaten", + "1/2 Knoblauchzehe", + "1 EL Oliven\u00f6l", + "80 ml Sahne", + "Frischer Basilikum", + "Parmesan", + "Salz und Pfeffer" + ], + "utensils": [ + "Nudelholz", + "Optional: Nudelmaschine", + "Optional: Mixer" + ], + "steps": [ + "Mehl auf der Arbeitsfl\u00e4che anh\u00e4ufen, eine Kuhle dr\u00fccken, Eier hineingeben. Mehl nach und nach einarbeiten, dann 5\u201310 Minuten kneten, bis der Teig glatt ist.", + "Teig auf 1\u20132 mm ausrollen (mit Nudelholz oder Nudelmaschine) und in Tagliatelle schneiden.", + "Gesalzenes Wasser aufsetzen; w\u00e4hrenddessen Tomaten halbieren und mit Oliven\u00f6l und einer Prise Salz in der Pfanne anr\u00f6sten, bis R\u00f6stnoten entstehen.", + "Knoblauch kurz mitr\u00f6sten, dann Tomaten mit Sahne p\u00fcrieren (Mixer) oder in der Pfanne zerdr\u00fccken und kurz einkochen lassen.", + "Tagliatelle 2\u20133 Minuten kochen, abgie\u00dfen und mit der Sauce vermengen. Mit Basilikum und Parmesan anrichten." + ] + } + } + }, + { + "slug": "oat-pancakes", + "hero": "/assets/pancakes.jpg", + "prep_time": 0, + "cook_time": 0, + "total_time": 0, + "servings": 2, + "featured": false, + "coming_soon": true, + "i18n": { + "en": { + "title": "Oat-Pancakes", + "description": "", + "category": "", + "difficulty": "Easy", + "tags": [], + "ingredients": [], + "utensils": [], + "steps": [] + }, + "de": { + "title": "Hafer-Pfannkuchen", + "description": "", + "category": "", + "difficulty": "Einfach", + "tags": [], + "ingredients": [], + "utensils": [], + "steps": [] + } + } + }, + { + "slug": "steak-with-onion-jam", + "hero": "/assets/steak.JPEG", + "prep_time": 0, + "cook_time": 0, + "total_time": 0, + "servings": 2, + "featured": true, + "coming_soon": false, + "i18n": { + "en": { + "title": "Steak with onion jam", + "description": "A hearty meal after a long day, best enjoyed with a glass of red wine and in good company\u2014because the onion jam still isn\u2019t satisfied on its own.", + "category": "", + "difficulty": "Easy", + "tags": [ + "meat", + "dinner" + ], + "ingredients": [ + "2 x 250g rump steak", + "300g potatoes", + "1 large red onion", + "1/2 clove garlic", + "50ml cream", + "50ml red wine", + "4 carrots (yellow and purple)", + "parmesan", + "Sicilian orange salt", + "thyme", + "1 tbsp butter" + ], + "utensils": [ + "knife", + "cutting board", + "pan", + "pot", + "oven" + ], + "steps": [ + "Take the steak out 30 minutes before cooking", + "Halve the potatoes", + "Halve the carrots", + "Dice the onion", + "Cook the potatoes and carrots until al dente", + "Sear the steak with butter", + "Once both sides are golden brown, bake for 6 minutes at 160\u00b0C (medium); for medium rare, let it rest after searing both sides for 3 minutes", + "Saut\u00e9 the onion in the steak butter, deglaze with red wine, and add cream once the alcohol has evaporated", + "Plate and serve!" + ] + }, + "de": { + "title": "Steak mit Zwiebelmarmelade", + "description": "Eine schwere Mahlzeit nach einem langen Tag, am besten zu einem Schluck Rotwein und in Gesellschaft genie\u00dfen, da die Zwiebelmarmelade noch nicht genug davon hat.", + "category": "", + "difficulty": "Einfach", + "tags": [ + "Fleisch", + "Abendessen" + ], + "ingredients": [ + "2 x 250g Rumpsteak", + "300gr Kartoffeln", + "1 Gro\u00dfe Rote Zwiebel", + "1/2 Knoblauch-Zehe", + "50ml Sahne", + "50ml Rotwein", + "4 Karotten (Geld und Lila)", + "Parmesan", + "Sizilianisches Orangensalz", + "Thymian", + "1 EL Butter" + ], + "utensils": [ + "Messer", + "Brett", + "Pfanne", + "Topf", + "Ofen" + ], + "steps": [ + "Steak 30 Minuten vor dem anbraten raus legen", + "Kartoffeln halbieren", + "Karotten halbieren", + "Zwiebel w\u00fcrfeln", + "Kartoffeln und Karotten al dente kochen", + "Steak anbraten mit Butter", + "sobald beide seiten goldbraun sind f\u00fcr 6 Minuten bei 160grad backen (Medium) f\u00fcr Medium Rare nur ruhen lassen nachdem es von beiden Seiten f\u00fcr 3 Minuten angebraten wurde", + "Zwiebel in Steak-Butter anschwitzen, mit Rotwein abl\u00f6schen und Sahne dazu geben nachdem er Alkohol verd\u00fcnstet ist", + "Anrichten!" + ] + } + } + }, + { + "slug": "oat-cake", + "hero": "/assets/haferkuchen.JPEG", + "prep_time": 0, + "cook_time": 0, + "total_time": 0, + "servings": 2, + "featured": false, + "coming_soon": true, + "i18n": { + "en": { + "title": "Oat cake", + "description": "", + "category": "", + "difficulty": "Easy", + "tags": [], + "ingredients": [], + "utensils": [], + "steps": [] + }, + "de": { + "title": "Haferkuchen", + "description": "", + "category": "", + "difficulty": "Einfach", + "tags": [], + "ingredients": [], + "utensils": [], + "steps": [] + } + } + } +] \ No newline at end of file diff --git a/error.log.20260313.gz b/error.log.20260313.gz new file mode 100644 index 0000000..a669362 Binary files /dev/null and b/error.log.20260313.gz differ diff --git a/helpers.php b/helpers.php new file mode 100644 index 0000000..2b63d3d --- /dev/null +++ b/helpers.php @@ -0,0 +1,131 @@ + $value) { + $localized[$key] = $value; + } + return $localized; +} + +function localize_recipes(array $recipes, string $lang): array { + return array_map(function ($recipe) use ($lang) { + return localize_recipe($recipe, $lang); + }, $recipes); +} + +function filter_recipes(array $recipes, ?string $query = null, ?string $tag = null, string $lang = 'en'): array { + $query = $query ? strtolower($query) : null; + $tag = $tag ? strtolower($tag) : null; + + $localized = localize_recipes($recipes, $lang); + + return array_values(array_filter($localized, function ($recipe) use ($query, $tag) { + $matchesQuery = true; + if ($query) { + $haystack = strtolower(($recipe['title'] ?? '') . ' ' . ($recipe['description'] ?? '')); + $matchesQuery = strpos($haystack, $query) !== false; + } + + $matchesTag = true; + if ($tag) { + $tags = array_map('strtolower', $recipe['tags'] ?? []); + $matchesTag = in_array($tag, $tags, true); + } + + return $matchesQuery && $matchesTag; + })); +} + +function format_minutes(int $minutes): string { + if ($minutes < 60) { + return $minutes . ' min'; + } + $hours = intdiv($minutes, 60); + $mins = $minutes % 60; + return $hours . ' hr' . ($hours > 1 ? 's ' : ' ') . ($mins ? $mins . ' min' : ''); +} + +function e(?string $value): string { + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +function normalize_asset_path(string $path): string { + if (!str_starts_with($path, '/assets/')) { + return $path; + } + + $absolute = __DIR__ . $path; + if (file_exists($absolute)) { + return $path; + } + + $dir = dirname($absolute); + $target = basename($absolute); + if (!is_dir($dir)) { + return $path; + } + + foreach (scandir($dir) ?: [] as $entry) { + if (strcasecmp($entry, $target) === 0) { + return rtrim(dirname($path), '/') . '/' . $entry; + } + } + + return $path; +} + +/** + * Build a URL to the current path with a specific language parameter, preserving other query params. + */ +function lang_url(string $lang): string { + $params = $_GET; + $params['lang'] = $lang; + $path = strtok($_SERVER['REQUEST_URI'], '?') ?: '/'; + return $path . '?' . http_build_query($params); +} diff --git a/index.htm b/index.htm new file mode 100644 index 0000000..c35d9ef --- /dev/null +++ b/index.htm @@ -0,0 +1,47 @@ + + + + +flixcooks.at + + +

flixcooks.at

diff --git a/index.php b/index.php new file mode 100644 index 0000000..46281fa --- /dev/null +++ b/index.php @@ -0,0 +1,287 @@ + [ + 'home' => 'Home', + 'latest' => 'Latest', + 'basics' => 'Basics', + 'eyebrow' => 'Seasonal, unfussy, ridiculously tasty.', + 'headline' => 'Cook better weeknights and brighter weekends.', + 'subhead' => 'Search for a craving, filter by vibe, or jump into this week\'s featured recipe.', + 'search_placeholder' => 'Search for pasta, brunch, sauce...', + 'search_button' => 'Search', + 'tag_clear' => 'Clear', + 'featured' => 'Featured', + 'latest_drops' => 'Latest drops', + 'count_suffix_default' => ' recipes ready to cook.', + 'count_suffix_filtered' => ' recipes matching your filter.', + 'kitchen_basics' => 'Kitchen basics', + 'basics_sub' => 'Quick wins that upgrade everything else.', + 'servings' => 'servings', + 'cook_it' => 'Cook it', + 'coming_features' => 'Coming soon', + 'coming_features_sub' => 'Features I’m shipping next.', + ], + 'de' => [ + 'home' => 'Start', + 'latest' => 'Neueste', + 'basics' => 'Basics', + '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_button' => 'Suchen', + 'tag_clear' => 'Zurücksetzen', + 'featured' => 'Highlight', + 'latest_drops' => 'Frisch dazugekommen', + 'count_suffix_default' => ' Rezepte bereit zum Kochen.', + 'count_suffix_filtered' => ' Rezepte passend zum Filter.', + 'kitchen_basics' => 'Küchenbasics', + 'basics_sub' => 'Kleine Tricks, die alles besser machen.', + 'servings' => 'Portionen', + 'cook_it' => 'Nachkochen', + 'coming_features' => 'Demnächst', + 'coming_features_sub' => 'Features, an denen ich gerade baue.', + ], +]; +$t = $copy[$lang]; + +$allRecipes = load_recipes(); +$localizedAll = localize_recipes($allRecipes, $lang); +$recipes = filter_recipes($allRecipes, $q ?: null, $tag ?: null, $lang); +$recipes = array_values(array_filter($recipes, fn($r) => empty($r['coming_soon']))); // hide coming soon from main list + +$comingSoon = array_values(array_filter($localizedAll, fn($r) => !empty($r['coming_soon']))); +$placeholderTitle = $lang === 'de' ? 'Bald verfügbar' : 'Coming soon'; +$comingSoonDisplay = array_pad($comingSoon, 5, [ + 'hero' => '/assets/placeholder.svg', + 'title' => $placeholderTitle, +]); + +$featured = null; +foreach ($allRecipes as $recipe) { + if (!empty($recipe['featured']) && empty($recipe['coming_soon'])) { + $featured = $recipe; + break; + } +} +$featured = $featured ?? ($allRecipes[0] ?? null); +$featured = $featured ? localize_recipe($featured, $lang) : null; + +// Build tag cloud +$allTags = []; +foreach ($localizedAll as $recipe) { + foreach ($recipe['tags'] ?? [] as $tTag) { + $allTags[] = $tTag; + } +} +$allTags = array_values(array_unique($allTags)); + +$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', + ]; + +$pageTitle = $lang === 'de' + ? 'FlixCooks | Moderner Foodblog für schnelle Alltagsküche' + : 'FlixCooks | Modern food blog for busy home cooks'; +$description = $lang === 'de' + ? 'Frische, schnelle Rezepte mit klaren Schritten und großem Geschmack. Gebaut für Hetzner Webhosting in einfachem PHP.' + : 'Fresh, fast recipes with clear steps, smart prep notes, and big flavor. Built for Hetzner web hosting in plain PHP.'; +$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN'; +$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de'); +$navHome = $t['home']; +$navLatest = $t['latest']; +$navBasics = $t['basics']; + +// Small helpers for URLs +$langQuery = ['lang' => $lang]; +$recipeUrl = fn(array $recipe) => '/recipe.php?' . http_build_query(['slug' => $recipe['slug']] + $langQuery); +$tagUrl = fn(string $tagValue) => '/index.php?' . http_build_query(['tag' => $tagValue] + $langQuery); + +include __DIR__ . '/partials/head.php'; +?> + + +
+ + +
+

+ +

+

+ + Besser kochen + unter der Woche. + Glänzen am Wochenende. + + Cook better + weeknights. + Brighter weekends. + +

+

+ + + + +
+ +
+ +
+ + +?> + + +
+
+

+ +
+
+ +
+
+ <?php echo e($cs['title'] ?? 'Coming soon'); ?> +
+
+
+ +
+
+ + +
+
+

+

+

+ +
+ + # + + + + +
+
+ +
+ <?php echo e($featured['title']); ?> +
+

+

+

+
+ + 🍽 +
+ +
+
+ +
+ +
+
+

+

+
+
+ +
+ + <?php echo e($recipe['title']); ?> + + +
+

+

+
+ + 🙂 +
+
+ + # + +
+
+
+ +
+
+ +
+
+

+

+
+
+ +
+ +

+
+ +
+
+ +
+ diff --git a/maintenance.php b/maintenance.php new file mode 100644 index 0000000..4fde4c8 --- /dev/null +++ b/maintenance.php @@ -0,0 +1,3 @@ + + + + + + + FlixCooks | Wartungsmodus + + + +
+
+

FlixCooks

+
+

Website in Bearbeitung

+

Wird in Kürze veröffentlicht

+

Countdown lädt...

+
+ + + +
+
+ + + diff --git a/partials/footer.php b/partials/footer.php new file mode 100644 index 0000000..1500c3c --- /dev/null +++ b/partials/footer.php @@ -0,0 +1,72 @@ + + + + + + + + + + diff --git a/partials/head.php b/partials/head.php new file mode 100644 index 0000000..11e77ce --- /dev/null +++ b/partials/head.php @@ -0,0 +1,21 @@ + + + + + + + + <?php echo e($pageTitle); ?> + + + + + + + + + + diff --git a/partials/header.php b/partials/header.php new file mode 100644 index 0000000..866b4cb --- /dev/null +++ b/partials/header.php @@ -0,0 +1,31 @@ + + diff --git a/recipe.php b/recipe.php new file mode 100644 index 0000000..52548e8 --- /dev/null +++ b/recipe.php @@ -0,0 +1,150 @@ + [ + 'home' => 'Home', + 'latest' => 'Latest', + 'basics' => 'Basics', + 'not_found' => 'Recipe not found', + 'not_found_sub' => 'Try searching for something else.', + 'back_home' => '← Back to all recipes', + 'ingredients' => 'Ingredients', + 'utensils' => 'Utensils', + 'steps' => 'Steps', + 'also_tasty' => 'Also tasty', + 'more_dishes' => 'More dishes to keep you cooking.', + 'prep' => 'Prep', + 'cook' => 'Cook', + 'total' => 'Total', + 'serves' => 'Serves', + 'level' => 'Level', + ], + 'de' => [ + 'home' => 'Start', + 'latest' => 'Neueste', + 'basics' => 'Basics', + 'not_found' => 'Rezept nicht gefunden', + 'not_found_sub' => 'Versuche eine andere Suche.', + 'back_home' => '← Zurück zu allen Rezepten', + 'ingredients' => 'Zutaten', + 'utensils' => 'Küchenwerkzeug', + 'steps' => 'Schritte', + 'also_tasty' => 'Auch lecker', + 'more_dishes' => 'Noch mehr zum Nachkochen.', + 'prep' => 'Vorbereitung', + 'cook' => 'Garen', + 'total' => 'Gesamt', + 'serves' => 'Portionen', + 'level' => 'Level', + ], +]; +$t = $copy[$lang]; + +$allRecipes = load_recipes(); +$slug = isset($_GET['slug']) ? trim($_GET['slug']) : ''; +$recipeRaw = $slug ? find_recipe_by_slug($allRecipes, $slug) : null; +$recipe = $recipeRaw ? localize_recipe($recipeRaw, $lang) : null; +$allRecipesLocalized = localize_recipes($allRecipes, $lang); + +$langSwitchLabel = $lang === 'de' ? 'EN' : 'DE'; +$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de'); +$navHome = $t['home']; +$navLatest = $t['latest']; +$navBasics = $t['basics']; + +if (!$recipe) { + http_response_code(404); + $pageTitle = $t['not_found'] . ' | FlixCooks'; + $description = $t['not_found_sub']; + include __DIR__ . '/partials/head.php'; + include __DIR__ . '/partials/header.php'; + ?> +
+

+

+ +
+ + +
+
+ <?php echo e($recipe['title']); ?> +
+

+

+

+
+ : + : + : + : + : +
+ +
+
+ +
+
+

+
    + +
  • + +
+
+ + +
+

+
    + +
  • + +
+
+ + +
+

+
    + +
  1. + +
+
+
+ +
+
+

+

+
+
+ + +
+

+

+
+ + 🙂 +
+
+ +
+
+
+ +