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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user