Files
shop-server/docs/superpowers/specs/2026-05-17-admin-image-redesign.md
T
2026-05-18 11:45:51 +05:00

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:

  1. Upload file to disk
  2. Eager resize (generate all .cache sizes + convert original to WebP)
  3. 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:

  1. Upload — file lands in gallery, no processing
  2. Resize — admin triggers image processing per image
  3. 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.url to /uploads/<uuid>.webp, sets isResized = 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 isResized to the response
  • No endpoint changes needed; client type updates only

Modified: POST /api/admin/uploadsREMOVED

  • 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 imageUrls have isResized = true in GalleryImage
  • If any image is not resized → 400 Bad Request with 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

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 "Готово" (if isResized)
  • 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 calls uploadAdminProductImages)
  • Keep only "Выбрать из галереи" dialog
  • Gallery selection dialog: filter to show only isResized = true images
  • Existing preview/sort/delete within product card unchanged

Admin UI — Slider Section

  • GallerySliderSection already 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, isResized flipped to true
  • Product creation: rejects imageUrls with isResized: false
  • Gallery GET: includes isResized field

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

  1. Deploy server changes first (schema migration + new routes, remove old upload route)
  2. One-time migration to mark existing images as resized
  3. Deploy client changes