fix: photos lost on save, multi-upload only kept last file, thumbnails 400
recipe-form/API routes were never sending/persisting photos on create+update. PhotoUploader accumulated uploads via a stale closure over the photos prop, so only the last file of a multi-select survived. Once both were fixed, thumbnails still 400'd because Next's image optimizer blocks upstream hosts resolving to private/loopback IPs (localhost:9000 MinIO) — skip optimization for storage-hosted photos since they're already web-sized user uploads.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db, recipes, recipeIngredients, recipeSteps, recipeSnapshots, ratings } from "@epicure/db";
|
||||
import { db, recipes, recipeIngredients, recipeSteps, recipePhotos, recipeSnapshots, ratings } from "@epicure/db";
|
||||
import { eq, and, max, isNotNull } from "@epicure/db";
|
||||
import { z } from "zod";
|
||||
import { requireSessionOrApiKey } from "@/lib/api-auth";
|
||||
@@ -41,6 +41,10 @@ const UpdateRecipeSchema = z.object({
|
||||
timerSeconds: z.number().int().min(0).max(86400).optional(),
|
||||
order: z.number().int(),
|
||||
})).max(100).optional(),
|
||||
photos: z.array(z.object({
|
||||
key: z.string().min(1).max(500),
|
||||
isCover: z.boolean().default(false),
|
||||
})).max(20).optional(),
|
||||
});
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
@@ -70,7 +74,11 @@ export async function PUT(req: NextRequest, { params }: Params) {
|
||||
|
||||
const existing = 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) } },
|
||||
with: {
|
||||
ingredients: { orderBy: (t, { asc }) => asc(t.order) },
|
||||
steps: { orderBy: (t, { asc }) => asc(t.order) },
|
||||
photos: true,
|
||||
},
|
||||
});
|
||||
if (!existing) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
@@ -162,8 +170,29 @@ export async function PUT(req: NextRequest, { params }: Params) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.photos !== undefined) {
|
||||
await tx.delete(recipePhotos).where(eq(recipePhotos.recipeId, id));
|
||||
if (data.photos.length > 0) {
|
||||
await tx.insert(recipePhotos).values(
|
||||
data.photos.map((photo, i) => ({
|
||||
id: crypto.randomUUID(),
|
||||
recipeId: id,
|
||||
storageKey: photo.key,
|
||||
order: i,
|
||||
isCover: photo.isCover,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (data.photos !== undefined) {
|
||||
const newKeys = new Set(data.photos.map((p) => p.key));
|
||||
const removedKeys = existing.photos.filter((p) => !newKeys.has(p.storageKey)).map((p) => p.storageKey);
|
||||
await Promise.all(removedKeys.map((key) => deleteObject(key).catch(() => {})));
|
||||
}
|
||||
|
||||
const updated = await getOwnedRecipe(id, session!.user.id);
|
||||
void dispatchWebhook(session!.user.id, "recipe.updated", { id, title: updated?.title });
|
||||
if (existing.visibility === "private" && data.visibility === "public") {
|
||||
|
||||
Reference in New Issue
Block a user