diff --git a/package.json b/package.json
index 3cb192a..9b44e8c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "homepage",
- "version": "1.19.0",
+ "version": "1.20.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/src/lib/components/fitness/FoodSearch.svelte b/src/lib/components/fitness/FoodSearch.svelte
index 94db09f..e04c93c 100644
--- a/src/lib/components/fitness/FoodSearch.svelte
+++ b/src/lib/components/fitness/FoodSearch.svelte
@@ -434,7 +434,7 @@
{item.calories} kcal
- {#if showDetailLinks && (item.source === 'bls' || item.source === 'usda')}
+ {#if showDetailLinks && (item.source === 'bls' || item.source === 'usda' || item.source === 'off')}
diff --git a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte
index 161fd84..2ab5102 100644
--- a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte
+++ b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte
@@ -1565,10 +1565,10 @@
- {#if entry.source === 'bls' || entry.source === 'usda'}
+ {#if entry.source === 'bls' || entry.source === 'usda' || entry.source === 'off'}
{entry.name}
- {:else if entry.source === 'recipe' && entry.sourceId}
-
{entry.name}
+ {:else if (entry.source === 'recipe' || entry.source === 'custom') && entry.sourceId}
+
{entry.name}
{:else}
{entry.name}
{/if}
diff --git a/src/routes/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]/+page.server.ts b/src/routes/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]/+page.server.ts
index d51eff0..214d403 100644
--- a/src/routes/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]/+page.server.ts
+++ b/src/routes/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]/+page.server.ts
@@ -4,6 +4,8 @@ import { NUTRITION_DB } from '$lib/data/nutritionDb';
import { BLS_DB } from '$lib/data/blsDb';
import { DRI_MALE } from '$lib/data/dailyReferenceIntake';
import { Recipe } from '$models/Recipe';
+import { OpenFoodFact } from '$models/OpenFoodFact';
+import { CustomMeal } from '$models/CustomMeal';
import { dbConnect } from '$utils/db';
import { computeRecipeNutritionTotals, resolvePer100g, parseAmount, resolveReferencedNutrition } from '$lib/server/nutritionMatcher';
import { FoodLogEntry } from '$models/FoodLogEntry';
@@ -61,7 +63,7 @@ async function computeRecipePer100g(id: string): Promise
>
export const load: PageServerLoad = async ({ params, url }) => {
const { source, id } = params;
- if (source !== 'bls' && source !== 'usda' && source !== 'recipe') {
+ if (source !== 'bls' && source !== 'usda' && source !== 'recipe' && source !== 'off' && source !== 'custom') {
throw error(404, 'Invalid source');
}
@@ -126,6 +128,86 @@ export const load: PageServerLoad = async ({ params, url }) => {
};
}
+ if (source === 'off') {
+ await dbConnect();
+ const entry = await OpenFoodFact.findOne({ barcode: id }).lean();
+ if (!entry) throw error(404, 'Food not found');
+ const portions: { description: string; grams: number }[] = [];
+ if (entry.serving?.grams) {
+ portions.push(entry.serving as { description: string; grams: number });
+ }
+ return {
+ food: {
+ source: 'off' as const,
+ id: entry.barcode,
+ name: entry.nameDe || entry.name,
+ nameDe: entry.nameDe,
+ nameEn: entry.nameDe ? entry.name : undefined,
+ category: entry.category || '',
+ per100g: entry.per100g as unknown as Record,
+ brands: entry.brands,
+ nutriscore: entry.nutriscore,
+ portions,
+ },
+ dri: DRI_MALE,
+ };
+ }
+
+ if (source === 'custom') {
+ await dbConnect();
+ const meal = await CustomMeal.findById(id).lean();
+ if (!meal) throw error(404, 'Meal not found');
+
+ // Aggregate per100g from ingredients
+ const totals: Record = {};
+ let totalGrams = 0;
+ for (const ing of meal.ingredients) {
+ const r = ing.amountGrams / 100;
+ totalGrams += ing.amountGrams;
+ for (const [k, v] of Object.entries(ing.per100g)) {
+ if (typeof v === 'number') {
+ totals[k] = (totals[k] || 0) + v * r;
+ }
+ }
+ }
+ const per100g: Record = {};
+ const scale = totalGrams > 0 ? 100 / totalGrams : 0;
+ for (const [k, v] of Object.entries(totals)) {
+ per100g[k] = v * scale;
+ }
+
+ // Use logged per100g snapshot if provided
+ const logEntryId = url.searchParams.get('logEntry');
+ if (logEntryId) {
+ const logEntry = await FoodLogEntry.findById(logEntryId).select('per100g').lean();
+ if (logEntry?.per100g) {
+ Object.assign(per100g, logEntry.per100g);
+ }
+ }
+
+ return {
+ food: {
+ source: 'custom' as const,
+ id: String(meal._id),
+ name: meal.name,
+ category: 'Custom Meal',
+ per100g,
+ totalGrams,
+ ingredients: meal.ingredients.map((ing: any) => ({
+ name: ing.name,
+ source: ing.source,
+ sourceId: ing.sourceId,
+ amountGrams: ing.amountGrams,
+ calories: (ing.per100g?.calories ?? 0) * ing.amountGrams / 100,
+ protein: (ing.per100g?.protein ?? 0) * ing.amountGrams / 100,
+ fat: (ing.per100g?.fat ?? 0) * ing.amountGrams / 100,
+ carbs: (ing.per100g?.carbs ?? 0) * ing.amountGrams / 100,
+ })),
+ },
+ dri: DRI_MALE,
+ };
+ }
+
// USDA
const fdcId = Number(id);
const entry = NUTRITION_DB.find(e => e.fdcId === fdcId);
diff --git a/src/routes/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]/+page.svelte b/src/routes/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]/+page.svelte
index b3bb541..614bbd9 100644
--- a/src/routes/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]/+page.svelte
+++ b/src/routes/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]/+page.svelte
@@ -182,8 +182,14 @@
{food.nameEn}
{/if}
-
{food.source === 'bls' ? 'BLS' : food.source === 'usda' ? 'USDA' : isEn ? 'Recipe' : 'Rezept'}
+
{food.source === 'bls' ? 'BLS' : food.source === 'usda' ? 'USDA' : food.source === 'off' ? 'OFF' : food.source === 'custom' ? (isEn ? 'Custom Meal' : 'Eigene Mahlzeit') : isEn ? 'Recipe' : 'Rezept'}
{food.category}
+ {#if food.brands}
+
{food.brands}
+ {/if}
+ {#if food.nutriscore}
+
Nutri-Score {food.nutriscore.toUpperCase()}
+ {/if}
{#if food.recipeSlug}
{isEn ? 'View recipe' : 'Zum Rezept'}
@@ -256,6 +262,33 @@
+
+ {#if food.ingredients?.length}
+