feat: manual nutrition entry on recipe editor
Lets authors enter nutrition values by hand instead of relying only on the AI estimate; the detail-page panel now shows whether data was manually entered or AI-estimated and offers to switch between them. v0.35.0
This commit is contained in:
@@ -61,6 +61,11 @@ export default async function EditRecipePage({ params }: Params) {
|
||||
preview: getPublicUrl(photo.storageKey),
|
||||
})),
|
||||
isBatchCook: recipe.isBatchCook,
|
||||
// Only pre-fill the manual-nutrition editor with existing values when
|
||||
// they were actually manually entered — AI-estimated data shouldn't
|
||||
// silently become "manual" just because the recipe happened to have
|
||||
// an estimate on file when it was opened for editing.
|
||||
nutrition: recipe.nutritionManual ? recipe.nutritionData?.perServing ?? null : null,
|
||||
dishes: recipe.batchDishes.map((dish) => ({
|
||||
id: dish.id,
|
||||
name: dish.name,
|
||||
|
||||
@@ -396,7 +396,7 @@ export default async function RecipePage({ params }: Params) {
|
||||
order: ing.order,
|
||||
}))}
|
||||
/>
|
||||
<NutritionPanel recipeId={id} initialData={recipe.nutritionData} />
|
||||
<NutritionPanel recipeId={id} initialData={recipe.nutritionData} initialManual={recipe.nutritionManual} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db, recipes, recipeIngredients } from "@epicure/db";
|
||||
import { db, recipes } from "@epicure/db";
|
||||
import { eq, and, or, ne } from "@epicure/db";
|
||||
import { requireSessionOrApiKey } from "@/lib/api-auth";
|
||||
import { applyRateLimit } from "@/lib/rate-limit";
|
||||
@@ -25,7 +25,7 @@ export async function GET(req: NextRequest, { params }: Params) {
|
||||
|
||||
if (!recipe) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
return NextResponse.json({ nutrition: recipe.nutritionData ?? null });
|
||||
return NextResponse.json({ nutrition: recipe.nutritionData ?? null, manual: recipe.nutritionManual });
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest, { params }: Params) {
|
||||
@@ -59,7 +59,7 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
);
|
||||
if (!result.ok) return result.response;
|
||||
|
||||
await db.update(recipes).set({ nutritionData: result.data }).where(and(eq(recipes.id, id), eq(recipes.authorId, session!.user.id)));
|
||||
await db.update(recipes).set({ nutritionData: result.data, nutritionManual: false }).where(and(eq(recipes.id, id), eq(recipes.authorId, session!.user.id)));
|
||||
|
||||
return NextResponse.json({ nutrition: result.data });
|
||||
}
|
||||
|
||||
@@ -56,6 +56,16 @@ const UpdateRecipeSchema = z.object({
|
||||
freezerNote: z.string().max(300).optional(),
|
||||
dayOfInstructions: z.string().min(1).max(1000),
|
||||
})).max(10).optional(),
|
||||
// Manually-entered per-serving nutrition — set to null to clear it and
|
||||
// re-enable the AI-estimate action on the recipe page.
|
||||
nutrition: z.object({
|
||||
calories: z.number().min(0).max(10000),
|
||||
proteinG: z.number().min(0).max(1000),
|
||||
carbsG: z.number().min(0).max(1000),
|
||||
fatG: z.number().min(0).max(1000),
|
||||
fiberG: z.number().min(0).max(1000),
|
||||
sodiumMg: z.number().min(0).max(100000),
|
||||
}).nullable().optional(),
|
||||
});
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
@@ -159,6 +169,10 @@ export async function PUT(req: NextRequest, { params }: Params) {
|
||||
if (data.tags !== undefined) updates.tags = data.tags;
|
||||
if (data.dietaryTags !== undefined) updates.dietaryTags = data.dietaryTags;
|
||||
if (data.isBatchCook !== undefined) updates.isBatchCook = data.isBatchCook;
|
||||
if (data.nutrition !== undefined) {
|
||||
updates.nutritionData = data.nutrition ? { perServing: data.nutrition } : null;
|
||||
updates.nutritionManual = !!data.nutrition;
|
||||
}
|
||||
|
||||
await tx.update(recipes).set(updates).where(eq(recipes.id, id));
|
||||
|
||||
|
||||
@@ -60,6 +60,17 @@ const CreateRecipeSchema = z.object({
|
||||
freezerNote: z.string().max(300).optional(),
|
||||
dayOfInstructions: z.string().min(1).max(1000),
|
||||
})).max(10).default([]),
|
||||
// Manually-entered per-serving nutrition — takes precedence over (and
|
||||
// disables) the AI-estimate action on the recipe page, since re-estimating
|
||||
// would silently discard values the author entered on purpose.
|
||||
nutrition: z.object({
|
||||
calories: z.number().min(0).max(10000),
|
||||
proteinG: z.number().min(0).max(1000),
|
||||
carbsG: z.number().min(0).max(1000),
|
||||
fatG: z.number().min(0).max(1000),
|
||||
fiberG: z.number().min(0).max(1000),
|
||||
sodiumMg: z.number().min(0).max(100000),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
@@ -132,6 +143,8 @@ export async function POST(req: NextRequest) {
|
||||
language: data.language,
|
||||
sourceUrl: data.sourceUrl,
|
||||
isBatchCook: data.isBatchCook,
|
||||
nutritionData: data.nutrition ? { perServing: data.nutrition } : undefined,
|
||||
nutritionManual: !!data.nutrition,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user