recipes: overhaul nutrition editor UI and defer saves to form submission
- Nutrition mappings and global overwrites are now local-only until the recipe is saved, preventing premature DB writes on generate/edit - Generate endpoint supports ?preview=true for non-persisting previews - Show existing nutrition data immediately instead of requiring generate - Replace raw checkboxes with Toggle component for global overwrites, initialized from existing NutritionOverwrite records - Fix search dropdown readability (solid backgrounds, proper theming) - Use fuzzy search (fzf-style) for manual nutrition ingredient lookup - Swap ingredient display: German primary, English in brackets - Allow editing g/u on manually mapped ingredients - Make translation optional: separate save (FAB) and translate buttons - "Vollständig neu übersetzen" now triggers actual full retranslation - Show existing translation inline instead of behind a button - Replace nord0 dark backgrounds with semantic theme variables
This commit is contained in:
@@ -4,7 +4,7 @@ import { dbConnect } from '$utils/db';
|
||||
import { isEnglish } from '$lib/server/recipeHelpers';
|
||||
import { generateNutritionMappings } from '$lib/server/nutritionMatcher';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, locals }) => {
|
||||
export const POST: RequestHandler = async ({ params, locals, url }) => {
|
||||
await locals.auth();
|
||||
await dbConnect();
|
||||
|
||||
@@ -19,6 +19,14 @@ export const POST: RequestHandler = async ({ params, locals }) => {
|
||||
const ingredients = recipe.ingredients || [];
|
||||
const translatedIngredients = recipe.translations?.en?.ingredients;
|
||||
|
||||
const newMappings = await generateNutritionMappings(ingredients, translatedIngredients);
|
||||
const preview = url.searchParams.get('preview') === 'true';
|
||||
|
||||
// In preview mode, return pure auto-matches without saving (client merges manual edits)
|
||||
if (preview) {
|
||||
return json({ mappings: newMappings, count: newMappings.length });
|
||||
}
|
||||
|
||||
// Preserve manually edited mappings
|
||||
const existingMappings = recipe.nutritionMappings || [];
|
||||
const manualMappings = new Map(
|
||||
@@ -27,8 +35,6 @@ export const POST: RequestHandler = async ({ params, locals }) => {
|
||||
.map((m: any) => [`${m.sectionIndex}-${m.ingredientIndex}`, m])
|
||||
);
|
||||
|
||||
const newMappings = await generateNutritionMappings(ingredients, translatedIngredients);
|
||||
|
||||
// Merge: keep manual edits, use new auto-matches for the rest
|
||||
const finalMappings = newMappings.map(m => {
|
||||
const key = `${m.sectionIndex}-${m.ingredientIndex}`;
|
||||
|
||||
@@ -1,41 +1,48 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { NUTRITION_DB } from '$lib/data/nutritionDb';
|
||||
import { BLS_DB } from '$lib/data/blsDb';
|
||||
import { fuzzyScore } from '$lib/js/fuzzy';
|
||||
|
||||
/** GET: Search BLS + USDA nutrition databases by name substring */
|
||||
/** GET: Search BLS + USDA nutrition databases by fuzzy name match */
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const q = (url.searchParams.get('q') || '').toLowerCase().trim();
|
||||
if (q.length < 2) return json([]);
|
||||
|
||||
const results: { source: 'bls' | 'usda'; id: string; name: string; category: string; calories: number }[] = [];
|
||||
const scored: { source: 'bls' | 'usda'; id: string; name: string; category: string; calories: number; score: number }[] = [];
|
||||
|
||||
// Search BLS first (primary)
|
||||
// Search BLS (primary)
|
||||
for (const entry of BLS_DB) {
|
||||
if (results.length >= 30) break;
|
||||
if (entry.nameDe.toLowerCase().includes(q) || entry.nameEn.toLowerCase().includes(q)) {
|
||||
results.push({
|
||||
const scoreDe = fuzzyScore(q, entry.nameDe.toLowerCase());
|
||||
const scoreEn = entry.nameEn ? fuzzyScore(q, entry.nameEn.toLowerCase()) : 0;
|
||||
const best = Math.max(scoreDe, scoreEn);
|
||||
if (best > 0) {
|
||||
scored.push({
|
||||
source: 'bls',
|
||||
id: entry.blsCode,
|
||||
name: `${entry.nameDe}${entry.nameEn ? ` (${entry.nameEn})` : ''}`,
|
||||
category: entry.category,
|
||||
calories: entry.per100g.calories,
|
||||
score: best,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Then USDA
|
||||
// Search USDA
|
||||
for (const entry of NUTRITION_DB) {
|
||||
if (results.length >= 40) break;
|
||||
if (entry.name.toLowerCase().includes(q)) {
|
||||
results.push({
|
||||
const s = fuzzyScore(q, entry.name.toLowerCase());
|
||||
if (s > 0) {
|
||||
scored.push({
|
||||
source: 'usda',
|
||||
id: String(entry.fdcId),
|
||||
name: entry.name,
|
||||
category: entry.category,
|
||||
calories: entry.per100g.calories,
|
||||
score: s,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return json(results.slice(0, 30));
|
||||
// Sort by score descending, return top 30 (without score field)
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
return json(scored.slice(0, 30).map(({ score, ...rest }) => rest));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user