From 99de0da823ccb64dcc026bac41346ddef2e16e34 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Thu, 25 Jun 2026 12:03:20 +0200 Subject: [PATCH] feat: add multi-select upload and unified selection bar on images page Co-Authored-By: Claude Sonnet 4.6 --- src/app/(dashboard)/images/page.tsx | 112 +++++++++++++++++++++------- src/hooks/useImages.ts | 12 ++- 2 files changed, 97 insertions(+), 27 deletions(-) diff --git a/src/app/(dashboard)/images/page.tsx b/src/app/(dashboard)/images/page.tsx index b9760ab..ff5f805 100644 --- a/src/app/(dashboard)/images/page.tsx +++ b/src/app/(dashboard)/images/page.tsx @@ -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(null) const [mockupItem, setMockupItem] = useState(null) + const [selected, setSelected] = useState>(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>(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() { )} - {/* Barre de sélection multiple pour détourage par lot */} - {detourable.length > 0 && ( -
- {detourable.length} image{detourable.length > 1 ? 's' : ''} sans détourage - + {/* Barre de sélection multiple */} + {showSelectionBar && ( +
+ {/* Compteurs et raccourcis de sélection */} +
+ {detourable.length > 0 && ( + + )} + {detourable.length > 0 && uploadable.length > 0 && ( + · + )} + {uploadable.length > 0 && ( + + )} +
+ + {/* Actions sur la sélection */} {selected.size > 0 && ( <> - · - {selected.size} sélectionnée{selected.size > 1 ? 's' : ''} - - + | + + {selected.size} sélectionnée{selected.size > 1 ? 's' : ''} + +
+ {selectedDetourable.length > 0 && ( + + )} + {selectedUploadable.length > 0 && ( + + )} + +
)}
diff --git a/src/hooks/useImages.ts b/src/hooks/useImages.ts index 3087590..f1b7b48 100644 --- a/src/hooks/useImages.ts +++ b/src/hooks/useImages.ts @@ -353,6 +353,16 @@ export function useImages() { } }, [items, uploadOne]) + const uploadSelected = useCallback(async (ids: Set) => { + 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 } }