From 4fd6a5cc77da4566b05cbfcfacd14ac290a355dc Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 15:45:21 +0200 Subject: [PATCH] feat(mana/web/nutriphi): meal detail page + foods breakdown + thumbnail-aware lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New /nutriphi/[id] route — the missing endpoint of the photo workflow. Loads the meal via inline useLiveQueryWithDefault(loadMealById, ...) so the closure captures page.params.id directly (planta DetailView pattern). Detail page features: - Full-resolution photo, click-to-expand lightbox modal - All six nutrient cards with the same color tokens as the dashboard - "Erkannte Bestandteile" — list of AI-identified foods (name + quantity + kcal) so users can see what Gemini actually parsed - Inline edit form (mealtype + description + 6 nutrient inputs), persists via mealMutations.update - "🔄 Erneut analysieren" for photo meals — calls analyze on the stored URL and overwrites description + nutrition without re-uploading the file - Two-stage delete confirm Add page (add/+page.svelte): - Captures upload.thumbnailUrl + analysis.foods after KI analysis - Persists both via the extended createFromPhoto signature - Shows the foods breakdown card under the confidence badge so users see the parse before saving (closes the trust gap on low-confidence runs) List pages (Heute + History): - Switch to photoThumbnailUrl ?? photoUrl for the row image — saves bandwidth on the most-rendered surface - Each meal row is now a link to /nutriphi/[id] - History row layout split into + sibling delete button so the delete click doesn't bubble through navigation Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/routes/(app)/nutriphi/+page.svelte | 11 +- .../routes/(app)/nutriphi/[id]/+page.svelte | 518 ++++++++++++++++++ .../routes/(app)/nutriphi/add/+page.svelte | 39 +- .../(app)/nutriphi/history/+page.svelte | 64 ++- 4 files changed, 596 insertions(+), 36 deletions(-) create mode 100644 apps/mana/apps/web/src/routes/(app)/nutriphi/[id]/+page.svelte diff --git a/apps/mana/apps/web/src/routes/(app)/nutriphi/+page.svelte b/apps/mana/apps/web/src/routes/(app)/nutriphi/+page.svelte index 9c56c5061..ad2a3f327 100644 --- a/apps/mana/apps/web/src/routes/(app)/nutriphi/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/nutriphi/+page.svelte @@ -196,13 +196,14 @@ {:else}
{#each todaysMeals as meal (meal.id)} -
- {#if meal.photoUrl} + {#if meal.photoThumbnailUrl || meal.photoUrl} {meal.description} {/if}
-
+
{/each}
{/if} diff --git a/apps/mana/apps/web/src/routes/(app)/nutriphi/[id]/+page.svelte b/apps/mana/apps/web/src/routes/(app)/nutriphi/[id]/+page.svelte new file mode 100644 index 000000000..0c8572541 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/nutriphi/[id]/+page.svelte @@ -0,0 +1,518 @@ + + + + {meal?.description ?? 'Mahlzeit'} - NutriPhi - Mana + + +
+ + + Zurueck + + + {#if !meal} +
+

Mahlzeit nicht gefunden.

+
+ {:else} + {#if error} +
+ {error} +
+ {/if} + + + {#if meal.photoUrl} + + {/if} + + +
+
+ + {getMealTypeLabel(meal.mealType)} + + + {formatDateTime(meal.createdAt)} + + {#if meal.inputType === 'photo'} + 📷 + {/if} + {#if meal.confidence > 0 && meal.confidence < 1} + + KI {Math.round(meal.confidence * 100)}% + + {/if} +
+ + {#if !editing} +

{meal.description}

+ {#if meal.nutrition} +
+
+
+
+ Kalorien +
+

+ {meal.nutrition.calories} + kcal +

+
+
+
+
+ Protein +
+

+ {meal.nutrition.protein}g +

+
+
+
+
+ Kohlenhydrate +
+

+ {meal.nutrition.carbohydrates}g +

+
+
+
+
+ Fett +
+

+ {meal.nutrition.fat}g +

+
+
+
+
Ballaststoffe: {meal.nutrition.fiber}g
+
Zucker: {meal.nutrition.sugar}g
+
+ {/if} + +
+ + {#if meal.inputType === 'photo' && meal.photoUrl} + + {/if} + {#if !confirmDelete} + + {:else} +
+ Sicher? + + +
+ {/if} +
+ {:else} + +
+
+ +
+ {#each mealTypes as type} + + {/each} +
+
+ +
+ + +
+ +
+

Naehrwerte

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ {/if} +
+ + + {#if !editing && meal.foods && meal.foods.length > 0} +
+

+ Erkannte Bestandteile +

+
    + {#each meal.foods as food} +
  • +
    + {food.name} + {#if food.quantity} + + · {food.quantity} + {/if} +
    + {#if food.calories != null} + + {food.calories} kcal + + {/if} +
  • + {/each} +
+
+ {/if} + {/if} +
+ + +{#if lightboxOpen && meal?.photoUrl} + +{/if} diff --git a/apps/mana/apps/web/src/routes/(app)/nutriphi/add/+page.svelte b/apps/mana/apps/web/src/routes/(app)/nutriphi/add/+page.svelte index f43b8d138..836b2c6fe 100644 --- a/apps/mana/apps/web/src/routes/(app)/nutriphi/add/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/nutriphi/add/+page.svelte @@ -8,7 +8,7 @@ textAnalysisMutations, } from '$lib/modules/nutriphi/mutations'; import { MEAL_TYPE_LABELS, suggestMealType } from '$lib/modules/nutriphi/constants'; - import type { MealType, NutritionData } from '$lib/modules/nutriphi/types'; + import type { AnalyzedFood, MealType, NutritionData } from '$lib/modules/nutriphi/types'; import { ArrowLeft } from '@mana/shared-icons'; const allFavorites = useAllFavorites(); @@ -32,7 +32,9 @@ let photoPreviewUrl = $state(null); let photoMediaId = $state(null); let photoUploadedUrl = $state(null); + let photoUploadedThumbnailUrl = $state(null); let aiConfidence = $state(null); + let aiFoods = $state(null); let analyzing = $state(false); let analyzed = $state(false); @@ -67,7 +69,9 @@ photoPreviewUrl = null; photoMediaId = null; photoUploadedUrl = null; + photoUploadedThumbnailUrl = null; aiConfidence = null; + aiFoods = null; analyzed = false; } @@ -93,7 +97,9 @@ photoPreviewUrl = URL.createObjectURL(file); photoMediaId = null; photoUploadedUrl = null; + photoUploadedThumbnailUrl = null; aiConfidence = null; + aiFoods = null; analyzed = false; error = ''; } @@ -106,6 +112,7 @@ const { upload, analysis } = await photoMutations.uploadAndAnalyze(photoFile); photoMediaId = upload.mediaId; photoUploadedUrl = upload.publicUrl; + photoUploadedThumbnailUrl = upload.thumbnailUrl; // Prefill the same fields the text mode uses, so the user can review/edit. if (analysis.description) description = analysis.description; @@ -118,6 +125,7 @@ sugar = analysis.totalNutrition.sugar ?? null; } aiConfidence = analysis.confidence ?? null; + aiFoods = analysis.foods?.length ? analysis.foods : null; analyzed = true; } catch (err) { console.error('photo analysis failed:', err); @@ -189,7 +197,9 @@ nutrition, photoMediaId, photoUrl: photoUploadedUrl, + photoThumbnailUrl: photoUploadedThumbnailUrl, confidence: aiConfidence ?? 0.8, + foods: aiFoods, }); } else { await mealMutations.create({ @@ -348,6 +358,33 @@ {/if} {/if} + + {#if analyzed && aiFoods && aiFoods.length > 0} +
+

+ Erkannte Bestandteile +

+
    + {#each aiFoods as food} +
  • + + {food.name} + {#if food.quantity} + · {food.quantity} + {/if} + + {#if food.calories != null} + + {food.calories} kcal + + {/if} +
  • + {/each} +
+
+ {/if} {/if} diff --git a/apps/mana/apps/web/src/routes/(app)/nutriphi/history/+page.svelte b/apps/mana/apps/web/src/routes/(app)/nutriphi/history/+page.svelte index 328c9853f..d2aad7914 100644 --- a/apps/mana/apps/web/src/routes/(app)/nutriphi/history/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/nutriphi/history/+page.svelte @@ -164,38 +164,42 @@