diff --git a/src/lib/components/Card.svelte b/src/lib/components/Card.svelte
index 204fbbc..5a46d88 100644
--- a/src/lib/components/Card.svelte
+++ b/src/lib/components/Card.svelte
@@ -11,6 +11,8 @@ export let isFavorite = false;
export let showFavoriteIndicator = false;
// to manually override lazy loading for top cards
export let loading_strat : "lazy" | "eager" | undefined;
+// route prefix for language support (/rezepte or /recipes)
+export let routePrefix = '/rezepte';
if(loading_strat === undefined){
loading_strat = "lazy"
}
@@ -27,7 +29,9 @@ onMount(() => {
isloaded = document.querySelector("img")?.complete ? true : false
})
-const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
+// Use germanShortName for images if available (English recipes), otherwise use short_name (German recipes)
+const imageShortName = recipe.germanShortName || recipe.short_name;
+const img_name = imageShortName + ".webp?v=" + recipe.dateModified
+
+
+ {#each ingredients as group, groupIndex}
+
+
updateIngredientGroupName(groupIndex, e)}
+ placeholder="Ingredient group name"
+ />
+ {#each group.list as item, itemIndex}
+
+ updateIngredientItem(groupIndex, itemIndex, 'amount', e)}
+ placeholder="Amt"
+ />
+ updateIngredientItem(groupIndex, itemIndex, 'unit', e)}
+ placeholder="Unit"
+ />
+ updateIngredientItem(groupIndex, itemIndex, 'name', e)}
+ placeholder="Ingredient name"
+ />
+
+ {/each}
+
+ {/each}
+
diff --git a/src/lib/components/EditableInstructions.svelte b/src/lib/components/EditableInstructions.svelte
new file mode 100644
index 0000000..935e706
--- /dev/null
+++ b/src/lib/components/EditableInstructions.svelte
@@ -0,0 +1,140 @@
+
+
+
+
+
+ {#each instructions as group, groupIndex}
+
+
updateInstructionGroupName(groupIndex, e)}
+ placeholder="Instruction section name"
+ />
+ {#each group.steps as step, stepIndex}
+
+ {/each}
+
+ {/each}
+
diff --git a/src/lib/components/RecipeLanguageSwitcher.svelte b/src/lib/components/RecipeLanguageSwitcher.svelte
new file mode 100644
index 0000000..d767ba4
--- /dev/null
+++ b/src/lib/components/RecipeLanguageSwitcher.svelte
@@ -0,0 +1,127 @@
+
+
+
+
+
diff --git a/src/lib/components/TranslationApproval.svelte b/src/lib/components/TranslationApproval.svelte
new file mode 100644
index 0000000..87c5cfa
--- /dev/null
+++ b/src/lib/components/TranslationApproval.svelte
@@ -0,0 +1,677 @@
+
+
+
+
+
+
+
+ {#if errorMessage}
+
+ Error: {errorMessage}
+
+ {/if}
+
+ {#if validationErrors.length > 0}
+
+
Please fix the following errors:
+
+ {#each validationErrors as error}
+ - {error}
+ {/each}
+
+
+ {/if}
+
+ {#if isEditMode && changedFields.length > 0}
+
+ Changed fields: {changedFields.join(', ')}
+
+ Only these fields will be re-translated if you use auto-translate.
+
+ {/if}
+
+ {#if translationState === 'idle'}
+
+
Click "Auto-translate" to generate English translation using DeepL.
+
+
+
+
+
+
+ {:else if translationState === 'translating'}
+
+
+
+ Translating recipe...
+
+
+
+ {:else if translationState === 'preview' || translationState === 'approved'}
+
+
+
🇩🇪 German (Original)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if germanData.tags && germanData.tags.length > 0}
+
+
+
+ {/if}
+
+ {#if germanData.preamble}
+
+
+
+ {/if}
+
+ {#if germanData.addendum}
+
+
+
+ {/if}
+
+ {#if germanData.note}
+
+
+
+ {/if}
+
+ {#if germanData.ingredients && germanData.ingredients.length > 0}
+
+
Ingredients
+
+ {#each germanData.ingredients as ing}
+
{ing.name || 'Ingredients'}
+
+ {#each ing.list as item}
+ - {item.amount} {item.unit} {item.name}
+ {/each}
+
+ {/each}
+
+
+ {/if}
+
+ {#if germanData.instructions && germanData.instructions.length > 0}
+
+
Instructions
+
+ {#each germanData.instructions as inst}
+
{inst.name || 'Steps'}
+
+ {#each inst.steps as step}
+ - {step}
+ {/each}
+
+ {/each}
+
+
+ {/if}
+
+
+
+
🇬🇧 English (Translated)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if editableEnglish?.tags}
+
+
+
+ {/if}
+
+ {#if editableEnglish?.preamble}
+
+
+
+ {/if}
+
+ {#if editableEnglish?.addendum}
+
+
+
+ {/if}
+
+ {#if editableEnglish?.note}
+
+
+
+ {/if}
+
+ {#if editableEnglish?.ingredients && editableEnglish.ingredients.length > 0}
+
+
Ingredients (Editable)
+
+
+ {/if}
+
+ {#if editableEnglish?.instructions && editableEnglish.instructions.length > 0}
+
+
Instructions (Editable)
+
+
+ {/if}
+
+
+
+
+ {#if translationState !== 'approved'}
+
+
+
+ {:else}
+ ✓ Translation Approved
+ {/if}
+
+ {/if}
+
diff --git a/src/lib/components/TranslationFieldComparison.svelte b/src/lib/components/TranslationFieldComparison.svelte
new file mode 100644
index 0000000..d67b1e1
--- /dev/null
+++ b/src/lib/components/TranslationFieldComparison.svelte
@@ -0,0 +1,142 @@
+
+
+
+
+
+
{label}
+ {#if readonly}
+
+ {germanValue || '(empty)'}
+
+ {:else if multiline}
+
+ {:else}
+
+ {/if}
+
diff --git a/src/models/Recipe.ts b/src/models/Recipe.ts
index 4ae3f82..d4d57a8 100644
--- a/src/models/Recipe.ts
+++ b/src/models/Recipe.ts
@@ -39,7 +39,53 @@ const RecipeSchema = new mongoose.Schema(
steps: [String]}],
preamble : String,
addendum : String,
+
+ // English translations
+ translations: {
+ en: {
+ short_name: {type: String}, // English slug for URLs
+ name: {type: String},
+ description: {type: String},
+ preamble: {type: String},
+ addendum: {type: String},
+ note: {type: String},
+ category: {type: String},
+ tags: [String],
+ ingredients: [{
+ name: {type: String, default: ""},
+ list: [{
+ name: {type: String, default: ""},
+ unit: String,
+ amount: String,
+ }]
+ }],
+ instructions: [{
+ name: {type: String, default: ""},
+ steps: [String]
+ }],
+ images: [{
+ alt: String,
+ caption: String,
+ }],
+ translationStatus: {
+ type: String,
+ enum: ['pending', 'approved', 'needs_update'],
+ default: 'pending'
+ },
+ lastTranslated: {type: Date},
+ changedFields: [String],
+ }
+ },
+
+ // Translation metadata for tracking changes
+ translationMetadata: {
+ lastModifiedGerman: {type: Date},
+ fieldsModifiedSinceTranslation: [String],
+ },
}, {timestamps: true}
);
+// Indexes for efficient querying
+RecipeSchema.index({ "translations.en.short_name": 1 });
+
export const Recipe = mongoose.model("Recipe", RecipeSchema);
diff --git a/src/routes/api/recipes/items/[name]/+server.ts b/src/routes/api/recipes/items/[name]/+server.ts
new file mode 100644
index 0000000..6a9fbf0
--- /dev/null
+++ b/src/routes/api/recipes/items/[name]/+server.ts
@@ -0,0 +1,96 @@
+import type { RequestHandler } from '@sveltejs/kit';
+import { Recipe } from '../../../../../models/Recipe';
+import { dbConnect } from '../../../../../utils/db';
+import { error } from '@sveltejs/kit';
+
+/**
+ * GET /api/recipes/items/[name]
+ * Fetch an English recipe by its English short_name
+ */
+export const GET: RequestHandler = async ({ params }) => {
+ await dbConnect();
+
+ try {
+ // Find recipe by English short_name
+ const recipe = await Recipe.findOne({
+ "translations.en.short_name": params.name
+ });
+
+ if (!recipe) {
+ throw error(404, 'Recipe not found');
+ }
+
+ if (!recipe.translations?.en) {
+ throw error(404, 'English translation not available for this recipe');
+ }
+
+ // Return English translation with necessary metadata
+ const englishRecipe = {
+ _id: recipe._id,
+ short_name: recipe.translations.en.short_name,
+ name: recipe.translations.en.name,
+ description: recipe.translations.en.description,
+ preamble: recipe.translations.en.preamble || '',
+ addendum: recipe.translations.en.addendum || '',
+ note: recipe.translations.en.note || '',
+ category: recipe.translations.en.category,
+ tags: recipe.translations.en.tags || [],
+ ingredients: recipe.translations.en.ingredients || [],
+ instructions: recipe.translations.en.instructions || [],
+ images: recipe.images || [], // Use original images with full paths, but English alt/captions
+ // Copy timing/metadata from German version (with defaults)
+ icon: recipe.icon || '',
+ dateCreated: recipe.dateCreated,
+ dateModified: recipe.dateModified,
+ season: recipe.season || [],
+ baking: recipe.baking || { temperature: '', length: '', mode: '' },
+ preparation: recipe.preparation || '',
+ fermentation: recipe.fermentation || { bulk: '', final: '' },
+ portions: recipe.portions || '',
+ cooking: recipe.cooking || '',
+ total_time: recipe.total_time || '',
+ // Include translation status for display
+ translationStatus: recipe.translations.en.translationStatus,
+ // Include German short_name for language switcher
+ germanShortName: recipe.short_name,
+ };
+
+ // Merge English alt/caption with original image paths
+ // Handle both array and single object (there's a bug in add page that sometimes saves as object)
+ const imagesArray = Array.isArray(recipe.images) ? recipe.images : (recipe.images ? [recipe.images] : []);
+
+ if (imagesArray.length > 0) {
+ const translatedImages = recipe.translations.en.images || [];
+
+ if (translatedImages.length > 0) {
+ englishRecipe.images = imagesArray.map((img: any, index: number) => ({
+ mediapath: img.mediapath,
+ alt: translatedImages[index]?.alt || img.alt || '',
+ caption: translatedImages[index]?.caption || img.caption || '',
+ }));
+ } else {
+ // No translated image captions, use German ones
+ englishRecipe.images = imagesArray.map((img: any) => ({
+ mediapath: img.mediapath,
+ alt: img.alt || '',
+ caption: img.caption || '',
+ }));
+ }
+ }
+
+ return new Response(JSON.stringify(englishRecipe), {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ } catch (err: any) {
+ console.error('Error fetching English recipe:', err);
+
+ if (err.status) {
+ throw err;
+ }
+
+ throw error(500, 'Failed to fetch recipe');
+ }
+};
diff --git a/src/routes/api/recipes/items/all_brief/+server.ts b/src/routes/api/recipes/items/all_brief/+server.ts
new file mode 100644
index 0000000..f61089a
--- /dev/null
+++ b/src/routes/api/recipes/items/all_brief/+server.ts
@@ -0,0 +1,31 @@
+import { json, type RequestHandler } from '@sveltejs/kit';
+import type { BriefRecipeType } from '../../../../../types/types';
+import { Recipe } from '../../../../../models/Recipe'
+import { dbConnect } from '../../../../../utils/db';
+import { rand_array } from '$lib/js/randomize';
+
+export const GET: RequestHandler = async ({params}) => {
+ await dbConnect();
+
+ // Find all recipes that have English translations
+ const recipes = await Recipe.find(
+ { 'translations.en': { $exists: true } },
+ '_id translations.en short_name season dateModified icon'
+ ).lean();
+
+ // Map to brief format with English data
+ const found_brief = recipes.map((recipe: any) => ({
+ _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 // For language switcher
+ })) as BriefRecipeType[];
+
+ return json(JSON.parse(JSON.stringify(rand_array(found_brief))));
+};
diff --git a/src/routes/api/recipes/items/category/[category]/+server.ts b/src/routes/api/recipes/items/category/[category]/+server.ts
new file mode 100644
index 0000000..1183ad5
--- /dev/null
+++ b/src/routes/api/recipes/items/category/[category]/+server.ts
@@ -0,0 +1,35 @@
+import { json, type RequestHandler } from '@sveltejs/kit';
+import { Recipe } from '../../../../../../models/Recipe';
+import { dbConnect } from '../../../../../../utils/db';
+import type {BriefRecipeType} from '../../../../../../types/types';
+import { rand_array } from '$lib/js/randomize';
+
+export const GET: RequestHandler = async ({params}) => {
+ await dbConnect();
+
+ // Find recipes in this category that have English translations
+ const recipes = await Recipe.find(
+ {
+ 'translations.en.category': params.category,
+ 'translations.en': { $exists: true }
+ },
+ '_id translations.en short_name images season dateModified icon'
+ ).lean();
+
+ // Map to brief format with English data
+ const englishRecipes = recipes.map((recipe: any) => ({
+ _id: recipe._id,
+ name: recipe.translations.en.name,
+ short_name: recipe.translations.en.short_name,
+ images: recipe.images || [],
+ 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
+ })) as BriefRecipeType[];
+
+ return json(JSON.parse(JSON.stringify(rand_array(englishRecipes))));
+};
diff --git a/src/routes/api/recipes/items/icon/[icon]/+server.ts b/src/routes/api/recipes/items/icon/[icon]/+server.ts
new file mode 100644
index 0000000..4bd64ab
--- /dev/null
+++ b/src/routes/api/recipes/items/icon/[icon]/+server.ts
@@ -0,0 +1,35 @@
+import { json, type RequestHandler } from '@sveltejs/kit';
+import { Recipe } from '../../../../../../models/Recipe';
+import { dbConnect } from '../../../../../../utils/db';
+import type {BriefRecipeType} from '../../../../../../types/types';
+import { rand_array } from '$lib/js/randomize';
+
+export const GET: RequestHandler = async ({params}) => {
+ await dbConnect();
+
+ // Find recipes with this icon that have English translations
+ const recipes = await Recipe.find(
+ {
+ icon: params.icon,
+ 'translations.en': { $exists: true }
+ },
+ '_id translations.en short_name images season dateModified icon'
+ ).lean();
+
+ // Map to brief format with English data
+ const englishRecipes = recipes.map((recipe: any) => ({
+ _id: recipe._id,
+ name: recipe.translations.en.name,
+ short_name: recipe.translations.en.short_name,
+ images: recipe.images || [],
+ 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
+ })) as BriefRecipeType[];
+
+ return json(JSON.parse(JSON.stringify(rand_array(englishRecipes))));
+};
diff --git a/src/routes/api/recipes/items/in_season/[month]/+server.ts b/src/routes/api/recipes/items/in_season/[month]/+server.ts
new file mode 100644
index 0000000..feacbcd
--- /dev/null
+++ b/src/routes/api/recipes/items/in_season/[month]/+server.ts
@@ -0,0 +1,35 @@
+import { json, type RequestHandler } from '@sveltejs/kit';
+import { Recipe } from '../../../../../../models/Recipe'
+import { dbConnect } from '../../../../../../utils/db';
+import { rand_array } from '$lib/js/randomize';
+
+export const GET: RequestHandler = async ({params}) => {
+ await dbConnect();
+
+ // Find recipes in season that have English translations
+ const recipes = await Recipe.find(
+ {
+ season: params.month,
+ icon: {$ne: "🍽️"},
+ 'translations.en': { $exists: true }
+ },
+ '_id translations.en short_name images season dateModified icon'
+ ).lean();
+
+ // Map to format with English data
+ const found_in_season = recipes.map((recipe: any) => ({
+ _id: recipe._id,
+ name: recipe.translations.en.name,
+ short_name: recipe.translations.en.short_name,
+ images: recipe.images || [],
+ 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 // For language switcher
+ }));
+
+ return json(JSON.parse(JSON.stringify(rand_array(found_in_season))));
+};
diff --git a/src/routes/api/recipes/items/tag/[tag]/+server.ts b/src/routes/api/recipes/items/tag/[tag]/+server.ts
new file mode 100644
index 0000000..0f8a995
--- /dev/null
+++ b/src/routes/api/recipes/items/tag/[tag]/+server.ts
@@ -0,0 +1,35 @@
+import { json, type RequestHandler } from '@sveltejs/kit';
+import { Recipe } from '../../../../../../models/Recipe';
+import { dbConnect } from '../../../../../../utils/db';
+import type {BriefRecipeType} from '../../../../../../types/types';
+import { rand_array } from '$lib/js/randomize';
+
+export const GET: RequestHandler = async ({params}) => {
+ await dbConnect();
+
+ // Find recipes with this tag that have English translations
+ const recipes = await Recipe.find(
+ {
+ 'translations.en.tags': params.tag,
+ 'translations.en': { $exists: true }
+ },
+ '_id translations.en short_name images season dateModified icon'
+ ).lean();
+
+ // Map to brief format with English data
+ const englishRecipes = recipes.map((recipe: any) => ({
+ _id: recipe._id,
+ name: recipe.translations.en.name,
+ short_name: recipe.translations.en.short_name,
+ images: recipe.images || [],
+ 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
+ })) as BriefRecipeType[];
+
+ return json(JSON.parse(JSON.stringify(rand_array(englishRecipes))));
+};
diff --git a/src/routes/api/rezepte/translate/+server.ts b/src/routes/api/rezepte/translate/+server.ts
new file mode 100644
index 0000000..290f41e
--- /dev/null
+++ b/src/routes/api/rezepte/translate/+server.ts
@@ -0,0 +1,88 @@
+import { json, error } from '@sveltejs/kit';
+import { translationService } from '$lib/../utils/translation';
+import type { RequestHandler } from './$types';
+
+/**
+ * POST /api/rezepte/translate
+ * Translates recipe data from German to English using DeepL API
+ *
+ * Request body:
+ * - recipe: Recipe object with German content
+ * - fields?: Optional array of specific fields to translate (for partial updates)
+ *
+ * Response:
+ * - translatedRecipe: Translated recipe data
+ */
+export const POST: RequestHandler = async ({ request }) => {
+ try {
+ const body = await request.json();
+ const { recipe, fields } = body;
+
+ if (!recipe) {
+ throw error(400, 'Recipe data is required');
+ }
+
+ // Validate that recipe has required fields
+ if (!recipe.name || !recipe.description) {
+ throw error(400, 'Recipe must have at least name and description');
+ }
+
+ let translatedRecipe;
+
+ // If specific fields are provided, translate only those
+ if (fields && Array.isArray(fields) && fields.length > 0) {
+ translatedRecipe = await translationService.translateFields(recipe, fields);
+ } else {
+ // Translate entire recipe
+ translatedRecipe = await translationService.translateRecipe(recipe);
+ }
+
+ return json({
+ success: true,
+ translatedRecipe,
+ });
+
+ } catch (err: any) {
+ console.error('Translation API error:', err);
+
+ // Handle specific error cases
+ if (err.message?.includes('DeepL API')) {
+ throw error(503, `Translation service error: ${err.message}`);
+ }
+
+ if (err.message?.includes('API key not configured')) {
+ throw error(500, 'Translation service is not configured. Please add DEEPL_API_KEY to environment variables.');
+ }
+
+ // Re-throw SvelteKit errors
+ if (err.status) {
+ throw err;
+ }
+
+ // Generic error
+ throw error(500, `Translation failed: ${err.message || 'Unknown error'}`);
+ }
+};
+
+/**
+ * GET /api/rezepte/translate/health
+ * Health check endpoint to verify translation service is configured
+ */
+export const GET: RequestHandler = async () => {
+ try {
+ // Simple check to verify API key is configured
+ const isConfigured = process.env.DEEPL_API_KEY ? true : false;
+
+ return json({
+ configured: isConfigured,
+ service: 'DeepL Translation API',
+ status: isConfigured ? 'ready' : 'not configured',
+ });
+ } catch (err: any) {
+ return json({
+ configured: false,
+ status: 'error',
+ error: err.message,
+ }, { status: 500 });
+ }
+};
diff --git a/src/routes/recipes/+layout.server.ts b/src/routes/recipes/+layout.server.ts
new file mode 100644
index 0000000..ed6565d
--- /dev/null
+++ b/src/routes/recipes/+layout.server.ts
@@ -0,0 +1,7 @@
+import type { LayoutServerLoad } from "./$types"
+
+export const load : LayoutServerLoad = async ({locals}) => {
+ return {
+ session: await locals.auth()
+ }
+};
diff --git a/src/routes/recipes/+layout.svelte b/src/routes/recipes/+layout.svelte
new file mode 100644
index 0000000..7f771b6
--- /dev/null
+++ b/src/routes/recipes/+layout.svelte
@@ -0,0 +1,25 @@
+
+
+
diff --git a/src/routes/recipes/+page.server.ts b/src/routes/recipes/+page.server.ts
new file mode 100644
index 0000000..ffcef6e
--- /dev/null
+++ b/src/routes/recipes/+page.server.ts
@@ -0,0 +1,22 @@
+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
+ };
+};
diff --git a/src/routes/recipes/+page.svelte b/src/routes/recipes/+page.svelte
new file mode 100644
index 0000000..00eb8ec
--- /dev/null
+++ b/src/routes/recipes/+page.svelte
@@ -0,0 +1,50 @@
+
+
+
+ Bocken Recipes
+
+
+
+
+
+
+
+Recipes
+{data.all_brief.length} recipes and constantly growing...
+
+
+
+
+{#each data.season as recipe}
+
+{/each}
+
+
+{#each categories as category}
+
+ {#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
+
+ {/each}
+
+{/each}
+
diff --git a/src/routes/recipes/[name]/+page.svelte b/src/routes/recipes/[name]/+page.svelte
new file mode 100644
index 0000000..ae5628a
--- /dev/null
+++ b/src/routes/recipes/[name]/+page.svelte
@@ -0,0 +1,352 @@
+
+
+
+ {stripHtmlTags(data.name)} - Bocken's Recipes
+
+
+
+
+
+ {@html ``}
+
+
+
+
+
+
+
+
+
+
+
{data.category}
+
{data.icon}
+
{@html data.name}
+ {#if data.description && ! data.preamble}
+
{data.description}
+ {/if}
+ {#if data.preamble}
+
{@html data.preamble}
+ {/if}
+
+
Keywords:
+
+
+
+
+ {#if data.note}
+
+ {/if}
+
+
+
+
+
+
+
+
+{#if data.addendum}
+ {@html data.addendum}
+{/if}
+
+
Last modified: {formatted_display_date}
+
+
+
+
diff --git a/src/routes/recipes/[name]/+page.ts b/src/routes/recipes/[name]/+page.ts
new file mode 100644
index 0000000..c7f7824
--- /dev/null
+++ b/src/routes/recipes/[name]/+page.ts
@@ -0,0 +1,102 @@
+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
+ };
+}
diff --git a/src/routes/recipes/category/[category]/+page.server.ts b/src/routes/recipes/category/[category]/+page.server.ts
new file mode 100644
index 0000000..f46e039
--- /dev/null
+++ b/src/routes/recipes/category/[category]/+page.server.ts
@@ -0,0 +1,19 @@
+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
+ };
+};
diff --git a/src/routes/recipes/category/[category]/+page.svelte b/src/routes/recipes/category/[category]/+page.svelte
new file mode 100644
index 0000000..5c8000a
--- /dev/null
+++ b/src/routes/recipes/category/[category]/+page.svelte
@@ -0,0 +1,24 @@
+
+
+Recipes in Category {data.category}
:
+
+
+
+ {#each rand_array(data.recipes) as recipe}
+
+ {/each}
+
+
diff --git a/src/routes/recipes/icon/[icon]/+page.server.ts b/src/routes/recipes/icon/[icon]/+page.server.ts
new file mode 100644
index 0000000..e0338ae
--- /dev/null
+++ b/src/routes/recipes/icon/[icon]/+page.server.ts
@@ -0,0 +1,22 @@
+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
+ };
+};
diff --git a/src/routes/recipes/icon/[icon]/+page.svelte b/src/routes/recipes/icon/[icon]/+page.svelte
new file mode 100644
index 0000000..aa27112
--- /dev/null
+++ b/src/routes/recipes/icon/[icon]/+page.svelte
@@ -0,0 +1,17 @@
+
+
+
+ {#each rand_array(data.season) as recipe}
+
+ {/each}
+
+
diff --git a/src/routes/recipes/season/[month]/+page.server.ts b/src/routes/recipes/season/[month]/+page.server.ts
new file mode 100644
index 0000000..3c7d510
--- /dev/null
+++ b/src/routes/recipes/season/[month]/+page.server.ts
@@ -0,0 +1,19 @@
+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
+ };
+};
diff --git a/src/routes/recipes/season/[month]/+page.svelte b/src/routes/recipes/season/[month]/+page.svelte
new file mode 100644
index 0000000..2503273
--- /dev/null
+++ b/src/routes/recipes/season/[month]/+page.svelte
@@ -0,0 +1,18 @@
+
+
+
+ {#each rand_array(data.season) as recipe}
+
+ {/each}
+
+
diff --git a/src/routes/recipes/tag/[tag]/+page.server.ts b/src/routes/recipes/tag/[tag]/+page.server.ts
new file mode 100644
index 0000000..0287032
--- /dev/null
+++ b/src/routes/recipes/tag/[tag]/+page.server.ts
@@ -0,0 +1,19 @@
+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
+ };
+};
diff --git a/src/routes/recipes/tag/[tag]/+page.svelte b/src/routes/recipes/tag/[tag]/+page.svelte
new file mode 100644
index 0000000..a3fe294
--- /dev/null
+++ b/src/routes/recipes/tag/[tag]/+page.svelte
@@ -0,0 +1,24 @@
+
+
+Recipes with Keyword {data.tag}
:
+
+
+
+ {#each rand_array(data.recipes) as recipe}
+
+ {/each}
+
+
diff --git a/src/routes/rezepte/[name]/+page.svelte b/src/routes/rezepte/[name]/+page.svelte
index 26e5a1b..4b3e522 100644
--- a/src/routes/rezepte/[name]/+page.svelte
+++ b/src/routes/rezepte/[name]/+page.svelte
@@ -13,6 +13,7 @@
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;
@@ -278,8 +279,23 @@ h4{
{@html ``}
+
+
+ {#if data.hasEnglishTranslation}
+
+ {/if}
+
+{#if data.hasEnglishTranslation}
+
+{/if}
+
{data.category}
diff --git a/src/routes/rezepte/[name]/+page.ts b/src/routes/rezepte/[name]/+page.ts
index bf0af8e..1d52f90 100644
--- a/src/routes/rezepte/[name]/+page.ts
+++ b/src/routes/rezepte/[name]/+page.ts
@@ -104,11 +104,17 @@ export async function load({ fetch, params, url}) {
// Generate JSON-LD server-side
const recipeJsonLd = generateRecipeJsonLd(item);
-
+
+ // Check if English translation exists
+ const hasEnglishTranslation = !!(item.translations?.en?.short_name);
+ const englishShortName = item.translations?.en?.short_name || '';
+
return {
...item,
isFavorite,
multiplier,
- recipeJsonLd
+ recipeJsonLd,
+ hasEnglishTranslation,
+ englishShortName,
};
}
diff --git a/src/routes/rezepte/add/+page.svelte b/src/routes/rezepte/add/+page.svelte
index 6fea4cb..f6579dd 100644
--- a/src/routes/rezepte/add/+page.svelte
+++ b/src/routes/rezepte/add/+page.svelte
@@ -1,12 +1,17 @@