feat: fruit overview with thumbnails and card layout (story #07)

- Auto-crop thumbnails on image upload (Go stdlib, no external deps):
  find non-background bounding box, pad 5px, scale to ≤300px, encode JPEG
- Add thumbnail_data BYTEA column to fruit_images and publication_fruit_images
- New GET /api/v1/fruits/:id/images/:imageId/thumbnail endpoint (fallback to full data)
- New GET /api/v1/publications/:id/fruit-images/:imgId/thumbnail endpoint
- New POST /api/v1/admin/backfill-thumbnails to retroactively generate thumbnails
- ListFruits response now includes synonyms and thumbnail_url per fruit
- Replace FruitList table with responsive FruitCard grid (thumbnail + name + OSDB ID + type + synonyms)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 09:02:14 +02:00
co-authored by Claude Sonnet 4.6
parent 2525e8b68d
commit 649c9687ac
17 changed files with 591 additions and 48 deletions
+18
View File
@@ -29,6 +29,7 @@ type FruitRepository interface {
ListImages(ctx context.Context, fruitID int) ([]domain.FruitImage, error)
AddImage(ctx context.Context, fruitID int, filename *string, data []byte, imageType string, title *string) (domain.FruitImage, error)
GetImageData(ctx context.Context, fruitID, imageID int) ([]byte, error)
GetThumbnailData(ctx context.Context, fruitID, imageID int) ([]byte, error)
DeleteImage(ctx context.Context, fruitID, imageID int) error
}
@@ -291,3 +292,20 @@ func (h *FruitHandler) DeleteImage(c echo.Context) error {
}
return c.NoContent(http.StatusNoContent)
}
func (h *FruitHandler) ServeThumbnail(c echo.Context) error {
fruitID, err := parseID(c, "id")
if err != nil {
return err
}
imageID, err := parseID(c, "imageId")
if err != nil {
return err
}
data, err := h.repo.GetThumbnailData(c.Request().Context(), fruitID, imageID)
if err != nil {
return mapRepoError(c, err)
}
contentType := http.DetectContentType(data)
return c.Blob(http.StatusOK, contentType, data)
}