Files
Epicure/apps/web/app/(app)/recipes/[id]/edit/page.tsx
T
Arnaud f6975e98a9 fix: recipe/storage tier limits are lifetime totals, not monthly (v0.58.0)
Recipe count and storage usage shared the monthly user_usage bucket with
AI calls, so both incorrectly reset every month even though nothing was
deleted. Only AI calls should be monthly.

Recipe count and storage are now derived live from real data (recipes,
recipe/review photos, avatar) instead of a counter — deleting a photo or
recipe is itself the "decrement", no extra wiring needed. Storage size is
tracked per-row (recipePhotos.sizeMb, ratings.photoSizeMb, users.avatarSizeMb)
and threaded through presign -> upload -> save.

Also fixes avatar removal silently no-oping (client sent a field the PATCH
schema didn't recognize).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 20:32:31 +02:00

104 lines
3.8 KiB
TypeScript

import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
import { db, recipes } from "@epicure/db";
import { and, eq } from "@epicure/db";
import { RecipeForm } from "@/components/recipe/recipe-form";
import { getPublicUrl } from "@/lib/storage";
import { getMessages } from "@/lib/i18n/server";
type Params = { params: Promise<{ id: string }> };
export const metadata: Metadata = {};
export default async function EditRecipePage({ params }: Params) {
const { id } = await params;
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return null;
const recipe = await db.query.recipes.findFirst({
where: and(eq(recipes.id, id), eq(recipes.authorId, session.user.id)),
with: {
ingredients: { orderBy: (t, { asc }) => asc(t.order) },
steps: { orderBy: (t, { asc }) => asc(t.order) },
photos: { orderBy: (t, { asc }) => asc(t.order) },
batchDishes: { orderBy: (t, { asc }) => asc(t.order) },
},
});
if (!recipe) notFound();
const m = getMessages((session.user as { locale?: string }).locale);
const defaultValues = {
title: recipe.title,
description: recipe.description ?? undefined,
baseServings: recipe.baseServings,
recipeType: recipe.recipeType,
visibility: recipe.visibility,
difficulty: recipe.difficulty,
prepMins: recipe.prepMins,
cookMins: recipe.cookMins,
tags: recipe.tags ?? [],
dietaryTags: (recipe.dietaryTags as Record<string, boolean | undefined>) ?? {},
ingredients: recipe.ingredients.map((ing) => ({
id: ing.id,
rawName: ing.rawName,
quantity: ing.quantity ?? "",
unit: ing.unit ?? "",
note: ing.note ?? "",
})),
steps: recipe.steps.map((step) => {
// Show the largest unit that divides evenly into the stored seconds,
// so editing a 90-minute braise shows "90 min" rather than "5400 sec".
const seconds = step.timerSeconds ?? 0;
const timer = seconds > 0 && seconds % 3600 === 0
? { value: String(seconds / 3600), unit: "hours" as const }
: seconds > 0 && seconds % 60 === 0
? { value: String(seconds / 60), unit: "minutes" as const }
: { value: seconds > 0 ? String(seconds) : "", unit: "seconds" as const };
return {
id: step.id,
instruction: step.instruction,
timerSeconds: timer.value,
timerUnit: timer.unit,
appliesTo: step.appliesTo ?? [],
};
}),
photos: recipe.photos.map((photo) => ({
key: photo.storageKey,
isCover: photo.isCover,
preview: getPublicUrl(photo.storageKey),
sizeMb: photo.sizeMb,
})),
coverIcon: recipe.coverIcon,
coverColor: recipe.coverColor,
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,
description: dish.description ?? "",
fridgeDays: String(dish.fridgeDays),
freezerFriendly: dish.freezerFriendly,
freezerNote: dish.freezerNote ?? "",
dayOfInstructions: dish.dayOfInstructions,
})),
};
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">{m.recipes.editTitle}</h1>
<p className="text-muted-foreground mt-1">{recipe.title}</p>
</div>
<RecipeForm recipeId={id} defaultValues={defaultValues} />
</div>
);
}