fix: complete image pipeline - rotation, URL, preview

- Add rotation_degrees parameter to ImageProcessor.process_photo()
- Pass rotation through _auto_save_photo_from_extraction() to processor
- Allow no-crop fallback when crop_bounds is None
- Add buildPhotoUrl() helper to resolve backend URLs correctly
- Update frontend components to use backend URL for image sources
- Replace Use/Skip Photo buttons with checkbox in AI extraction UI
- Add images/ to .gitignore to prevent accidental commits

Addresses: rotation never applied, image 404s (relative to Next.js not backend), preview blank in edit form
This commit is contained in:
2026-04-22 08:50:25 +03:00
parent 1c13ebd76f
commit 64d177e791
10 changed files with 79 additions and 70 deletions

View File

@@ -237,23 +237,15 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div>
<div className="p-3 space-y-2">
<p className="text-xs text-muted font-normal">Extracted Photo</p>
<div className="flex gap-2">
<button
onClick={() => updateEditingItem({ _skipPhoto: false })}
className="flex-1 px-3 py-2 bg-green-500/20 border border-green-500/50 text-green-400 rounded-lg hover:bg-green-500/30 transition-colors font-normal text-xs"
>
Use Photo
</button>
<button
onClick={() => updateEditingItem({ _skipPhoto: true })}
className="flex-1 px-3 py-2 bg-rose-500/20 border border-rose-500/50 text-rose-400 rounded-lg hover:bg-rose-500/30 transition-colors font-normal text-xs"
>
Skip Photo
</button>
</div>
{extractedItems[editingIndex]._skipPhoto && (
<p className="text-xs text-rose-400/80">Photo will not be saved</p>
)}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={!extractedItems[editingIndex]._skipPhoto}
onChange={(e) => updateEditingItem({ _skipPhoto: !e.target.checked })}
className="w-4 h-4 accent-primary cursor-pointer"
/>
<span className="text-xs text-secondary font-normal">Save this photo with the item</span>
</label>
</div>
</div>
)}

View File

@@ -2,6 +2,7 @@
import { useState } from 'react';
import { Item } from '@/lib/db';
import { buildPhotoUrl } from '@/lib/api';
import { ChevronRight, ChevronDown, Layers, Package } from 'lucide-react';
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
@@ -20,6 +21,7 @@ interface InventoryTableProps {
onItemClick: (item: Item) => void;
onEditCategory?: (category: string) => void;
categoriesList?: any[];
backendUrl?: string;
}
export default function InventoryTable({
@@ -29,7 +31,8 @@ export default function InventoryTable({
onExpandCategory,
onItemClick,
onEditCategory,
categoriesList = []
categoriesList = [],
backendUrl = ''
}: InventoryTableProps) {
const [selectedItemDetail, setSelectedItemDetail] = useState<Item | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0);
@@ -115,7 +118,7 @@ export default function InventoryTable({
className="w-12 h-12 rounded-xl shrink-0 border-2 border-slate-300 overflow-hidden cursor-pointer hover:border-primary transition-colors active:scale-95"
>
<img
src={item.photo_path || item.image_url || ''}
src={buildPhotoUrl(backendUrl, item.photo_path || item.image_url || '')}
alt={item.name}
className="w-full h-full object-cover"
loading="lazy"
@@ -160,6 +163,7 @@ export default function InventoryTable({
item={selectedItemDetail}
onClose={handleCloseDetail}
onItemRefresh={handleItemRefresh}
backendUrl={backendUrl}
/>
)}

View File

@@ -2,7 +2,7 @@
import React, { useState, useRef } from 'react';
import { Item } from '@/lib/db';
import { inventoryApi } from '@/lib/api';
import { inventoryApi, buildPhotoUrl } from '@/lib/api';
import ItemPhotoUpload from '@/components/ItemPhotoUpload';
import { X, Camera, Trash2 } from 'lucide-react';
import { toast } from 'react-hot-toast';
@@ -12,6 +12,7 @@ interface ItemDetailModalProps {
onClose: () => void;
onPhotoUpdated?: (photo: { thumbnail_url: string; full_url: string; uploaded_at: string }) => void;
onItemRefresh?: () => void;
backendUrl?: string;
}
export default function ItemDetailModal({
@@ -19,6 +20,7 @@ export default function ItemDetailModal({
onClose,
onPhotoUpdated,
onItemRefresh,
backendUrl = '',
}: ItemDetailModalProps) {
const [showPhotoUpload, setShowPhotoUpload] = useState(false);
const [currentPhoto, setCurrentPhoto] = useState<{ thumbnail_url: string; full_url: string } | null>(
@@ -119,7 +121,7 @@ export default function ItemDetailModal({
<div className="space-y-3">
<div className="relative bg-slate-900/50 rounded-2xl overflow-hidden border border-slate-800/50 aspect-video max-h-96">
<img
src={currentPhoto.full_url}
src={buildPhotoUrl(backendUrl, currentPhoto.full_url)}
alt={item.name}
className="w-full h-full object-contain"
/>