From b41b8329bcd58d0779d425e5f2f17dbef9f591df Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 20 Apr 2026 22:49:34 +0200 Subject: [PATCH 1/2] fix(admin): return presignedUrl from from-url endpoint and update image preview after fetch - images.ts: import getImageUrl from storage service, call after fetchImageFromUrl and include presignedUrl in response - $itemId.tsx: update handleFetchFromUrl to use presignedUrl and dominantColor from response, set imageUrl in form state so ImageUpload component shows preview immediately --- src/client/routes/admin/items/$itemId.tsx | 12 ++++++++---- src/server/routes/images.ts | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/client/routes/admin/items/$itemId.tsx b/src/client/routes/admin/items/$itemId.tsx index b42d586..078b260 100644 --- a/src/client/routes/admin/items/$itemId.tsx +++ b/src/client/routes/admin/items/$itemId.tsx @@ -161,13 +161,17 @@ function AdminItemEditPage() { setFetchingImage(true); setFetchError(null); try { - const result = await apiPost<{ filename: string; sourceUrl: string }>( - "/api/images/from-url", - { url: fetchUrl.trim() }, - ); + const result = await apiPost<{ + filename: string; + sourceUrl: string; + presignedUrl: string; + dominantColor: string | null; + }>("/api/images/from-url", { url: fetchUrl.trim() }); setForm((prev) => ({ ...prev, imageFilename: result.filename, + imageUrl: result.presignedUrl, + dominantColor: result.dominantColor ?? "", imageSourceUrl: fetchUrl.trim(), })); setFetchUrl(""); diff --git a/src/server/routes/images.ts b/src/server/routes/images.ts index 7644ac2..bb50bb0 100644 --- a/src/server/routes/images.ts +++ b/src/server/routes/images.ts @@ -6,7 +6,7 @@ import { extractDominantColor, fetchImageFromUrl, } from "../services/image.service"; -import { uploadImage } from "../services/storage.service"; +import { getImageUrl, uploadImage } from "../services/storage.service"; const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]; const MAX_SIZE = 5 * 1024 * 1024; // 5MB @@ -19,7 +19,8 @@ app.post("/from-url", zValidator("json", fromUrlSchema), async (c) => { const { url } = c.req.valid("json"); try { const result = await fetchImageFromUrl(url); - return c.json(result, 201); + const presignedUrl = await getImageUrl(result.filename); + return c.json({ ...result, presignedUrl }, 201); } catch (err) { const message = (err as Error).message; // Known validation errors from the service From 113e68993209213c11402bca2bd644e63448a9a0 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 20 Apr 2026 22:50:35 +0200 Subject: [PATCH 2/2] fix(admin): handle duplicate tag name with 409 + polish inline tag form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin-tags.ts: wrap createTag in try/catch, detect UNIQUE constraint violations and return 409 with friendly message - tags/index.tsx: surface server error message in catch block via err.message (ApiError carries the message from response body) - tags/index.tsx: replace bare form row with card-style wrapper — label for Name and Parent, card border/bg, shrink-0 submit button --- src/client/routes/admin/tags/index.tsx | 83 +++++++++++++++----------- src/server/routes/admin-tags.ts | 18 +++++- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/client/routes/admin/tags/index.tsx b/src/client/routes/admin/tags/index.tsx index e70ee1a..ffd3d08 100644 --- a/src/client/routes/admin/tags/index.tsx +++ b/src/client/routes/admin/tags/index.tsx @@ -124,8 +124,10 @@ function AdminTagsPage() { }); setNewName(""); setNewParentId(null); - } catch { - setCreateError("Failed to create tag. Please try again."); + } catch (err: unknown) { + const message = + err instanceof Error ? err.message : "Failed to create tag"; + setCreateError(message); } } @@ -156,39 +158,50 @@ function AdminTagsPage() { {/* Quick-add form */} -
- setNewName(e.target.value)} - placeholder="Tag name..." - className={`flex-1 ${inputClass}`} - /> - - -
- {createError && ( -

{createError}

- )} +
+

+ Add Tag +

+
+
+ + setNewName(e.target.value)} + placeholder="e.g. Bikepacking" + className={inputClass} + /> +
+
+ + +
+ +
+ {createError && ( +

{createError}

+ )} +
{/* Error state */} {isError && ( diff --git a/src/server/routes/admin-tags.ts b/src/server/routes/admin-tags.ts index 4683df4..8c466b3 100644 --- a/src/server/routes/admin-tags.ts +++ b/src/server/routes/admin-tags.ts @@ -45,8 +45,22 @@ app.get("/:id", async (c) => { app.post("/", zValidator("json", createTagSchema), async (c) => { const db = c.get("db"); const data = c.req.valid("json"); - const tag = await createTag(db, data); - return c.json(tag, 201); + try { + const tag = await createTag(db, data); + return c.json(tag, 201); + } catch (err) { + if ( + err instanceof Error && + (err.message.includes("UNIQUE constraint failed") || + err.message.includes("unique constraint")) + ) { + return c.json( + { error: `A tag named "${data.name}" already exists` }, + 409, + ); + } + throw err; + } }); // PUT /api/admin/tags/:id — rename and/or reparent a tag