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