feat: add multi-select upload and unified selection bar on images page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 12:03:20 +02:00
parent 3ec0b9fa3c
commit 99de0da823
2 changed files with 97 additions and 27 deletions
+77 -17
View File
@@ -11,15 +11,16 @@ import { useImages } from '@/hooks/useImages'
import type { ImageItem } from '@/types/images'
function ImagesPageInner() {
const { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, removeItem, assignProduct, reprocess, detour } = useImages()
const { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, uploadSelected, removeItem, assignProduct, reprocess, detour } = useImages()
const [lightboxItem, setLightboxItem] = useState<ImageItem | null>(null)
const [mockupItem, setMockupItem] = useState<ImageItem | null>(null)
const [selected, setSelected] = useState<Set<string>>(new Set())
const [uploadingSelected, setUploadingSelected] = useState(false)
const openMockup = useCallback((id: string) => {
const item = items.find(i => i.id === id)
if (item) setMockupItem(item)
}, [items])
const [selected, setSelected] = useState<Set<string>>(new Set())
const toggleSelect = useCallback((id: string, value: boolean) => {
setSelected(prev => {
@@ -30,23 +31,51 @@ function ImagesPageInner() {
})
}, [])
const selectAll = useCallback(() => {
const detourable = items.filter(i =>
i.status === 'converted' || i.status === 'not_found' || i.status === 'error'
)
setSelected(new Set(detourable.map(i => i.id) as string[]))
const clearSelection = useCallback(() => setSelected(new Set()), [])
const selectAllDetourable = useCallback(() => {
const ids = items
.filter(i => i.status === 'converted' || i.status === 'not_found' || i.status === 'error')
.map(i => i.id) as string[]
setSelected(new Set(ids))
}, [items])
const clearSelection = useCallback(() => setSelected(new Set()), [])
const selectAllUploadable = useCallback(() => {
const ids = items
.filter(i => (i.status === 'ready' || i.status === 'converted') && !!i.shopifyProductId)
.map(i => i.id) as string[]
setSelected(new Set(ids))
}, [items])
const detourSelected = useCallback(() => {
Array.from(selected).forEach(id => detour(id))
setSelected(new Set())
}, [selected, detour])
const handleUploadSelected = useCallback(async () => {
setUploadingSelected(true)
await uploadSelected(selected)
setUploadingSelected(false)
setSelected(new Set())
}, [selected, uploadSelected])
const selectedDetourable = Array.from(selected).filter(id => {
const item = items.find(i => i.id === id)
return item && (item.status === 'converted' || item.status === 'not_found' || item.status === 'error')
})
const selectedUploadable = Array.from(selected).filter(id => {
const item = items.find(i => i.id === id)
return item && (item.status === 'ready' || item.status === 'converted') && !!item.shopifyProductId
})
const detourable = items.filter(i =>
i.status === 'converted' || i.status === 'not_found' || i.status === 'error'
)
const uploadable = items.filter(i =>
(i.status === 'ready' || i.status === 'converted') && !!i.shopifyProductId
)
const showSelectionBar = detourable.length > 0 || uploadable.length > 0
return (
<>
@@ -66,26 +95,57 @@ function ImagesPageInner() {
<GlobalActions items={items} onUploadAll={uploadAll} />
)}
{/* Barre de sélection multiple pour détourage par lot */}
{/* Barre de sélection multiple */}
{showSelectionBar && (
<div className="flex items-center gap-2 px-3 py-2 bg-slate-800 rounded-lg border border-slate-700 flex-wrap">
{/* Compteurs et raccourcis de sélection */}
<div className="flex items-center gap-2 text-xs text-slate-400">
{detourable.length > 0 && (
<div className="flex items-center gap-3 px-3 py-2 bg-slate-800 rounded-lg border border-slate-700">
<span className="text-xs text-slate-400">{detourable.length} image{detourable.length > 1 ? 's' : ''} sans détourage</span>
<button onClick={selectAll} className="text-xs text-indigo-400 hover:text-indigo-300">
Tout sélectionner
<button onClick={selectAllDetourable} className="hover:text-purple-300 transition-colors">
Sélectionner à détourer ({detourable.length})
</button>
)}
{detourable.length > 0 && uploadable.length > 0 && (
<span className="text-slate-600">·</span>
)}
{uploadable.length > 0 && (
<button onClick={selectAllUploadable} className="hover:text-indigo-300 transition-colors">
Sélectionner à uploader ({uploadable.length})
</button>
)}
</div>
{/* Actions sur la sélection */}
{selected.size > 0 && (
<>
<span className="text-xs text-slate-500">·</span>
<span className="text-xs text-white font-medium">{selected.size} sélectionnée{selected.size > 1 ? 's' : ''}</span>
<span className="text-slate-600 text-xs">|</span>
<span className="text-xs text-white font-medium">
{selected.size} sélectionnée{selected.size > 1 ? 's' : ''}
</span>
<div className="ml-auto flex items-center gap-2">
{selectedDetourable.length > 0 && (
<button
onClick={detourSelected}
className="ml-auto px-3 py-1 text-xs bg-purple-700 hover:bg-purple-600 text-white font-semibold rounded"
className="px-3 py-1 text-xs bg-purple-700 hover:bg-purple-600 text-white font-semibold rounded"
>
Détourer la sélection
Détourer ({selectedDetourable.length})
</button>
)}
{selectedUploadable.length > 0 && (
<button
onClick={handleUploadSelected}
disabled={uploadingSelected}
className="px-3 py-1 text-xs bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded disabled:opacity-50"
>
{uploadingSelected
? `⬆️ Upload en cours…`
: `⬆️ Uploader (${selectedUploadable.length})`}
</button>
)}
<button onClick={clearSelection} className="text-xs text-slate-500 hover:text-slate-300">
</button>
</div>
</>
)}
</div>
+11 -1
View File
@@ -353,6 +353,16 @@ export function useImages() {
}
}, [items, uploadOne])
const uploadSelected = useCallback(async (ids: Set<string>) => {
const toUpload = items.filter(i =>
ids.has(i.id) && (i.status === 'ready' || i.status === 'converted') && !!i.shopifyProductId
)
for (const item of toUpload) {
await uploadOne(item.id)
await new Promise(r => setTimeout(r, 500))
}
}, [items, uploadOne])
const removeItem = useCallback((id: string) => {
dispatch({ type: 'REMOVE', id })
}, [])
@@ -366,5 +376,5 @@ export function useImages() {
[items, update],
)
return { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, removeItem, assignProduct, reprocess, detour }
return { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, uploadSelected, removeItem, assignProduct, reprocess, detour }
}