refactor: unify recipe routes into [recipeLang] slug with full bilingual support

Consolidate /rezepte and /recipes routes into single [recipeLang] structure to eliminate code duplication. All pages now use conditional API routing and reactive labels based on language parameter.

- Merge duplicate route structures into /[recipeLang] with 404 for invalid slugs
- Add English API endpoints for search, favorites, tags, and categories
- Implement language dropdown in header with localStorage persistence
- Convert all pages to use Svelte 5 runes (, , )
- Add German-only redirects (301) for add/edit pages
- Make all view pages (list, detail, filters, search, favorites) fully bilingual
- Remove floating language switcher in favor of header dropdown
This commit is contained in:
2025-12-26 21:19:27 +01:00
parent 36a7fac39a
commit 6de3d76504
72 changed files with 417511 additions and 1097 deletions

28
.env.example Normal file
View File

@@ -0,0 +1,28 @@
# Database Configuration
MONGO_URL="mongodb://user:password@host:port/database?authSource=admin"
# Authentication Secrets (runtime only - not embedded in build)
AUTHENTIK_ID="your-authentik-client-id"
AUTHENTIK_SECRET="your-authentik-client-secret"
# Static Configuration (embedded in build - OK to be public)
AUTHENTIK_ISSUER="https://sso.example.com/application/o/your-app/"
# File Storage
IMAGE_DIR="/path/to/static/files"
# Optional: Development Settings
# DEV_DISABLE_AUTH="true"
# ORIGIN="http://127.0.0.1:3000"
# Optional: Additional Configuration
# BEARER_TOKEN="your-bearer-token"
# COOKIE_SECRET="your-cookie-secret"
# PEPPER="your-pepper-value"
# ALLOW_REGISTRATION="1"
# AUTH_SECRET="your-auth-secret"
# USDA_API_KEY="your-usda-api-key"
# Translation Service (DeepL API)
DEEPL_API_KEY="your-deepl-api-key"
DEEPL_API_URL="https://api-free.deepl.com/v2/translate" # Use https://api.deepl.com/v2/translate for Pro

13
.mcp.json Normal file
View File

@@ -0,0 +1,13 @@
{
"mcpServers": {
"svelte": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@sveltejs/mcp"
],
"env": {}
}
}
}

15
.mcp.json.bak Normal file
View File

@@ -0,0 +1,15 @@
{
"mcpServers": {
"svelte": {
"type": "stdio",
"command": "npx",
"env": {
},
"args": [
"-y",
"@sveltejs/mcp"
]
}
}
}

57
dove.svg Normal file
View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="-10 0 2058 2048"
id="svg1"
sodipodi:docname="dove.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.20494477"
inkscape:cx="875.84571"
inkscape:cy="3122.7925"
inkscape:window-width="1436"
inkscape:window-height="1749"
inkscape:window-x="1440"
inkscape:window-y="47"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
fill="currentColor"
d="M1935 90q0 32 -38 91q-21 29 -56 90q-20 55 -63 164q-35 86 -95 143q-22 -21 -43 -45q51 -49 85 -139q49 -130 61 -152q-126 48 -152 63q-76 46 -95 128q-27 -18 -58 -25q28 -104 97 -149q31 -20 138 -52q90 -28 137 -74l29 -39q22 -30 32 -30q21 0 21 26zM1714 653 q-90 30 -113 43q-65 36 -65 90q0 19 20 119q23 116 23 247q0 169 -103 299q-111 141 -275 141q-254 0 -283 87q-16 104 -31 207q-27 162 -76 162q-21 0 -41 -20q-16 -19 -32 -37q-10 3 -33 22q-18 15 -39 15q-28 0 -50 -44.5t-30 -44.5q-10 0 -35.5 11.5t-41.5 11.5 q-47 0 -58.5 -45.5t-21.5 -45.5t-29.5 2.5t-29.5 2.5q-46 0 -46 -30q0 -16 14 -44.5t14 -44.5q0 -8 -46.5 -25.5t-46.5 -48.5q0 -34 35.5 -52t99.5 -31q91 -19 103 -22q113 -32 171 -93q37 -39 105 -165q34 -64 43 -82q26 -53 31 -85q-129 -67 -224 -76q-33 0 -96 -11 q-36 -13 -36 -41q0 -7 2 -19.5t2 -19.5q0 -20 -67.5 -42t-67.5 -64q0 -11 8.5 -30t8.5 -30q0 -15 -79 -39t-79 -63q0 -16 9 -45t9 -45q0 -20 -29 -43q-23 -17 -46 -33q-49 -44 -49 -215q0 -8 1 -15q91 53 194 68l282 16q202 12 304 59q143 65 143 210q0 15 -2 44t-2 44 q0 122 78 122q73 0 108 -133q16 -70 32 -139q21 -81 57 -119q46 -51 130 -51q71 0 122 61q90 107 154 149zM1597 636q-25 -22 -77 -91q-30 -40 -75 -40q-91 0 -131 115q-30 106 -59 213q-44 115 -144 115q-146 0 -146 -180q0 -16 2.5 -46.5t2.5 -46.5q0 -62 -19 -87 q-70 -92 -303 -115q-173 -9 -347 -18q-55 -6 -116 -30v34q0 27 57.5 73.5t57.5 91.5q0 16 -10.5 45t-10.5 44q1 1 7 1q3 0 7 1q146 36 146 105q0 13 -8.5 32.5t-8.5 27.5h10q5 0 9 1q61 15 86 36q32 28 28 85q173 15 372 107q-7 77 -80 215q-67 128 -127 195 q-67 74 -169 104q-96 24 -193 47q-10 3 -29 13q86 18 86 70q0 19 -19 62q15 -5 33 -5q42 0 59 26q8 11 22 61l-1 3q10 0 34.5 -11.5t42.5 -11.5q55 0 88 84q38 -32 64 -32q37 0 66 41q25 -53 33 -151q10 -112 23 -154q43 -136 337 -136q116 0 215 -108q105 -114 105 -277 q0 -23 -12 -112l-28 -207q-4 -30 -4 -42q0 -97 124 -147zM1506 605q0 38 -38 38q-39 0 -39 -38t39 -38q38 0 38 38z"
id="path1" />
<path
d="m 1724.44,1054.6641 c -31.1769,-18 -37.7653,-42.5884 -19.7653,-73.76528 5.3333,-9.2376 12.354,-16.7312 21.0621,-22.4808 6.2201,-4.1068 44.7886,-7.2427 115.7055,-9.4077 70.9168,-2.1649 110.128,-1.0807 117.6336,3.2526 30.0222,17.3334 35.5333,42.45448 16.5333,75.36348 -7.3333,12.7017 -16.1754,20.6833 -26.5263,23.9448 -24.5645,1.2137 -56.7805,3.0135 -96.648,5.3994 -72.6282,5.7957 -115.2931,5.0269 -127.9949,-2.3065 z"
style="fill:currentColor"
id="path1-2"
sodipodi:nodetypes="ssssssccs" />
<path
d="m 386.57764,1262.0569 c 53.44793,-14.3214 85.17574,-2.8075 95.18337,34.5417 9.83517,36.7052 -12.29319,62.3047 -66.38503,76.7986 l -82.1037,21.9996 c -54.09184,14.4939 -86.05533,3.3882 -95.89047,-33.317 -10.00766,-37.3491 12.67841,-63.4432 68.05807,-78.2821 z"
style="fill:currentColor"
id="path1-7" />
<path
d="m 1115.7599,372.22724 c 14.3213,53.44793 2.8073,85.17581 -34.5418,95.18323 -36.705,9.83527 -62.3047,-12.29323 -76.7986,-66.38485 l -21.99962,-82.10394 c -14.4939,-54.09162 -3.3882,-86.05531 33.31712,-95.89019 37.349,-10.00765 63.4431,12.67818 78.2821,68.05802 z"
style="fill:currentColor"
id="path1-7-6" />
<path
d="m 1184.6228,1956.284 c -4.807,-8.0003 -6.8298,-42.7561 -6.0684,-104.2674 0.7614,-61.5113 2.7093,-100.0139 5.8437,-115.508 3.1343,-15.4941 11.8445,-27.5329 26.1306,-36.117 30.2866,-18.198 54.7006,-11.868 73.242,18.99 5.4937,9.1432 8.145,43.3269 7.9537,102.5512 -0.081,52.9359 -1.4296,89.5231 -4.0464,109.7617 -2.276,16.9226 -11.1284,30.0192 -26.5575,39.29 -33.1439,19.9148 -58.643,15.0146 -76.4977,-14.7005 z"
style="fill:currentColor"
id="path1-6" />
<path
d="m 1773.3127,1737.6952 c -9.0153,-2.4157 -34.6139,-26.0118 -76.7955,-70.7882 -42.1816,-44.7764 -67.5266,-73.826 -76.035,-87.1489 -8.5084,-13.3228 -10.6057,-28.0334 -6.2922,-44.1323 9.145,-34.1293 31.1041,-46.5353 65.8774,-37.2179 10.3033,2.7609 35.9565,25.5088 76.9595,68.2441 36.7142,38.1352 61.1596,65.3907 73.3362,81.7668 10.1182,13.7541 12.8479,29.3245 8.1892,46.7113 -10.0077,37.3492 -31.7542,51.5375 -65.2396,42.5651 z"
style="fill:currentColor"
id="path1-6-9" />
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

38
extract_crown.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Extract crown emoji from Symbola font as SVG."""
import fontforge
import sys
# Path to Symbola font
font_path = "/usr/share/fonts/TTF/Symbola.ttf"
# Dove emoji Unicode codepoint
dove_codepoint = 0x1F54A # U+1F54A 🕊️
# Output SVG file
output_path = "dove.svg"
try:
# Open the font
font = fontforge.open(font_path)
# Select the dove glyph by Unicode codepoint
if dove_codepoint in font:
glyph = font[dove_codepoint]
# Export as SVG
glyph.export(output_path)
print(f"✓ Successfully extracted dove emoji to {output_path}")
print(f" Glyph name: {glyph.glyphname}")
print(f" Unicode: U+{dove_codepoint:04X}")
else:
print(f"✗ Dove emoji (U+{dove_codepoint:04X}) not found in font")
sys.exit(1)
font.close()
except Exception as e:
print(f"✗ Error: {e}")
sys.exit(1)

View File

@@ -4,6 +4,7 @@
import Search from './Search.svelte'; import Search from './Search.svelte';
export let icons export let icons
export let active_icon export let active_icon
export let routePrefix = '/rezepte'
</script> </script>
<style> <style>
@@ -68,7 +69,7 @@
<div class=flex> <div class=flex>
{#each icons as icon, i} {#each icons as icon, i}
<a class:active={active_icon == icon} href="/rezepte/icon/{icon}">{icon}</a> <a class:active={active_icon == icon} href="{routePrefix}/icon/{icon}">{icon}</a>
{/each} {/each}
</div> </div>
<section> <section>

View File

@@ -3,6 +3,12 @@
export let englishUrl: string; export let englishUrl: string;
export let currentLang: 'de' | 'en' = 'de'; export let currentLang: 'de' | 'en' = 'de';
export let hasTranslation: boolean = true; export let hasTranslation: boolean = true;
function setLanguagePreference(lang: 'de' | 'en') {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('preferredLanguage', lang);
}
}
</script> </script>
<style> <style>
@@ -101,6 +107,7 @@
href={germanUrl} href={germanUrl}
class:active={currentLang === 'de'} class:active={currentLang === 'de'}
aria-label="Switch to German" aria-label="Switch to German"
onclick={() => setLanguagePreference('de')}
> >
<span class="flag">🇩🇪</span> <span class="flag">🇩🇪</span>
<span class="label">DE</span> <span class="label">DE</span>
@@ -110,6 +117,7 @@
href={englishUrl} href={englishUrl}
class:active={currentLang === 'en'} class:active={currentLang === 'en'}
aria-label="Switch to English" aria-label="Switch to English"
onclick={() => setLanguagePreference('en')}
> >
<span class="flag">🇬🇧</span> <span class="flag">🇬🇧</span>
<span class="label">EN</span> <span class="label">EN</span>

View File

@@ -2,9 +2,10 @@
import '$lib/css/nordtheme.css'; import '$lib/css/nordtheme.css';
import Recipes from '$lib/components/Recipes.svelte'; import Recipes from '$lib/components/Recipes.svelte';
import Search from './Search.svelte'; import Search from './Search.svelte';
let months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"] export let months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
let month : number; let month : number;
export let active_index; export let active_index;
export let routePrefix = '/rezepte';
</script> </script>
<style> <style>
@@ -37,7 +38,7 @@ a.month:hover,
<div class=months> <div class=months>
{#each months as month, i} {#each months as month, i}
<a class:active={i == active_index} class=month href="/rezepte/season/{i+1}">{month}</a> <a class:active={i == active_index} class=month href="{routePrefix}/season/{i+1}">{month}</a>
{/each} {/each}
</div> </div>
<section> <section>

View File

@@ -1,17 +1,74 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
export let user; import { goto } from '$app/navigation';
import { page } from '$app/stores';
let { user, showLanguageSelector = false } = $props();
let currentLang = $state('de');
let currentPath = $state('');
$effect(() => {
// Update current language and path when page changes
if (typeof window !== 'undefined') {
const path = window.location.pathname;
currentPath = path;
if (path.startsWith('/recipes')) {
currentLang = 'en';
} else if (path.startsWith('/rezepte')) {
currentLang = 'de';
}
}
});
function toggle_options(){ function toggle_options(){
const el = document.querySelector("#options") const el = document.querySelector("#options")
el.hidden = !el.hidden el.hidden = !el.hidden
} }
function toggle_language_options(){
const el = document.querySelector("#language-options")
el.hidden = !el.hidden
}
function switchLanguage(lang: 'de' | 'en') {
// Update the current language state immediately
currentLang = lang;
// Store preference
if (typeof localStorage !== 'undefined') {
localStorage.setItem('preferredLanguage', lang);
}
// Get the current path directly from window
const path = typeof window !== 'undefined' ? window.location.pathname : currentPath;
// Convert current path to target language
let newPath = path;
if (lang === 'en' && path.startsWith('/rezepte')) {
newPath = path.replace('/rezepte', '/recipes');
} else if (lang === 'de' && path.startsWith('/recipes')) {
newPath = path.replace('/recipes', '/rezepte');
} else if (path === '/' || (!path.startsWith('/rezepte') && !path.startsWith('/recipes'))) {
// On main page or other pages, go to recipe home
newPath = lang === 'en' ? '/recipes' : '/rezepte';
}
goto(newPath);
}
onMount( () => { onMount( () => {
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
const el = document.querySelector("#button") const userButton = document.querySelector("#button")
if(!el.contains(e.target)){ const langButton = document.querySelector("#language-button")
document.querySelector("#options").hidden = true
if(userButton && !userButton.contains(e.target)){
const options = document.querySelector("#options");
if (options) options.hidden = true;
}
if(langButton && !langButton.contains(e.target)){
const langOptions = document.querySelector("#language-options");
if (langOptions) langOptions.hidden = true;
} }
}) })
}) })
@@ -68,6 +125,59 @@
background-position: center; background-position: center;
background-size: contain; background-size: contain;
} }
.language-selector{
position: relative;
}
#language-button{
width: auto;
padding: 0.5rem 1rem;
border-radius: 8px;
background-color: var(--nord3);
color: white;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background-color 100ms;
}
#language-button:hover{
background-color: var(--nord2);
}
#language-options{
--bg_color: var(--nord3);
box-sizing: border-box;
border-radius: 5px;
position: absolute;
right: 0;
top: calc(100% + 10px);
background-color: var(--bg_color);
width: 10ch;
padding: 0.5rem;
z-index: 1000;
}
#language-options button{
width: 100%;
background-color: transparent;
color: white;
border: none;
padding: 0.5rem;
margin: 0;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
text-align: left;
transition: background-color 100ms;
}
#language-options button:hover{
background-color: var(--nord2);
}
#language-options button.active{
background-color: var(--nord14);
}
.header-right{
display: flex;
align-items: center;
gap: 0.5rem;
}
#options{ #options{
--bg_color: var(--nord3); --bg_color: var(--nord3);
box-sizing: border-box; box-sizing: border-box;
@@ -132,6 +242,29 @@ h2 + p{
} }
</style> </style>
<div class="header-right">
{#if showLanguageSelector}
<div class="language-selector">
<button on:click={toggle_language_options} id="language-button">
{currentLang.toUpperCase()}
</button>
<div id="language-options" hidden>
<button
class:active={currentLang === 'de'}
on:click={() => switchLanguage('de')}
>
DE
</button>
<button
class:active={currentLang === 'en'}
on:click={() => switchLanguage('en')}
>
EN
</button>
</div>
</div>
{/if}
{#if user} {#if user}
<button on:click={toggle_options} style="background-image: url(https://bocken.org/static/user/thumb/{user.nickname}.webp)" id=button> <button on:click={toggle_options} style="background-image: url(https://bocken.org/static/user/thumb/{user.nickname}.webp)" id=button>
<div id=options class="speech top" hidden> <div id=options class="speech top" hidden>
@@ -146,3 +279,4 @@ h2 + p{
{:else} {:else}
<a class=entry href=/login>Log In</a> <a class=entry href=/login>Log In</a>
{/if} {/if}
</div>

View File

@@ -1,7 +1,20 @@
<script lang="ts"> <script lang="ts">
import "$lib/css/nordtheme.css"; import "$lib/css/nordtheme.css";
import LinksGrid from "$lib/components/LinksGrid.svelte"; import LinksGrid from "$lib/components/LinksGrid.svelte";
export let data; import { onMount } from 'svelte';
let { data } = $props();
let recipesUrl = $state('/rezepte');
onMount(() => {
// Check localStorage for preferred language
const preferredLanguage = localStorage.getItem('preferredLanguage');
if (preferredLanguage === 'en') {
recipesUrl = '/recipes';
} else {
recipesUrl = '/rezepte';
}
});
</script> </script>
<style> <style>
.hero{ .hero{
@@ -80,7 +93,7 @@ section h2{
<h2>Seiten</h2> <h2>Seiten</h2>
<LinksGrid> <LinksGrid>
<a href="rezepte"> <a href={recipesUrl}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M240 144A96 96 0 1 0 48 144a96 96 0 1 0 192 0zm44.4 32C269.9 240.1 212.5 288 144 288C64.5 288 0 223.5 0 144S64.5 0 144 0c68.5 0 125.9 47.9 140.4 112h71.8c8.8-9.8 21.6-16 35.8-16H496c26.5 0 48 21.5 48 48s-21.5 48-48 48H392c-14.2 0-27-6.2-35.8-16H284.4zM144 80a64 64 0 1 1 0 128 64 64 0 1 1 0-128zM400 240c13.3 0 24 10.7 24 24v8h96c13.3 0 24 10.7 24 24s-10.7 24-24 24H280c-13.3 0-24-10.7-24-24s10.7-24 24-24h96v-8c0-13.3 10.7-24 24-24zM288 464V352H512V464c0 26.5-21.5 48-48 48H336c-26.5 0-48-21.5-48-48zM48 320h80 16 32c26.5 0 48 21.5 48 48s-21.5 48-48 48H160c0 17.7-14.3 32-32 32H64c-17.7 0-32-14.3-32-32V336c0-8.8 7.2-16 16-16zm128 64c8.8 0 16-7.2 16-16s-7.2-16-16-16H160v32h16zM24 464H200c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24s10.7-24 24-24z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M240 144A96 96 0 1 0 48 144a96 96 0 1 0 192 0zm44.4 32C269.9 240.1 212.5 288 144 288C64.5 288 0 223.5 0 144S64.5 0 144 0c68.5 0 125.9 47.9 140.4 112h71.8c8.8-9.8 21.6-16 35.8-16H496c26.5 0 48 21.5 48 48s-21.5 48-48 48H392c-14.2 0-27-6.2-35.8-16H284.4zM144 80a64 64 0 1 1 0 128 64 64 0 1 1 0-128zM400 240c13.3 0 24 10.7 24 24v8h96c13.3 0 24 10.7 24 24s-10.7 24-24 24H280c-13.3 0-24-10.7-24-24s10.7-24 24-24h96v-8c0-13.3 10.7-24 24-24zM288 464V352H512V464c0 26.5-21.5 48-48 48H336c-26.5 0-48-21.5-48-48zM48 320h80 16 32c26.5 0 48 21.5 48 48s-21.5 48-48 48H160c0 17.7-14.3 32-32 32H64c-17.7 0-32-14.3-32-32V336c0-8.8 7.2-16 16-16zm128 64c8.8 0 16-7.2 16-16s-7.2-16-16-16H160v32h16zM24 464H200c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24s10.7-24 24-24z"/></svg>
<h3>Rezepte</h3> <h3>Rezepte</h3>
</a> </a>

View File

@@ -0,0 +1,17 @@
import type { LayoutServerLoad } from "./$types"
import { error } from "@sveltejs/kit";
export const load : LayoutServerLoad = async ({locals, params}) => {
// Validate recipeLang parameter
if (params.recipeLang !== 'rezepte' && params.recipeLang !== 'recipes') {
throw error(404, 'Not found');
}
const lang = params.recipeLang === 'recipes' ? 'en' : 'de';
return {
session: await locals.auth(),
lang,
recipeLang: params.recipeLang
}
};

View File

@@ -0,0 +1,34 @@
<script>
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
let { data } = $props();
let user = $derived(data.session?.user);
const isEnglish = $derived(data.lang === 'en');
const labels = $derived({
allRecipes: isEnglish ? 'All Recipes' : 'Alle Rezepte',
favorites: isEnglish ? 'Favorites' : 'Favoriten',
inSeason: isEnglish ? 'In Season' : 'In Saison',
category: isEnglish ? 'Category' : 'Kategorie',
icon: 'Icon',
keywords: isEnglish ? 'Keywords' : 'Stichwörter',
tips: isEnglish ? 'Tips' : 'Tipps'
});
</script>
<Header>
<ul class=site_header slot=links>
<li><a href="/{data.recipeLang}">{labels.allRecipes}</a></li>
{#if user}
<li><a href="/{data.recipeLang}/favorites">{labels.favorites}</a></li>
{/if}
<li><a href="/{data.recipeLang}/season">{labels.inSeason}</a></li>
<li><a href="/{data.recipeLang}/category">{labels.category}</a></li>
<li><a href="/{data.recipeLang}/icon">{labels.icon}</a></li>
<li><a href="/{data.recipeLang}/tag">{labels.keywords}</a></li>
<li><a href="/rezepte/tips-and-tricks">{labels.tips}</a></li>
</ul>
<UserHeader slot=right_side {user} showLanguageSelector={true}></UserHeader>
<slot></slot>
</Header>

View File

@@ -1,10 +1,13 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites"; import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export async function load({ fetch, locals }) { export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
let current_month = new Date().getMonth() + 1 let current_month = new Date().getMonth() + 1
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month); const res_season = await fetch(`${apiBase}/items/in_season/` + current_month);
const res_all_brief = await fetch(`/api/rezepte/items/all_brief`); const res_all_brief = await fetch(`${apiBase}/items/all_brief`);
const item_season = await res_season.json(); const item_season = await res_season.json();
const item_all_brief = await res_all_brief.json(); const item_all_brief = await res_all_brief.json();

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import type { PageData } from './$types';
import MediaScroller from '$lib/components/MediaScroller.svelte';
import AddButton from '$lib/components/AddButton.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
let { data }: { data: PageData } = $props();
let current_month = new Date().getMonth() + 1
const isEnglish = $derived(data.lang === 'en');
const categories = $derived(isEnglish
? ["Main Course", "Pasta", "Bread", "Dessert", "Soup", "Side Dish", "Salad", "Cake", "Breakfast", "Sauce", "Ingredient", "Drink", "Spread", "Cookie", "Snack"]
: ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Snack"]);
const labels = $derived({
title: isEnglish ? 'Recipes' : 'Rezepte',
subheading: isEnglish
? `${data.all_brief.length} recipes and constantly growing...`
: `${data.all_brief.length} Rezepte und stetig wachsend...`,
inSeason: isEnglish ? 'In Season' : 'In Saison',
metaTitle: isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte',
metaDescription: isEnglish
? "A constantly growing collection of recipes from Bocken's kitchen."
: "Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche.",
metaAlt: isEnglish ? 'Pasta al Ragu with Linguine' : 'Pasta al Ragu mit Linguine'
});
</script>
<style>
h1{
text-align: center;
margin-bottom: 0;
font-size: 4rem;
}
.subheading{
text-align: center;
margin-top: 0;
font-size: 1.5rem;
}
</style>
<svelte:head>
<title>{labels.metaTitle}</title>
<meta name="description" content="{labels.metaDescription}" />
<meta property="og:image" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="{labels.metaAlt}" />
</svelte:head>
<h1>{labels.title}</h1>
<p class=subheading>{labels.subheading}</p>
<Search></Search>
<MediaScroller title={labels.inSeason}>
{#each data.season as recipe}
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each}
</MediaScroller>
{#each categories as category}
<MediaScroller title={category}>
{#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
<Card {recipe} {current_month} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each}
</MediaScroller>
{/each}
{#if !isEnglish}
<AddButton href="/rezepte/add"></AddButton>
{/if}

View File

@@ -15,12 +15,18 @@
import FavoriteButton from '$lib/components/FavoriteButton.svelte'; import FavoriteButton from '$lib/components/FavoriteButton.svelte';
import RecipeLanguageSwitcher from '$lib/components/RecipeLanguageSwitcher.svelte'; import RecipeLanguageSwitcher from '$lib/components/RecipeLanguageSwitcher.svelte';
export let data: PageData; let { data }: { data: PageData } = $props();
const isEnglish = $derived(data.lang === 'en');
// Use German short_name for images (they're the same for both languages) // Use German short_name for images (they're the same for both languages)
let hero_img_src = "https://bocken.org/static/rezepte/full/" + data.germanShortName + ".webp?v=" + data.dateModified const imageShortName = $derived(data.germanShortName || data.short_name);
let placeholder_src = "https://bocken.org/static/rezepte/placeholder/" + data.germanShortName + ".webp?v=" + data.dateModified const hero_img_src = $derived("https://bocken.org/static/rezepte/full/" + imageShortName + ".webp?v=" + data.dateModified);
export let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] const placeholder_src = $derived("https://bocken.org/static/rezepte/placeholder/" + imageShortName + ".webp?v=" + data.dateModified);
const months = $derived(isEnglish
? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
function season_intervals() { function season_intervals() {
let interval_arr = [] let interval_arr = []
@@ -54,17 +60,9 @@
return interval_arr return interval_arr
} }
export let season_iv = season_intervals(); const season_iv = $derived(season_intervals());
afterNavigate(() => { const display_date = $derived(data.updatedAt ? new Date(data.updatedAt) : new Date(data.dateCreated));
hero_img_src = "https://bocken.org/static/rezepte/full/" + data.germanShortName + ".webp"
placeholder_src = "https://bocken.org/static/rezepte/placeholder/" + data.germanShortName + ".webp"
season_iv = season_intervals();
})
let display_date = new Date(data.dateCreated);
if (data.updatedAt){
display_date = new Date(data.updatedAt);
}
const options = { const options = {
day: '2-digit', day: '2-digit',
month: 'short', month: 'short',
@@ -72,7 +70,14 @@
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}; };
const formatted_display_date = display_date.toLocaleDateString('en-US', options) const formatted_display_date = $derived(display_date.toLocaleDateString(isEnglish ? 'en-US' : 'de-DE', options));
const labels = $derived({
season: isEnglish ? 'Season:' : 'Saison:',
keywords: isEnglish ? 'Keywords:' : 'Stichwörter:',
lastModified: isEnglish ? 'Last modified:' : 'Letzte Änderung:',
title: isEnglish ? "Bocken's Recipes" : "Bocken'sche Rezepte"
});
</script> </script>
<style> <style>
*{ *{
@@ -273,30 +278,25 @@ h4{
</style> </style>
<svelte:head> <svelte:head>
<title>{stripHtmlTags(data.name)} - Bocken's Recipes</title> <title>{stripHtmlTags(data.name)} - {labels.title}</title>
<meta name="description" content="{stripHtmlTags(data.description)}" /> <meta name="description" content="{stripHtmlTags(data.description)}" />
<meta property="og:image" content="https://bocken.org/static/rezepte/thumb/{data.germanShortName}.webp" /> <meta property="og:image" content="https://bocken.org/static/rezepte/thumb/{imageShortName}.webp" />
<meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/{data.germanShortName}.webp" /> <meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/{imageShortName}.webp" />
<meta property="og:image:type" content="image/webp" /> <meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="{stripHtmlTags(data.name)}" /> <meta property="og:image:alt" content="{stripHtmlTags(data.name)}" />
{@html `<script type="application/ld+json">${JSON.stringify(data.recipeJsonLd)}</script>`} {@html `<script type="application/ld+json">${JSON.stringify(data.recipeJsonLd)}</script>`}
<!-- SEO: hreflang tags --> <!-- SEO: hreflang tags -->
<link rel="alternate" hreflang="de" href="https://bocken.org/rezepte/{data.germanShortName}" /> <link rel="alternate" hreflang="de" href="https://bocken.org/rezepte/{data.germanShortName}" />
<link rel="alternate" hreflang="en" href="https://bocken.org/recipes/{data.short_name}" /> {#if isEnglish || data.hasEnglishTranslation}
<link rel="alternate" hreflang="en" href="https://bocken.org/recipes/{isEnglish ? data.short_name : data.englishShortName}" />
{/if}
<link rel="alternate" hreflang="x-default" href="https://bocken.org/rezepte/{data.germanShortName}" /> <link rel="alternate" hreflang="x-default" href="https://bocken.org/rezepte/{data.germanShortName}" />
</svelte:head> </svelte:head>
<RecipeLanguageSwitcher
germanUrl="/rezepte/{data.germanShortName}"
englishUrl="/recipes/{data.short_name}"
currentLang="en"
hasTranslation={true}
/>
<TitleImgParallax src={hero_img_src} {placeholder_src}> <TitleImgParallax src={hero_img_src} {placeholder_src}>
<div class=title> <div class=title>
<a class="category" href='/recipes/category/{data.category}'>{data.category}</a> <a class="category" href='/{data.recipeLang}/category/{data.category}'>{data.category}</a>
<a class="icon" href='/recipes/icon/{data.icon}'>{data.icon}</a> <a class="icon" href='/{data.recipeLang}/icon/{data.icon}'>{data.icon}</a>
<h1>{@html data.name}</h1> <h1>{@html data.name}</h1>
{#if data.description && ! data.preamble} {#if data.description && ! data.preamble}
<p class=description>{data.description}</p> <p class=description>{data.description}</p>
@@ -305,9 +305,9 @@ h4{
<p>{@html data.preamble}</p> <p>{@html data.preamble}</p>
{/if} {/if}
<div class=tags> <div class=tags>
<h4>Season:</h4> <h4>{labels.season}</h4>
{#each season_iv as season} {#each season_iv as season}
<a class=tag href="/recipes/season/{season[0]}"> <a class=tag href="/{data.recipeLang}/season/{season[0]}">
{#if season[0]} {#if season[0]}
{months[season[0] - 1]} {months[season[0] - 1]}
{/if} {/if}
@@ -317,10 +317,10 @@ h4{
</a> </a>
{/each} {/each}
</div> </div>
<h4>Keywords:</h4> <h4>{labels.keywords}</h4>
<div class="tags center"> <div class="tags center">
{#each data.tags as tag} {#each data.tags as tag}
<a class=tag href="/recipes/tag/{tag}">{tag}</a> <a class=tag href="/{data.recipeLang}/tag/{tag}">{tag}</a>
{/each} {/each}
</div> </div>
@@ -345,7 +345,7 @@ h4{
{@html data.addendum} {@html data.addendum}
{/if} {/if}
</div> </div>
<p class=date>Last modified: {formatted_display_date}</p> <p class=date>{labels.lastModified} {formatted_display_date}</p>
</div> </div>
</TitleImgParallax> </TitleImgParallax>

View File

@@ -2,7 +2,10 @@ import { error } from "@sveltejs/kit";
import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd'; import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd';
export async function load({ fetch, params, url}) { export async function load({ fetch, params, url}) {
const res = await fetch(`/api/rezepte/items/${params.name}`); const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const res = await fetch(`${apiBase}/items/${params.name}`);
let item = await res.json(); let item = await res.json();
if(!res.ok){ if(!res.ok){
throw error(res.status, item.message) throw error(res.status, item.message)
@@ -35,8 +38,11 @@ export async function load({ fetch, params, url}) {
for (let ingredientIndex = 0; ingredientIndex < list.list.length; ingredientIndex++) { for (let ingredientIndex = 0; ingredientIndex < list.list.length; ingredientIndex++) {
const ingredient = list.list[ingredientIndex]; const ingredient = list.list[ingredientIndex];
// Check if this is a yeast ingredient // Check if this is a yeast ingredient (both German and English names)
if (ingredient.name === "Frischhefe" || ingredient.name === "Trockenhefe") { const isFreshYeast = ingredient.name === "Frischhefe" || ingredient.name === "Fresh Yeast";
const isDryYeast = ingredient.name === "Trockenhefe" || ingredient.name === "Dry Yeast";
if (isFreshYeast || isDryYeast) {
// Check if this yeast should be toggled // Check if this yeast should be toggled
const yeastParam = `y${yeastCounter}`; const yeastParam = `y${yeastCounter}`;
const isToggled = url.searchParams.has(yeastParam); const isToggled = url.searchParams.has(yeastParam);
@@ -49,9 +55,9 @@ export async function load({ fetch, params, url}) {
let newName: string, newAmount: string, newUnit: string; let newName: string, newAmount: string, newUnit: string;
if (originalName === "Frischhefe") { if (isFreshYeast) {
// Convert fresh yeast to dry yeast // Convert fresh yeast to dry yeast
newName = "Trockenhefe"; newName = isEnglish ? "Dry Yeast" : "Trockenhefe";
if (originalUnit === "Prise") { if (originalUnit === "Prise") {
// "1 Prise Frischhefe" → "1 Prise Trockenhefe" // "1 Prise Frischhefe" → "1 Prise Trockenhefe"
@@ -66,9 +72,9 @@ export async function load({ fetch, params, url}) {
newAmount = (originalAmount / 3).toString(); newAmount = (originalAmount / 3).toString();
newUnit = "g"; newUnit = "g";
} }
} else if (originalName === "Trockenhefe") { } else if (isDryYeast) {
// Convert dry yeast to fresh yeast // Convert dry yeast to fresh yeast
newName = "Frischhefe"; newName = isEnglish ? "Fresh Yeast" : "Frischhefe";
if (originalUnit === "Prise") { if (originalUnit === "Prise") {
// "1 Prise Trockenhefe" → "1 g Frischhefe" // "1 Prise Trockenhefe" → "1 g Frischhefe"
@@ -105,9 +111,11 @@ export async function load({ fetch, params, url}) {
// Generate JSON-LD server-side // Generate JSON-LD server-side
const recipeJsonLd = generateRecipeJsonLd(item); const recipeJsonLd = generateRecipeJsonLd(item);
// Check if English translation exists // For German page: check if English translation exists
const hasEnglishTranslation = !!(item.translations?.en?.short_name); // For English page: germanShortName is already in item (from API)
const englishShortName = item.translations?.en?.short_name || ''; const hasEnglishTranslation = !isEnglish && !!(item.translations?.en?.short_name);
const englishShortName = !isEnglish ? (item.translations?.en?.short_name || '') : '';
const germanShortName = isEnglish ? (item.germanShortName || '') : item.short_name;
return { return {
...item, ...item,
@@ -116,5 +124,6 @@ export async function load({ fetch, params, url}) {
recipeJsonLd, recipeJsonLd,
hasEnglishTranslation, hasEnglishTranslation,
englishShortName, englishShortName,
germanShortName,
}; };
} }

View File

@@ -0,0 +1,13 @@
import { redirect } from "@sveltejs/kit";
export async function load({locals, params}) {
// Add is German-only - redirect to German version
if (params.recipeLang === 'recipes') {
throw redirect(301, '/rezepte/add');
}
const session = await locals.auth();
return {
user: session?.user
};
};

View File

@@ -1,9 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import "$lib/css/nordtheme.css"; import "$lib/css/nordtheme.css";
export let data: PageData; let { data }: { data: PageData } = $props();
import TagCloud from '$lib/components/TagCloud.svelte'; import TagCloud from '$lib/components/TagCloud.svelte';
import TagBall from '$lib/components/TagBall.svelte'; import TagBall from '$lib/components/TagBall.svelte';
const isEnglish = $derived(data.lang === 'en');
const labels = $derived({
title: isEnglish ? 'Categories' : 'Kategorien'
});
</script> </script>
<style> <style>
h1 { h1 {
@@ -11,11 +16,11 @@
font-size: 3rem; font-size: 3rem;
} }
</style> </style>
<h1>Kategorien</h1> <h1>{labels.title}</h1>
<section> <section>
<TagCloud> <TagCloud>
{#each data.categories as tag} {#each data.categories as tag}
<TagBall {tag} ref="/rezepte/category"> <TagBall {tag} ref="/{data.recipeLang}/category">
</TagBall> </TagBall>
{/each} {/each}
</TagCloud> </TagCloud>

View File

@@ -0,0 +1,10 @@
import type { PageLoad } from "./$types";
export async function load({ fetch, params}) {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const res = await fetch(`${apiBase}/items/category`);
const categories= await res.json();
return {categories}
};

View File

@@ -2,7 +2,10 @@ import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites"; import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => { export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res = await fetch(`/api/rezepte/items/category/${params.category}`); const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const res = await fetch(`${apiBase}/items/category/${params.category}`);
const items = await res.json(); const items = await res.json();
// Get user favorites and session // Get user favorites and session

View File

@@ -2,10 +2,13 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte'; import Recipes from '$lib/components/Recipes.svelte';
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
export let data: PageData; let { data }: { data: PageData } = $props();
export let current_month = new Date().getMonth() + 1; let current_month = new Date().getMonth() + 1;
import Card from '$lib/components/Card.svelte' import Card from '$lib/components/Card.svelte'
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
const isEnglish = $derived(data.lang === 'en');
const label = $derived(isEnglish ? 'Recipes in Category' : 'Rezepte in Kategorie');
</script> </script>
<style> <style>
h1 { h1 {
@@ -13,12 +16,12 @@
font-size: 3em; font-size: 3em;
} }
</style> </style>
<h1>Rezepte in Kategorie <q>{data.category}</q>:</h1> <h1>{label} <q>{data.category}</q>:</h1>
<Search category={data.category}></Search> <Search category={data.category}></Search>
<section> <section>
<Recipes> <Recipes>
{#each rand_array(data.recipes) as recipe} {#each rand_array(data.recipes) as recipe}
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card> <Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each} {/each}
</Recipes> </Recipes>
</section> </section>

View File

@@ -0,0 +1,24 @@
import type { PageServerLoad } from "./$types";
import { redirect } from "@sveltejs/kit";
export const load: PageServerLoad = async ({ fetch, params, locals}) => {
// Edit is German-only - redirect to German version
if (params.recipeLang === 'recipes') {
// We need to get the German short_name first
const res = await fetch(`/api/recipes/items/${params.name}`);
if (res.ok) {
const recipe = await res.json();
throw redirect(301, `/rezepte/edit/${recipe.germanShortName}`);
}
// If recipe not found, redirect to German recipes list
throw redirect(301, '/rezepte');
}
let current_month = new Date().getMonth() + 1
const apiRes = await fetch(`/api/rezepte/items/${params.name}`);
const recipe = await apiRes.json();
const session = await locals.auth();
return {recipe: recipe,
user: session?.user
};
};

View File

@@ -1,15 +1,17 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ fetch, locals }) => { export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const session = await locals.auth(); const session = await locals.auth();
if (!session?.user?.nickname) { if (!session?.user?.nickname) {
throw redirect(302, '/rezepte'); throw redirect(302, `/${params.recipeLang}`);
} }
try { try {
const res = await fetch('/api/rezepte/favorites/recipes'); const res = await fetch(`${apiBase}/favorites/recipes`);
if (!res.ok) { if (!res.ok) {
return { return {
favorites: [], favorites: [],

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import type { PageData } from './$types';
import '$lib/css/nordtheme.css';
import Recipes from '$lib/components/Recipes.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
let { data }: { data: PageData } = $props();
let current_month = new Date().getMonth() + 1;
const isEnglish = $derived(data.lang === 'en');
const labels = $derived({
title: isEnglish ? 'Favorites' : 'Favoriten',
pageTitle: isEnglish ? 'My Favorites - Bocken Recipes' : 'Meine Favoriten - Bocken Rezepte',
metaDescription: isEnglish
? 'My favorite recipes from Bocken\'s kitchen.'
: 'Meine favorisierten Rezepte aus der Bockenschen Küche.',
count: isEnglish
? `${data.favorites.length} favorite recipe${data.favorites.length !== 1 ? 's' : ''}`
: `${data.favorites.length} favorisierte Rezepte`,
noFavorites: isEnglish ? 'No favorites saved yet' : 'Noch keine Favoriten gespeichert',
errorLoading: isEnglish ? 'Error loading favorites:' : 'Fehler beim Laden der Favoriten:',
emptyState1: isEnglish
? 'You haven\'t saved any recipes as favorites yet.'
: 'Du hast noch keine Rezepte als Favoriten gespeichert.',
emptyState2: isEnglish
? 'Visit a recipe and click the heart icon to add it to your favorites.'
: 'Besuche ein Rezept und klicke auf das Herz-Symbol, um es zu deinen Favoriten hinzuzufügen.',
recipesLink: isEnglish ? 'recipe' : 'Rezept'
});
</script>
<style>
h1{
text-align: center;
margin-bottom: 0;
font-size: 4rem;
}
.subheading{
text-align: center;
margin-top: 0;
font-size: 1.5rem;
}
.empty-state{
text-align: center;
margin-top: 3rem;
color: var(--nord3);
}
</style>
<svelte:head>
<title>{labels.pageTitle}</title>
<meta name="description" content={labels.metaDescription} />
</svelte:head>
<h1>{labels.title}</h1>
<p class=subheading>
{#if data.favorites.length > 0}
{labels.count}
{:else}
{labels.noFavorites}
{/if}
</p>
<Search favoritesOnly={true}></Search>
{#if data.error}
<p class="empty-state">{labels.errorLoading} {data.error}</p>
{:else if data.favorites.length > 0}
<Recipes>
{#each data.favorites as recipe}
<Card {recipe} {current_month} isFavorite={true} showFavoriteIndicator={true} routePrefix="/{data.recipeLang}"></Card>
{/each}
</Recipes>
{:else}
<div class="empty-state">
<p>{labels.emptyState1}</p>
<p><a href="/{data.recipeLang}">{labels.emptyState2}</a></p>
</div>
{/if}

View File

@@ -6,7 +6,7 @@
import SeasonLayout from '$lib/components/SeasonLayout.svelte' import SeasonLayout from '$lib/components/SeasonLayout.svelte'
import Card from '$lib/components/Card.svelte'; import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
export let data: PageData; let { data }: { data: PageData } = $props();
</script> </script>
<style> <style>
a{ a{
@@ -75,6 +75,6 @@
</style> </style>
<div class=flex> <div class=flex>
{#each data.icons as icon} {#each data.icons as icon}
<a href="/rezepte/icon/{icon}">{icon}</a> <a href="/{data.recipeLang}/icon/{icon}">{icon}</a>
{/each} {/each}
</div> </div>

View File

@@ -2,8 +2,11 @@ import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites"; import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => { export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res_season = await fetch(`/api/rezepte/items/icon/` + params.icon); const isEnglish = params.recipeLang === 'recipes';
const res_icons = await fetch(`/api/rezepte/items/icon`); const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const res_season = await fetch(`${apiBase}/items/icon/` + params.icon);
const res_icons = await fetch(`/api/rezepte/items/icon`); // Icons are shared across languages
const icons = await res_icons.json(); const icons = await res_icons.json();
const item_season = await res_season.json(); const item_season = await res_season.json();

View File

@@ -5,13 +5,13 @@
import MediaScroller from '$lib/components/MediaScroller.svelte'; import MediaScroller from '$lib/components/MediaScroller.svelte';
import Card from '$lib/components/Card.svelte'; import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
export let data: PageData; let { data }: { data: PageData } = $props();
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
</script> </script>
<IconLayout icons={data.icons} active_icon={data.icon} > <IconLayout icons={data.icons} active_icon={data.icon} routePrefix="/{data.recipeLang}">
<Recipes slot=recipes> <Recipes slot=recipes>
{#each rand_array(data.season) as recipe} {#each rand_array(data.season) as recipe}
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card> <Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each} {/each}
</Recipes> </Recipes>
</IconLayout> </IconLayout>

View File

@@ -1,6 +1,9 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url, fetch }) => { export const load: PageServerLoad = async ({ url, fetch, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const query = url.searchParams.get('q') || ''; const query = url.searchParams.get('q') || '';
const category = url.searchParams.get('category'); const category = url.searchParams.get('category');
const tag = url.searchParams.get('tag'); const tag = url.searchParams.get('tag');
@@ -9,7 +12,7 @@ export const load: PageServerLoad = async ({ url, fetch }) => {
const favoritesOnly = url.searchParams.get('favorites') === 'true'; const favoritesOnly = url.searchParams.get('favorites') === 'true';
// Build API URL with filters // Build API URL with filters
const apiUrl = new URL('/api/rezepte/search', url.origin); const apiUrl = new URL(`${apiBase}/search`, url.origin);
if (query) apiUrl.searchParams.set('q', query); if (query) apiUrl.searchParams.set('q', query);
if (category) apiUrl.searchParams.set('category', category); if (category) apiUrl.searchParams.set('category', category);
if (tag) apiUrl.searchParams.set('tag', tag); if (tag) apiUrl.searchParams.set('tag', tag);

View File

@@ -0,0 +1,96 @@
<script lang="ts">
import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte';
import Search from '$lib/components/Search.svelte';
import Card from '$lib/components/Card.svelte';
let { data }: { data: PageData } = $props();
let current_month = new Date().getMonth() + 1;
const isEnglish = $derived(data.lang === 'en');
const labels = $derived({
title: isEnglish ? 'Search Results' : 'Suchergebnisse',
pageTitle: isEnglish
? `Search Results${data.query ? ` for "${data.query}"` : ''} - Bocken Recipes`
: `Suchergebnisse${data.query ? ` für "${data.query}"` : ''} - Bocken Rezepte`,
metaDescription: isEnglish
? 'Search results in Bocken\'s recipes.'
: 'Suchergebnisse in den Bockenschen Rezepten.',
filteredBy: isEnglish ? 'Filtered by:' : 'Gefiltert nach:',
category: isEnglish ? 'Category' : 'Kategorie',
keyword: isEnglish ? 'Keyword' : 'Stichwort',
icon: 'Icon',
season: isEnglish ? 'Season' : 'Saison',
favoritesOnly: isEnglish ? 'Favorites only' : 'Nur Favoriten',
searchError: isEnglish ? 'Search error:' : 'Fehler bei der Suche:',
resultsFor: isEnglish ? 'results for' : 'Ergebnisse für',
noResults: isEnglish ? 'No recipes found.' : 'Keine Rezepte gefunden.',
tryOther: isEnglish ? 'Try different search terms.' : 'Versuche es mit anderen Suchbegriffen.'
});
</script>
<style>
h1 {
text-align: center;
font-size: 3em;
}
.search-info {
text-align: center;
margin-bottom: 2rem;
color: var(--nord3);
}
.filter-info {
text-align: center;
margin-bottom: 1rem;
font-size: 0.9em;
color: var(--nord2);
}
</style>
<svelte:head>
<title>{labels.pageTitle}</title>
<meta name="description" content={labels.metaDescription} />
</svelte:head>
<h1>{labels.title}</h1>
{#if data.filters.category || data.filters.tag || data.filters.icon || data.filters.season || data.filters.favoritesOnly}
<div class="filter-info">
{labels.filteredBy}
{#if data.filters.category}{labels.category} "{data.filters.category}"{/if}
{#if data.filters.tag}{labels.keyword} "{data.filters.tag}"{/if}
{#if data.filters.icon}{labels.icon} "{data.filters.icon}"{/if}
{#if data.filters.season}{labels.season} "{data.filters.season}"{/if}
{#if data.filters.favoritesOnly}{labels.favoritesOnly}{/if}
</div>
{/if}
<Search
category={data.filters.category}
tag={data.filters.tag}
icon={data.filters.icon}
season={data.filters.season}
favoritesOnly={data.filters.favoritesOnly}
/>
{#if data.error}
<div class="search-info">
<p>{labels.searchError} {data.error}</p>
</div>
{:else if data.query}
<div class="search-info">
<p>{data.results.length} {labels.resultsFor} "{data.query}"</p>
</div>
{/if}
{#if data.results.length > 0}
<Recipes>
{#each data.results as recipe}
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={true} routePrefix="/{data.recipeLang}"></Card>
{/each}
</Recipes>
{:else if data.query && !data.error}
<div class="search-info">
<p>{labels.noResults}</p>
<p>{labels.tryOther}</p>
</div>
{/if}

View File

@@ -1,9 +1,12 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites"; import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals }) => { export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
let current_month = new Date().getMonth() + 1 let current_month = new Date().getMonth() + 1
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month); const res_season = await fetch(`${apiBase}/items/in_season/` + current_month);
const item_season = await res_season.json(); const item_season = await res_season.json();
// Get user favorites and session // Get user favorites and session

View File

@@ -6,15 +6,20 @@
import SeasonLayout from '$lib/components/SeasonLayout.svelte' import SeasonLayout from '$lib/components/SeasonLayout.svelte'
import Card from '$lib/components/Card.svelte'; import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
export let data: PageData; let { data }: { data: PageData } = $props();
export let current_month = new Date().getMonth() + 1 let current_month = new Date().getMonth() + 1
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
const isEnglish = $derived(data.lang === 'en');
const months = $derived(isEnglish
? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
</script> </script>
<SeasonLayout active_index={current_month-1}> <SeasonLayout active_index={current_month-1} {months} routePrefix="/{data.recipeLang}">
<Recipes slot=recipes> <Recipes slot=recipes>
{#each rand_array(data.season) as recipe} {#each rand_array(data.season) as recipe}
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card> <Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each} {/each}
</Recipes> </Recipes>
</SeasonLayout> </SeasonLayout>

View File

@@ -2,7 +2,10 @@ import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites"; import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => { export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res_season = await fetch(`/api/rezepte/items/in_season/` + params.month); const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const res_season = await fetch(`${apiBase}/items/in_season/` + params.month);
const item_season = await res_season.json(); const item_season = await res_season.json();
// Get user favorites and session // Get user favorites and session

View File

@@ -5,14 +5,19 @@
import MediaScroller from '$lib/components/MediaScroller.svelte'; import MediaScroller from '$lib/components/MediaScroller.svelte';
import Card from '$lib/components/Card.svelte'; import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
export let data: PageData; let { data }: { data: PageData } = $props();
let months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
const isEnglish = $derived(data.lang === 'en');
const months = $derived(isEnglish
? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
</script> </script>
<SeasonLayout active_index={data.month -1}> <SeasonLayout active_index={data.month -1} {months} routePrefix="/{data.recipeLang}">
<Recipes slot=recipes> <Recipes slot=recipes>
{#each rand_array(data.season) as recipe} {#each rand_array(data.season) as recipe}
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card> <Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each} {/each}
</Recipes> </Recipes>
</SeasonLayout> </SeasonLayout>

View File

@@ -1,9 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; let { data }: { data: PageData } = $props();
import "$lib/css/nordtheme.css"; import "$lib/css/nordtheme.css";
import TagCloud from '$lib/components/TagCloud.svelte'; import TagCloud from '$lib/components/TagCloud.svelte';
import TagBall from '$lib/components/TagBall.svelte'; import TagBall from '$lib/components/TagBall.svelte';
const isEnglish = $derived(data.lang === 'en');
const labels = $derived({
title: isEnglish ? 'Keywords' : 'Stichwörter'
});
</script> </script>
<style> <style>
h1 { h1 {
@@ -11,11 +16,11 @@
text-align: center; text-align: center;
} }
</style> </style>
<h1>Stichwörter</h1> <h1>{labels.title}</h1>
<section> <section>
<TagCloud> <TagCloud>
{#each data.tags as tag} {#each data.tags as tag}
<TagBall {tag} ref="/rezepte/tag"> <TagBall {tag} ref="/{data.recipeLang}/tag">
</TagBall> </TagBall>
{/each} {/each}
</TagCloud> </TagCloud>

View File

@@ -0,0 +1,10 @@
import type { PageLoad } from "./$types";
export async function load({ fetch, params}) {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const res = await fetch(`${apiBase}/items/tag`);
const tags = await res.json();
return {tags}
};

View File

@@ -2,7 +2,10 @@ import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites"; import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => { export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res_tag = await fetch(`/api/rezepte/items/tag/${params.tag}`); const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const res_tag = await fetch(`${apiBase}/items/tag/${params.tag}`);
const items_tag = await res_tag.json(); const items_tag = await res_tag.json();
// Get user favorites and session // Get user favorites and session

View File

@@ -1,11 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte'; import Recipes from '$lib/components/Recipes.svelte';
export let data: PageData; let { data }: { data: PageData } = $props();
export let current_month = new Date().getMonth() + 1; let current_month = new Date().getMonth() + 1;
import Card from '$lib/components/Card.svelte' import Card from '$lib/components/Card.svelte'
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
const isEnglish = $derived(data.lang === 'en');
const label = $derived(isEnglish ? 'Recipes with Keyword' : 'Rezepte mit Stichwort');
</script> </script>
<style> <style>
h1 { h1 {
@@ -13,12 +16,12 @@
font-size: 2em; font-size: 2em;
} }
</style> </style>
<h1>Rezepte mit Stichwort <q>{data.tag}</q>:</h1> <h1>{label} <q>{data.tag}</q>:</h1>
<Search tag={data.tag}></Search> <Search tag={data.tag}></Search>
<section> <section>
<Recipes> <Recipes>
{#each rand_array(data.recipes) as recipe} {#each rand_array(data.recipes) as recipe}
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card> <Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each} {/each}
</Recipes> </Recipes>
</section> </section>

View File

@@ -0,0 +1,70 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { UserFavorites } from '../../../../../models/UserFavorites';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect } from '../../../../../utils/db';
import type { RecipeModelType } from '../../../../../types/types';
import { error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ locals }) => {
const session = await locals.auth();
if (!session?.user?.nickname) {
throw error(401, 'Authentication required');
}
await dbConnect();
try {
const userFavorites = await UserFavorites.findOne({
username: session.user.nickname
}).lean();
if (!userFavorites?.favorites?.length) {
return json([]);
}
// Get recipes that are favorited AND have English translations
let recipes = await Recipe.find({
_id: { $in: userFavorites.favorites },
'translations.en': { $exists: true }
}).lean();
// Transform to English format
const englishRecipes = recipes.map(recipe => ({
_id: recipe._id,
short_name: recipe.translations.en.short_name,
name: recipe.translations.en.name,
category: recipe.translations.en.category,
icon: recipe.icon,
dateCreated: recipe.dateCreated,
dateModified: recipe.dateModified,
images: recipe.images?.map((img, idx) => ({
mediapath: img.mediapath,
alt: recipe.translations.en.images?.[idx]?.alt || img.alt,
caption: recipe.translations.en.images?.[idx]?.caption || img.caption,
})),
description: recipe.translations.en.description,
note: recipe.translations.en.note,
tags: recipe.translations.en.tags || [],
season: recipe.season,
baking: recipe.baking,
preparation: recipe.preparation,
fermentation: recipe.fermentation,
portions: recipe.portions,
cooking: recipe.cooking,
total_time: recipe.total_time,
ingredients: recipe.translations.en.ingredients || [],
instructions: recipe.translations.en.instructions || [],
preamble: recipe.translations.en.preamble,
addendum: recipe.translations.en.addendum,
germanShortName: recipe.short_name,
translationStatus: recipe.translations.en.translationStatus
}));
const result = JSON.parse(JSON.stringify(englishRecipes));
return json(result);
} catch (e) {
throw error(500, 'Failed to fetch favorite recipes');
}
};

View File

@@ -0,0 +1,14 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect } from '../../../../../utils/db';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
// Get distinct categories from English translations
const categories = await Recipe.distinct('translations.en.category', {
'translations.en': { $exists: true }
}).lean();
return json(JSON.parse(JSON.stringify(categories)));
};

View File

@@ -0,0 +1,24 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect } from '../../../../../utils/db';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
// Get all recipes with English translations
const recipes = await Recipe.find({
'translations.en': { $exists: true }
}, 'translations.en.tags').lean();
// Extract and flatten all unique tags
const tagsSet = new Set<string>();
recipes.forEach(recipe => {
if (recipe.translations?.en?.tags) {
recipe.translations.en.tags.forEach((tag: string) => tagsSet.add(tag));
}
});
const tags = Array.from(tagsSet).sort();
return json(JSON.parse(JSON.stringify(tags)));
};

View File

@@ -0,0 +1,88 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '../../../../types/types';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect } from '../../../../utils/db';
export const GET: RequestHandler = async ({ url, locals }) => {
await dbConnect();
const query = url.searchParams.get('q')?.toLowerCase().trim() || '';
const category = url.searchParams.get('category');
const tag = url.searchParams.get('tag');
const icon = url.searchParams.get('icon');
const season = url.searchParams.get('season');
const favoritesOnly = url.searchParams.get('favorites') === 'true';
try {
// Build base query - only recipes with English translations
let dbQuery: any = {
'translations.en': { $exists: true }
};
// Apply filters based on context
if (category) {
dbQuery['translations.en.category'] = category;
}
if (tag) {
dbQuery['translations.en.tags'] = { $in: [tag] };
}
if (icon) {
dbQuery.icon = icon; // Icon is the same for both languages
}
if (season) {
const seasonNum = parseInt(season);
if (!isNaN(seasonNum)) {
dbQuery.season = { $in: [seasonNum] }; // Season is the same for both languages
}
}
// Get all recipes matching base filters
let recipes = await Recipe.find(dbQuery).lean();
// Handle favorites filter
if (favoritesOnly && locals.session?.user) {
const { UserFavorites } = await import('../../../../models/UserFavorites');
const userFavorites = await UserFavorites.findOne({ username: locals.session.user.username });
if (userFavorites && userFavorites.favorites) {
const favoriteIds = userFavorites.favorites;
recipes = recipes.filter(recipe => favoriteIds.some(id => id.toString() === recipe._id?.toString()));
} else {
recipes = [];
}
}
// Transform to English brief format
let briefRecipes: BriefRecipeType[] = recipes.map(recipe => ({
_id: recipe._id,
name: recipe.translations.en.name,
short_name: recipe.translations.en.short_name,
tags: recipe.translations.en.tags || [],
category: recipe.translations.en.category,
icon: recipe.icon,
description: recipe.translations.en.description,
season: recipe.season,
dateModified: recipe.dateModified,
germanShortName: recipe.short_name
}));
// Apply text search if query provided
if (query) {
const searchTerms = query.normalize('NFD').replace(/\p{Diacritic}/gu, "").split(" ");
briefRecipes = briefRecipes.filter(recipe => {
const searchString = `${recipe.name} ${recipe.description || ''} ${recipe.tags?.join(' ') || ''}`.toLowerCase()
.normalize('NFD').replace(/\p{Diacritic}/gu, "").replace(/&shy;|­/g, '');
return searchTerms.every(term => searchString.includes(term));
});
}
return json(JSON.parse(JSON.stringify(briefRecipes)));
} catch (error) {
return json({ error: 'Search failed' }, { status: 500 });
}
};

View File

@@ -1,7 +0,0 @@
import type { LayoutServerLoad } from "./$types"
export const load : LayoutServerLoad = async ({locals}) => {
return {
session: await locals.auth()
}
};

View File

@@ -1,25 +0,0 @@
<script>
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
export let data
let user;
if(data.session){
user = data.session.user
}
</script>
<Header>
<ul class=site_header slot=links>
<li><a href="/recipes">All Recipes</a></li>
{#if user}
<li><a href="/rezepte/favorites">Favorites</a></li>
{/if}
<li><a href="/recipes/season">In Season</a></li>
<li><a href="/recipes/category">Category</a></li>
<li><a href="/recipes/icon">Icon</a></li>
<li><a href="/recipes/tag">Keywords</a></li>
<li><a href="/rezepte/tips-and-tricks">Tips</a></li>
</ul>
<UserHeader slot=right_side {user}></UserHeader>
<slot></slot>
</Header>

View File

@@ -1,22 +0,0 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals }) => {
let current_month = new Date().getMonth() + 1
const res_season = await fetch(`/api/recipes/items/in_season/` + current_month);
const res_all_brief = await fetch(`/api/recipes/items/all_brief`);
const item_season = await res_season.json();
const item_all_brief = await res_all_brief.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
season: addFavoriteStatusToRecipes(item_season, userFavorites),
all_brief: addFavoriteStatusToRecipes(item_all_brief, userFavorites),
session
};
};

View File

@@ -1,50 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import MediaScroller from '$lib/components/MediaScroller.svelte';
import AddButton from '$lib/components/AddButton.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
export let data: PageData;
export let current_month = new Date().getMonth() + 1
const categories = ["Main Course", "Pasta", "Bread", "Dessert", "Soup", "Side Dish", "Salad", "Cake", "Breakfast", "Sauce", "Ingredient", "Drink", "Spread", "Cookie", "Snack"]
</script>
<style>
h1{
text-align: center;
margin-bottom: 0;
font-size: 4rem;
}
.subheading{
text-align: center;
margin-top: 0;
font-size: 1.5rem;
}
</style>
<svelte:head>
<title>Bocken Recipes</title>
<meta name="description" content="A constantly growing collection of recipes from Bocken's kitchen." />
<meta property="og:image" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="Pasta al Ragu with Linguine" />
</svelte:head>
<h1>Recipes</h1>
<p class=subheading>{data.all_brief.length} recipes and constantly growing...</p>
<Search></Search>
<MediaScroller title="In Season">
{#each data.season as recipe}
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/recipes"></Card>
{/each}
</MediaScroller>
{#each categories as category}
<MediaScroller title={category}>
{#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
<Card {recipe} {current_month} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/recipes"></Card>
{/each}
</MediaScroller>
{/each}
<AddButton href="/rezepte/add"></AddButton>

View File

@@ -1,102 +0,0 @@
import { error } from "@sveltejs/kit";
import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd';
export async function load({ fetch, params, url}) {
const res = await fetch(`/api/recipes/items/${params.name}`);
let item = await res.json();
if(!res.ok){
throw error(res.status, item.message)
}
// Check if this recipe is favorited by the user
let isFavorite = false;
try {
const favRes = await fetch(`/api/rezepte/favorites/check/${item.germanShortName}`);
if (favRes.ok) {
const favData = await favRes.json();
isFavorite = favData.isFavorite;
}
} catch (e) {
// Silently fail if not authenticated or other error
}
// Get multiplier from URL parameters
const multiplier = parseFloat(url.searchParams.get('multiplier') || '1');
// Handle yeast swapping from URL parameters
if (item.ingredients) {
let yeastCounter = 0;
for (let listIndex = 0; listIndex < item.ingredients.length; listIndex++) {
const list = item.ingredients[listIndex];
if (list.list) {
for (let ingredientIndex = 0; ingredientIndex < list.list.length; ingredientIndex++) {
const ingredient = list.list[ingredientIndex];
// Check for English yeast names
if (ingredient.name === "Fresh Yeast" || ingredient.name === "Dry Yeast") {
const yeastParam = `y${yeastCounter}`;
const isToggled = url.searchParams.has(yeastParam);
if (isToggled) {
const originalName = ingredient.name;
const originalAmount = parseFloat(ingredient.amount);
const originalUnit = ingredient.unit;
let newName: string, newAmount: string, newUnit: string;
if (originalName === "Fresh Yeast") {
newName = "Dry Yeast";
if (originalUnit === "Pinch") {
newAmount = ingredient.amount;
newUnit = "Pinch";
} else if (originalUnit === "g" && originalAmount === 1) {
newAmount = "1";
newUnit = "Pinch";
} else {
newAmount = (originalAmount / 3).toString();
newUnit = "g";
}
} else if (originalName === "Dry Yeast") {
newName = "Fresh Yeast";
if (originalUnit === "Pinch") {
newAmount = "1";
newUnit = "g";
} else {
newAmount = (originalAmount * 3).toString();
newUnit = "g";
}
} else {
newName = originalName;
newAmount = ingredient.amount;
newUnit = originalUnit;
}
item.ingredients[listIndex].list[ingredientIndex] = {
...item.ingredients[listIndex].list[ingredientIndex],
name: newName,
amount: newAmount,
unit: newUnit
};
}
yeastCounter++;
}
}
}
}
}
// Generate JSON-LD with English data and language tag
const recipeJsonLd = generateRecipeJsonLd({ ...item, inLanguage: 'en' });
return {
...item,
isFavorite,
multiplier,
recipeJsonLd,
lang: 'en', // Mark as English page
};
}

View File

@@ -1,19 +0,0 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res = await fetch(`/api/recipes/items/category/${params.category}`);
const items = await res.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
category: params.category,
recipes: addFavoriteStatusToRecipes(items, userFavorites),
session
};
};

View File

@@ -1,24 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte';
import Search from '$lib/components/Search.svelte';
export let data: PageData;
export let current_month = new Date().getMonth() + 1;
import Card from '$lib/components/Card.svelte'
import { rand_array } from '$lib/js/randomize';
</script>
<style>
h1 {
text-align: center;
font-size: 3em;
}
</style>
<h1>Recipes in Category <q>{data.category}</q>:</h1>
<Search category={data.category}></Search>
<section>
<Recipes>
{#each rand_array(data.recipes) as recipe}
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/recipes"></Card>
{/each}
</Recipes>
</section>

View File

@@ -1,22 +0,0 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res_season = await fetch(`/api/recipes/items/icon/` + params.icon);
const res_icons = await fetch(`/api/rezepte/items/icon`); // Icons are shared across languages
const icons = await res_icons.json();
const item_season = await res_season.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
icons: icons,
icon: params.icon,
season: addFavoriteStatusToRecipes(item_season, userFavorites),
session
};
};

View File

@@ -1,17 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte';
import IconLayout from '$lib/components/IconLayout.svelte';
import MediaScroller from '$lib/components/MediaScroller.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
export let data: PageData;
import { rand_array } from '$lib/js/randomize';
</script>
<IconLayout icons={data.icons} active_icon={data.icon} >
<Recipes slot=recipes>
{#each rand_array(data.season) as recipe}
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/recipes"></Card>
{/each}
</Recipes>
</IconLayout>

View File

@@ -1,19 +0,0 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res_season = await fetch(`/api/recipes/items/in_season/` + params.month);
const item_season = await res_season.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
month: params.month,
season: addFavoriteStatusToRecipes(item_season, userFavorites),
session
};
};

View File

@@ -1,18 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte';
import SeasonLayout from '$lib/components/SeasonLayout.svelte';
import MediaScroller from '$lib/components/MediaScroller.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
export let data: PageData;
let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
import { rand_array } from '$lib/js/randomize';
</script>
<SeasonLayout active_index={data.month -1}>
<Recipes slot=recipes>
{#each rand_array(data.season) as recipe}
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/recipes"></Card>
{/each}
</Recipes>
</SeasonLayout>

View File

@@ -1,19 +0,0 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res_tag = await fetch(`/api/recipes/items/tag/${params.tag}`);
const items_tag = await res_tag.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
tag: params.tag,
recipes: addFavoriteStatusToRecipes(items_tag, userFavorites),
session
};
};

View File

@@ -1,24 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte';
export let data: PageData;
export let current_month = new Date().getMonth() + 1;
import Card from '$lib/components/Card.svelte'
import Search from '$lib/components/Search.svelte';
import { rand_array } from '$lib/js/randomize';
</script>
<style>
h1 {
text-align: center;
font-size: 2em;
}
</style>
<h1>Recipes with Keyword <q>{data.tag}</q>:</h1>
<Search tag={data.tag}></Search>
<section>
<Recipes>
{#each rand_array(data.recipes) as recipe}
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/recipes"></Card>
{/each}
</Recipes>
</section>

View File

@@ -1,7 +0,0 @@
import type { LayoutServerLoad } from "./$types"
export const load : LayoutServerLoad = async ({locals}) => {
return {
session: await locals.auth()
}
};

View File

@@ -1,25 +0,0 @@
<script>
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
export let data
let user;
if(data.session){
user = data.session.user
}
</script>
<Header>
<ul class=site_header slot=links>
<li><a href="/rezepte">Alle Rezepte</a></li>
{#if user}
<li><a href="/rezepte/favorites">Favoriten</a></li>
{/if}
<li><a href="/rezepte/season">In Saison</a></li>
<li><a href="/rezepte/category">Kategorie</a></li>
<li><a href="/rezepte/icon">Icon</a></li>
<li><a href="/rezepte/tag">Stichwörter</a></li>
<li><a href="/rezepte/tips-and-tricks">Tipps</a></li>
</ul>
<UserHeader slot=right_side {user}></UserHeader>
<slot></slot>
</Header>

View File

@@ -1,50 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import MediaScroller from '$lib/components/MediaScroller.svelte';
import AddButton from '$lib/components/AddButton.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
export let data: PageData;
export let current_month = new Date().getMonth() + 1
const categories = ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Snack"]
</script>
<style>
h1{
text-align: center;
margin-bottom: 0;
font-size: 4rem;
}
.subheading{
text-align: center;
margin-top: 0;
font-size: 1.5rem;
}
</style>
<svelte:head>
<title>Bocken Rezepte</title>
<meta name="description" content="Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche." />
<meta property="og:image" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="Pasta al Ragu mit Linguine" />
</svelte:head>
<h1>Rezepte</h1>
<p class=subheading>{data.all_brief.length} Rezepte und stetig wachsend...</p>
<Search></Search>
<MediaScroller title="In Saison">
{#each data.season as recipe}
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
{/each}
</MediaScroller>
{#each categories as category}
<MediaScroller title={category}>
{#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
<Card {recipe} {current_month} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
{/each}
</MediaScroller>
{/each}
<AddButton href="/rezepte/add"></AddButton>

View File

@@ -1,355 +0,0 @@
<script lang="ts">
import { writable } from 'svelte/store';
export const multiplier = writable(0);
import type { PageData } from './$types';
import "$lib/css/nordtheme.css"
import EditButton from '$lib/components/EditButton.svelte';
import InstructionsPage from '$lib/components/InstructionsPage.svelte';
import IngredientsPage from '$lib/components/IngredientsPage.svelte';
import TitleImgParallax from '$lib/components/TitleImgParallax.svelte';
import { afterNavigate } from '$app/navigation';
import {season} from '$lib/js/season_store';
import RecipeNote from '$lib/components/RecipeNote.svelte';
import {stripHtmlTags} from '$lib/js/stripHtmlTags';
import FavoriteButton from '$lib/components/FavoriteButton.svelte';
import RecipeLanguageSwitcher from '$lib/components/RecipeLanguageSwitcher.svelte';
export let data: PageData;
let hero_img_src = "https://bocken.org/static/rezepte/full/" + data.short_name + ".webp?v=" + data.dateModified
let placeholder_src = "https://bocken.org/static/rezepte/placeholder/" + data.short_name + ".webp?v=" + data.dateModified
export let months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
function season_intervals() {
let interval_arr = []
let start_i = 0
for(var i = 12; i > 0; i--){
if(data.season.includes(i)){
start_i = data.season.indexOf(i);
}
else{
break
}
}
var start = data.season[start_i]
var end_i
const len = data.season.length
for(var i = 0; i < len -1; i++){
if(data.season.includes((start + i) %12 + 1)){
end_i = (start_i + i + 1) % len
}
else{
interval_arr.push([start, data.season[end_i]])
start = data.season[(start + i + 1) % len]
}
}
if(interval_arr.length == 0){
interval_arr.push([start, data.season[end_i]])
}
return interval_arr
}
export let season_iv = season_intervals();
afterNavigate(() => {
hero_img_src = "https://bocken.org/static/rezepte/full/" + data.short_name + ".webp"
placeholder_src = "https://bocken.org/static/rezepte/placeholder/" + data.short_name + ".webp"
season_iv = season_intervals();
})
let display_date = new Date(data.dateCreated);
if (data.updatedAt){
display_date = new Date(data.updatedAt);
}
const options = {
day: '2-digit',
month: 'short', // German abbreviation for the month
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
};
const formatted_display_date = display_date.toLocaleDateString('de-DE', options)
</script>
<style>
*{
font-family: sans-serif;
}
h1{
text-align: center;
padding-block: 0.5em;
border-radius: 10000px;
margin:0;
font-size: 3rem;
overflow-wrap: break-word;
hyphens: auto;
text-wrap: balance;
}
.category{
--size: 1.75rem;
position: absolute;
top: calc(-1* var(--size) );
left:calc(-3/2 * var(--size));
background-color: var(--nord0);
color: var(--nord6);
text-decoration: none;
font-size: var(--size);
padding: calc(var(--size) * 2/3);
border-radius: 1000px;
transition: 100ms;
box-shadow: 0em 0em 1em 0.3em rgba(0,0,0,0.4);
}
.category:hover,
.category:focus-visible{
background-color: var(--nord1);
scale: 1.1;
}
.tags{
margin-block: 1rem;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 1em;
}
.center{
justify-content: center;
}
.tag{
all:unset;
color: var(--nord0);
font-size: 1.1rem;
background-color: var(--nord5);
border-radius: 10000px;
padding: 0.25em 1em;
transition: 100ms;
box-shadow: 0em 0em 0.5em 0.05em rgba(0,0,0,0.3);
}
@media (prefers-color-scheme: dark) {
.tag{
background-color: var(--nord0);
color: white;
}
}
.tag:hover,
.tag:focus-visible
{
cursor: pointer;
transform: scale(1.1,1.1);
background-color: var(--orange);
box-shadow: 0.1em 0.1em 0.5em 0.1em rgba(0,0,0,0.3);
}
.wrapper_wrapper{
background-color: #fbf9f3;
padding-top: 10rem;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 3rem;
transform: translateY(-7rem);
z-index: -2;
}
@media (prefers-color-scheme: dark) {
.wrapper_wrapper{
background-color: var(--background-dark);
}
}
.wrapper{
display: flex;
flex-direction: row;
max-width: 1000px;
justify-content: center;
margin-inline: auto;
}
@media screen and (max-width: 700px){
.wrapper{
flex-direction:column;
}
}
.title{
position: relative;
width: min(800px, 80vw);
margin-inline: auto;
background-color: var(--nord6);
padding: 1rem 2rem;
translate: 0 1px; /*bruh*/
z-index: 1;
}
@media (prefers-color-scheme: dark) {
.title{
background-color: var(--nord6-dark);
}
}
.icon{
font-family: "Noto Color Emoji", emoji;
position: absolute;
top: -1em;
right: -0.75em;
text-decoration: unset;
background-color: #FAFAFE;
padding: 0.5em;
font-size: 1.5rem;
border-radius: 100000px;
transition: 100ms;
box-shadow: 0em 0em 1em 0.3em rgba(0,0,0,0.4);
}
@media (prefers-color-scheme: dark) {
.icon{
background-color: var(--accent-dark);
}
}
.icon:hover,
.icon:focus-visible{
scale: 1.2 1.2;
animation: shake 0.5s ease forwards;
}
h4{
margin-block: 0;
}
.addendum{
max-width: 800px;
margin-inline: auto;
padding-inline: 2rem;
}
@media screen and (max-width: 800px){
.title{
width: 100%;
}
.icon{
right: 1rem;
top: -1.75rem;
}
.category{
left: 1rem;
top: calc(var(--size) * -1.5);
}
}
@keyframes shake{
0%{
transform: rotate(0)
scale(1,1);
}
25%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle))
scale(1.2,1.2)
;
}
50%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(calc(-1* var(--angle)))
scale(1.2,1.2);
}
74%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle))
scale(1.2, 1.2);
}
100%{
transform: rotate(0)
scale(1.2,1.2);
}
}
.description{
text-align: center;
margin-bottom: 2em;
margin-top: -0.5em;
}
.date{
margin-bottom: 0;
}
</style>
<svelte:head>
<title>{stripHtmlTags(data.name)} - Bocken'sche Rezepte</title>
<meta name="description" content="{stripHtmlTags(data.description)}" />
<meta property="og:image" content="https://bocken.org/static/rezepte/thumb/{data.short_name}.webp" />
<meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/{data.short_name}.webp" />
<meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="{stripHtmlTags(data.name)}" />
{@html `<script type="application/ld+json">${JSON.stringify(data.recipeJsonLd)}</script>`}
<!-- SEO: hreflang tags -->
<link rel="alternate" hreflang="de" href="https://bocken.org/rezepte/{data.short_name}" />
{#if data.hasEnglishTranslation}
<link rel="alternate" hreflang="en" href="https://bocken.org/recipes/{data.englishShortName}" />
{/if}
<link rel="alternate" hreflang="x-default" href="https://bocken.org/rezepte/{data.short_name}" />
</svelte:head>
{#if data.hasEnglishTranslation}
<RecipeLanguageSwitcher
germanUrl="/rezepte/{data.short_name}"
englishUrl="/recipes/{data.englishShortName}"
currentLang="de"
hasTranslation={true}
/>
{/if}
<TitleImgParallax src={hero_img_src} {placeholder_src}>
<div class=title>
<a class="category" href='/rezepte/category/{data.category}'>{data.category}</a>
<a class="icon" href='/rezepte/icon/{data.icon}'>{data.icon}</a>
<h1>{@html data.name}</h1>
{#if data.description && ! data.preamble}
<p class=description>{data.description}</p>
{/if}
{#if data.preamble}
<p>{@html data.preamble}</p>
{/if}
<div class=tags>
<h4>Saison:</h4>
{#each season_iv as season}
<a class=tag href="/rezepte/season/{season[0]}">
{#if season[0]}
{months[season[0] - 1]}
{/if}
{#if season[1]}
- {months[season[1] - 1]}
{/if}
</a>
{/each}
</div>
<h4>Stichwörter:</h4>
<div class="tags center">
{#each data.tags as tag}
<a class=tag href="/rezepte/tag/{tag}">{tag}</a>
{/each}
</div>
<FavoriteButton
recipeId={data.short_name}
isFavorite={data.isFavorite || false}
isLoggedIn={!!data.session?.user}
/>
{#if data.note}
<RecipeNote note={data.note}></RecipeNote>
{/if}
</div>
<div class=wrapper_wrapper>
<div class=wrapper>
<IngredientsPage {data}></IngredientsPage>
<InstructionsPage {data}></InstructionsPage>
</div>
<div class=addendum>
{#if data.addendum}
{@html data.addendum}
{/if}
</div>
<p class=date>Letzte Änderung: {formatted_display_date}</p>
</div>
</TitleImgParallax>
<EditButton href="/rezepte/edit/{data.short_name}"></EditButton>

View File

@@ -1,6 +0,0 @@
export async function load({locals}) {
const session = await locals.auth();
return {
user: session?.user
};
};

View File

@@ -1,7 +0,0 @@
import type { PageLoad } from "./$types";
export async function load({ fetch}) {
const res = await fetch(`/api/rezepte/items/category`);
const categories= await res.json();
return {categories}
};

View File

@@ -1,11 +0,0 @@
import type { PageServerLoad } from "./$types";
export async function load({ fetch, params, locals}) {
let current_month = new Date().getMonth() + 1
const res = await fetch(`/api/rezepte/items/${params.name}`);
const recipe = await res.json();
const session = await locals.auth();
return {recipe: recipe,
user: session?.user
};
};

View File

@@ -1,58 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import '$lib/css/nordtheme.css';
import Recipes from '$lib/components/Recipes.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
export let data: PageData;
export let current_month = new Date().getMonth() + 1;
</script>
<style>
h1{
text-align: center;
margin-bottom: 0;
font-size: 4rem;
}
.subheading{
text-align: center;
margin-top: 0;
font-size: 1.5rem;
}
.empty-state{
text-align: center;
margin-top: 3rem;
color: var(--nord3);
}
</style>
<svelte:head>
<title>Meine Favoriten - Bocken Rezepte</title>
<meta name="description" content="Meine favorisierten Rezepte aus der Bockenschen Küche." />
</svelte:head>
<h1>Favoriten</h1>
<p class=subheading>
{#if data.favorites.length > 0}
{data.favorites.length} favorisierte Rezepte
{:else}
Noch keine Favoriten gespeichert
{/if}
</p>
<Search favoritesOnly={true}></Search>
{#if data.error}
<p class="empty-state">Fehler beim Laden der Favoriten: {data.error}</p>
{:else if data.favorites.length > 0}
<Recipes>
{#each data.favorites as recipe}
<Card {recipe} {current_month} isFavorite={true} showFavoriteIndicator={true}></Card>
{/each}
</Recipes>
{:else}
<div class="empty-state">
<p>Du hast noch keine Rezepte als Favoriten gespeichert.</p>
<p>Besuche ein <a href="/rezepte">Rezept</a> und klicke auf das Herz-Symbol, um es zu deinen Favoriten hinzuzufügen.</p>
</div>
{/if}

View File

@@ -1,75 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte';
import Search from '$lib/components/Search.svelte';
import Card from '$lib/components/Card.svelte';
export let data: PageData;
export let current_month = new Date().getMonth() + 1;
</script>
<style>
h1 {
text-align: center;
font-size: 3em;
}
.search-info {
text-align: center;
margin-bottom: 2rem;
color: var(--nord3);
}
.filter-info {
text-align: center;
margin-bottom: 1rem;
font-size: 0.9em;
color: var(--nord2);
}
</style>
<svelte:head>
<title>Suchergebnisse{data.query ? ` für "${data.query}"` : ''} - Bocken Rezepte</title>
<meta name="description" content="Suchergebnisse in den Bockenschen Rezepten." />
</svelte:head>
<h1>Suchergebnisse</h1>
{#if data.filters.category || data.filters.tag || data.filters.icon || data.filters.season || data.filters.favoritesOnly}
<div class="filter-info">
Gefiltert nach:
{#if data.filters.category}Kategorie "{data.filters.category}"{/if}
{#if data.filters.tag}Stichwort "{data.filters.tag}"{/if}
{#if data.filters.icon}Icon "{data.filters.icon}"{/if}
{#if data.filters.season}Saison "{data.filters.season}"{/if}
{#if data.filters.favoritesOnly}Nur Favoriten{/if}
</div>
{/if}
<Search
category={data.filters.category}
tag={data.filters.tag}
icon={data.filters.icon}
season={data.filters.season}
favoritesOnly={data.filters.favoritesOnly}
/>
{#if data.error}
<div class="search-info">
<p>Fehler bei der Suche: {data.error}</p>
</div>
{:else if data.query}
<div class="search-info">
<p>{data.results.length} Ergebnisse für "{data.query}"</p>
</div>
{/if}
{#if data.results.length > 0}
<Recipes>
{#each data.results as recipe}
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={true}></Card>
{/each}
</Recipes>
{:else if data.query && !data.error}
<div class="search-info">
<p>Keine Rezepte gefunden.</p>
<p>Versuche es mit anderen Suchbegriffen.</p>
</div>
{/if}

View File

@@ -1,7 +0,0 @@
import type { PageLoad } from "./$types";
export async function load({ fetch}) {
const res = await fetch(`/api/rezepte/items/tag`);
const tags = await res.json();
return {tags}
};

416458
static/allioli.json Normal file

File diff suppressed because one or more lines are too long

11
wine_glass.svg Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-10 0 1332 2048">
<path fill="currentColor"
d="M1240 488q0 -4 -9 47q-45 252 -164 367q-39 38 -192 103q-31 10 -74 38q-7 11 -9 68q-2 67 -7 202q-15 284 -15 291q0 30 9 45.5t40 28.5q28 6 84 19q209 48 209 159q0 95 -185 136q-116 26 -266 26q-149 0 -266 -26q-185 -41 -185 -136q0 -76 79 -116q41 -21 153 -46
q89 -20 100 -43q10 -20 10 -47q0 12 -12 -219t-12 -277q0 -6 1.5 -17t1.5 -17q0 -18 -12 -32t-122 -61t-142 -79q-131 -131 -162 -348q-11 -78 -11 -66q0 3 5 -40q24 -211 111 -341h926q41 46 83 185q11 37 27 156q6 45 6 40zM1153 456q0 -154 -63 -283h-858
q-63 132 -63 283q0 134 45 217q160 -21 231 -21h432q102 0 231 21q45 -77 45 -217zM1078 709q-28 -8 -85 -19q-64 -7 -107 -7h-450q-109 0 -192 26q36 8 110 20q109 13 153 13h308q93 0 263 -33zM943 889q-28 2 -82 10q-130 30 -203 30t-113 -7q-14 -2 -79 -20
q-49 -13 -81 -13h-7q54 21 107 41q63 28 91 66q12 17 15 68q8 144 16 349q7 179 7 197q0 38 -10 66q-2 6 -19 35.5t-17 31.5v41q33 16 103 16q47 0 83 -16v-42q0 3 -23 -46t-23 -86q0 -13 7 -197q4 -116 15 -349q3 -47 16 -68q22 -35 90 -65q53 -21 107 -42zM1034 1850
q0 -32 -102 -71q-91 -35 -133 -35q-10 0 -23 7q3 10 28 20.5t25 29.5q0 49 -168 49t-168 -49q0 -16 45 -37l8 -13q-13 -7 -23 -7q-42 0 -133 35q-102 39 -102 71q0 18 32 34q23 11 47 22l11 -11q-21 -11 -35 -19v-9l2 -2q54 22 139 30q72 6 143 13v14q-15 15 -45 15
q-53 0 -118 -16q-73 -18 -61 -18q-6 0 -6 7v4q105 35 264 35q116 0 192 -13q45 -8 103 -30q78 -30 78 -56zM639 1560q-12 -6 -12 -20l-10 -451q-1 -49 -9 -85h21q10 18 10 19v537zM826 1868q-43 22 -165 22q-121 0 -165 -22l7 -12q44 12 107 12h91q69 0 118 -12z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB