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'
|
import type { ImageItem } from '@/types/images'
|
||||||
|
|
||||||
function ImagesPageInner() {
|
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 [lightboxItem, setLightboxItem] = useState<ImageItem | null>(null)
|
||||||
const [mockupItem, setMockupItem] = 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 openMockup = useCallback((id: string) => {
|
||||||
const item = items.find(i => i.id === id)
|
const item = items.find(i => i.id === id)
|
||||||
if (item) setMockupItem(item)
|
if (item) setMockupItem(item)
|
||||||
}, [items])
|
}, [items])
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
||||||
|
|
||||||
const toggleSelect = useCallback((id: string, value: boolean) => {
|
const toggleSelect = useCallback((id: string, value: boolean) => {
|
||||||
setSelected(prev => {
|
setSelected(prev => {
|
||||||
@@ -30,23 +31,51 @@ function ImagesPageInner() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const selectAll = useCallback(() => {
|
const clearSelection = useCallback(() => setSelected(new Set()), [])
|
||||||
const detourable = items.filter(i =>
|
|
||||||
i.status === 'converted' || i.status === 'not_found' || i.status === 'error'
|
const selectAllDetourable = useCallback(() => {
|
||||||
)
|
const ids = items
|
||||||
setSelected(new Set(detourable.map(i => i.id) as string[]))
|
.filter(i => i.status === 'converted' || i.status === 'not_found' || i.status === 'error')
|
||||||
|
.map(i => i.id) as string[]
|
||||||
|
setSelected(new Set(ids))
|
||||||
}, [items])
|
}, [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(() => {
|
const detourSelected = useCallback(() => {
|
||||||
Array.from(selected).forEach(id => detour(id))
|
Array.from(selected).forEach(id => detour(id))
|
||||||
setSelected(new Set())
|
setSelected(new Set())
|
||||||
}, [selected, detour])
|
}, [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 =>
|
const detourable = items.filter(i =>
|
||||||
i.status === 'converted' || i.status === 'not_found' || i.status === 'error'
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -66,26 +95,57 @@ function ImagesPageInner() {
|
|||||||
<GlobalActions items={items} onUploadAll={uploadAll} />
|
<GlobalActions items={items} onUploadAll={uploadAll} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Barre de sélection multiple pour détourage par lot */}
|
{/* Barre de sélection multiple */}
|
||||||
{detourable.length > 0 && (
|
{showSelectionBar && (
|
||||||
<div className="flex items-center gap-3 px-3 py-2 bg-slate-800 rounded-lg border border-slate-700">
|
<div className="flex items-center gap-2 px-3 py-2 bg-slate-800 rounded-lg border border-slate-700 flex-wrap">
|
||||||
<span className="text-xs text-slate-400">{detourable.length} image{detourable.length > 1 ? 's' : ''} sans détourage</span>
|
{/* Compteurs et raccourcis de sélection */}
|
||||||
<button onClick={selectAll} className="text-xs text-indigo-400 hover:text-indigo-300">
|
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||||
Tout sélectionner
|
{detourable.length > 0 && (
|
||||||
</button>
|
<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 && (
|
{selected.size > 0 && (
|
||||||
<>
|
<>
|
||||||
<span className="text-xs text-slate-500">·</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>
|
<span className="text-xs text-white font-medium">
|
||||||
<button
|
{selected.size} sélectionnée{selected.size > 1 ? 's' : ''}
|
||||||
onClick={detourSelected}
|
</span>
|
||||||
className="ml-auto px-3 py-1 text-xs bg-purple-700 hover:bg-purple-600 text-white font-semibold rounded"
|
<div className="ml-auto flex items-center gap-2">
|
||||||
>
|
{selectedDetourable.length > 0 && (
|
||||||
✂️ Détourer la sélection
|
<button
|
||||||
</button>
|
onClick={detourSelected}
|
||||||
<button onClick={clearSelection} className="text-xs text-slate-500 hover:text-slate-300">
|
className="px-3 py-1 text-xs bg-purple-700 hover:bg-purple-600 text-white font-semibold rounded"
|
||||||
✕
|
>
|
||||||
</button>
|
✂️ 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>
|
</div>
|
||||||
|
|||||||
+11
-1
@@ -353,6 +353,16 @@ export function useImages() {
|
|||||||
}
|
}
|
||||||
}, [items, uploadOne])
|
}, [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) => {
|
const removeItem = useCallback((id: string) => {
|
||||||
dispatch({ type: 'REMOVE', id })
|
dispatch({ type: 'REMOVE', id })
|
||||||
}, [])
|
}, [])
|
||||||
@@ -366,5 +376,5 @@ export function useImages() {
|
|||||||
[items, update],
|
[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