d616f1d74c
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
5.1 KiB
TypeScript
141 lines
5.1 KiB
TypeScript
'use client'
|
||
import { useState, useCallback } from 'react'
|
||
import { Header } from '@/components/layout/Header'
|
||
import { DropZone } from '@/components/images/DropZone'
|
||
import { ImageCard } from '@/components/images/ImageCard'
|
||
import { Lightbox } from '@/components/images/Lightbox'
|
||
import { GlobalActions } from '@/components/images/GlobalActions'
|
||
import { MockupModalFromImage } from '@/components/mockup/MockupModalFromImage'
|
||
import { ErrorBoundary } from '@/components/common/ErrorBoundary'
|
||
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 [lightboxItem, setLightboxItem] = useState<ImageItem | null>(null)
|
||
const [mockupItem, setMockupItem] = useState<ImageItem | null>(null)
|
||
|
||
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 => {
|
||
const next = new Set(prev)
|
||
if (value) next.add(id)
|
||
else next.delete(id)
|
||
return next
|
||
})
|
||
}, [])
|
||
|
||
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[]))
|
||
}, [items])
|
||
|
||
const clearSelection = useCallback(() => setSelected(new Set()), [])
|
||
|
||
const detourSelected = useCallback(() => {
|
||
Array.from(selected).forEach(id => detour(id))
|
||
setSelected(new Set())
|
||
}, [selected, detour])
|
||
|
||
const detourable = items.filter(i =>
|
||
i.status === 'converted' || i.status === 'not_found' || i.status === 'error'
|
||
)
|
||
|
||
return (
|
||
<>
|
||
<Header
|
||
title="🖼️ Images"
|
||
actions={
|
||
<span className="text-xs text-slate-500">
|
||
{items.length > 0 ? `${items.length} image${items.length > 1 ? 's' : ''}` : ''}
|
||
</span>
|
||
}
|
||
/>
|
||
|
||
<div className="p-6 flex flex-col gap-4 flex-1 overflow-y-auto">
|
||
<DropZone onFiles={addFiles} />
|
||
|
||
{items.length > 0 && (
|
||
<GlobalActions items={items} onUploadAll={uploadAll} />
|
||
)}
|
||
|
||
{/* Barre de sélection multiple pour détourage par lot */}
|
||
{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>
|
||
{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>
|
||
<button
|
||
onClick={detourSelected}
|
||
className="ml-auto px-3 py-1 text-xs bg-purple-700 hover:bg-purple-600 text-white font-semibold rounded"
|
||
>
|
||
✂️ Détourer la sélection
|
||
</button>
|
||
<button onClick={clearSelection} className="text-xs text-slate-500 hover:text-slate-300">
|
||
✕
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{items.length > 0 && (
|
||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||
{items.map((item) => (
|
||
<ImageCard
|
||
key={item.id}
|
||
item={item}
|
||
selected={selected.has(item.id)}
|
||
onSelect={toggleSelect}
|
||
onFeatherChange={setFeather}
|
||
onAlphaThresholdChange={setAlphaThreshold}
|
||
onRotate={rotate}
|
||
onUpload={uploadOne}
|
||
onRemove={removeItem}
|
||
onOpenLightbox={setLightboxItem}
|
||
onAssignProduct={assignProduct}
|
||
onReprocess={reprocess}
|
||
onDetour={detour}
|
||
onMockup={openMockup}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{items.length === 0 && (
|
||
<div className="text-center py-12 text-slate-600">
|
||
<p className="text-4xl mb-3">📂</p>
|
||
<p className="text-sm">Déposez des images pour commencer</p>
|
||
<p className="text-xs mt-1">Le nom du fichier (sans extension) doit correspondre à une référence produit Shopify</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<Lightbox item={lightboxItem} onClose={() => setLightboxItem(null)} />
|
||
{mockupItem && (
|
||
<MockupModalFromImage item={mockupItem} onClose={() => setMockupItem(null)} />
|
||
)}
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default function ImagesPage() {
|
||
return (
|
||
<ErrorBoundary>
|
||
<ImagesPageInner />
|
||
</ErrorBoundary>
|
||
)
|
||
}
|