5.7 KiB
Admin Image Redesign — Separate Upload, Resize & Attach
Problem
Current admin image flow bundles three concerns into one POST /api/admin/uploads call:
- Upload file to disk
- Eager resize (generate all
.cachesizes + convert original to WebP) - Register in gallery (upsert GalleryImage)
This prevents the admin from uploading raw images and deciding later when to process them. Photos attached to products are always "ready", but the admin has no control over when processing happens.
Goal
Separate the concerns into three explicit steps:
- Upload — file lands in gallery, no processing
- Resize — admin triggers image processing per image
- Attach — only processed images can be attached to products / slider
Prisma Schema Change
Add isResized field to GalleryImage:
model GalleryImage {
id String @id @default(cuid())
url String @unique
isResized Boolean @default(false)
createdAt DateTime @default(now())
catalogSliderSlides CatalogSliderSlide[]
}
Existing data: after deploy, run a one-time migration script:
await prisma.galleryImage.updateMany({
where: { isResized: false },
data: { isResized: true },
})
Existing images are already on disk in their processed state (WebP + .cache), so marking them isResized = true is correct.
API Routes
New: POST /api/admin/gallery/upload
- Multipart file upload
- Saves to
/uploads/<uuid>.<ext>(original extension preserved, NO WebP conversion) - Creates
GalleryImage { url, isResized: false } - Returns
{ url: string }
New: POST /api/admin/gallery/:id/resize
- Reads original from
/uploads/<uuid>.<ext> - Calls
convertOriginalToWebp(converts to/uploads/<uuid>.webp, deletes original) - Calls
generateAllSizes(populates.cache/) - Updates
GalleryImage.urlto/uploads/<uuid>.webp, setsisResized = true - Returns
{ url: string } - Errors if already resized (409) or image not found (404)
Modified: GET /api/admin/gallery
- Already returns all fields via Prisma — just add
isResizedto the response - No endpoint changes needed; client type updates only
Modified: POST /api/admin/uploads → REMOVED
- The old combined upload endpoint is deleted
- It was only used by admin product form
Modified: POST /api/admin/products / PATCH /api/admin/products/:id
- Validate that all passed
imageUrlshaveisResized = truein GalleryImage - If any image is not resized →
400 Bad Requestwith explanation
No changes to:
DELETE /api/admin/gallery/:id(already works correctly)PUT /api/admin/catalog-slider(slider picks from GalleryImage — handled by gallery endpoint filter)GET /uploads-resized/(on-demand resizer unchanged)- All public routes
Admin UI — Gallery Page
Upload: Stays as <input type="file" multiple> → calls new POST /api/admin/gallery/upload.
Gallery card: Each image now shows:
- OptimizedImage preview (on-demand resizer still works for display)
- Status badge: "Не обработано" (if
!isResized) or "Готово" (ifisResized) - If
!isResized: a "Resize" button visible - If
isResized: "Resize" hidden, delete button remains - Existing delete behaviour unchanged (checks usage before deletion)
GalleryGrid: Updated to accept and render isResized property, conditionally show resize button.
React Query: Add resizeGalleryImage mutation + uploadGalleryImages mutation.
Admin UI — Product Form
- Remove direct file upload from
AdminProductsPage(the<input>that callsuploadAdminProductImages) - Keep only "Выбрать из галереи" dialog
- Gallery selection dialog: filter to show only
isResized = trueimages - Existing preview/sort/delete within product card unchanged
Admin UI — Slider Section
GallerySliderSectionalready uses gallery for selection- When picking an image for a slide, filter to
isResized = true
Data Flow Summary
1. Upload
[Admin] → POST /api/admin/gallery/upload → /uploads/<uuid>.png
→ GalleryImage { url: "/uploads/<uuid>.png", isResized: false }
2. Resize (triggered manually in gallery)
[Admin] → POST /api/admin/gallery/:id/resize
→ convertOriginalToWebp → /uploads/<uuid>.webp
→ generateAllSizes → .cache/<uuid>_w{320,640,1024,1600}.{avif,webp}
→ GalleryImage { url: "/uploads/<uuid>.webp", isResized: true }
3. Attach to product / slider
[Admin] → product form / slider form
→ gallery picker shows only isResized = true images
→ write chosen URLs to ProductImage / CatalogSliderSlide
Error Handling
- Resize of already-resized image → 409 Conflict
- Resize of missing file → 404 Not Found
- Attach unprocessed image to product → 400 Bad Request with message
- Upload invalid file type → 400 (existing validation reused)
- Upload over size limit → 413 (existing validation reused)
Testing
Server
- Upload endpoint: file saved, no processing, GalleryImage created with
isResized: false - Resize endpoint: original converted to WebP, .cache populated,
isResizedflipped totrue - Product creation: rejects imageUrls with
isResized: false - Gallery GET: includes
isResizedfield
Client
- Gallery page: badge visible for unprocessed, hidden for processed
- Resize button click → mutation → refetch → updated state
- Product form: no upload button, only "from gallery" picker
- Gallery picker in product/slider: unprocessed images hidden or disabled
Rollout
- Deploy server changes first (schema migration + new routes, remove old upload route)
- One-time migration to mark existing images as resized
- Deploy client changes