feat: composant ImageCard avec miniatures avant/après, feather, rotation, upload

This commit is contained in:
2026-06-08 17:44:45 +02:00
parent 4c1cf73435
commit 683db58f4b
+146
View File
@@ -0,0 +1,146 @@
'use client'
import { useEffect, useState } from 'react'
import type { ImageItem } from '@/types/images'
interface ImageCardProps {
item: ImageItem
onFeatherChange: (id: string, value: number) => void
onRotate: (id: string, direction: 'cw' | 'ccw') => void
onUpload: (id: string) => void
onRemove: (id: string) => void
onOpenLightbox: (item: ImageItem) => void
}
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
pending: { label: '⏳ En attente', color: 'text-slate-400' },
converting: { label: '🔄 Conversion HEIC', color: 'text-yellow-400' },
processing: { label: '🤖 Détourage IA', color: 'text-blue-400' },
searching: { label: '🔍 Recherche…', color: 'text-purple-400' },
ready: { label: '✓ Prêt', color: 'text-green-400' },
uploading: { label: '⬆️ Upload…', color: 'text-blue-400' },
uploaded: { label: '✅ Uploadé', color: 'text-green-500' },
not_found: { label: '❌ Réf. introuvable', color: 'text-red-400' },
error: { label: '❌ Erreur', color: 'text-red-400' },
}
function BlobThumbnail({ blob, alt }: { blob: Blob; alt: string }) {
const [url, setUrl] = useState<string | null>(null)
useEffect(() => {
const objectUrl = URL.createObjectURL(blob)
setUrl(objectUrl)
return () => URL.revokeObjectURL(objectUrl)
}, [blob])
if (!url) return <div className="w-full h-full bg-slate-700 animate-pulse" />
return <img src={url} alt={alt} className="w-full h-full object-contain" />
}
export function ImageCard({
item,
onFeatherChange,
onRotate,
onUpload,
onRemove,
onOpenLightbox,
}: ImageCardProps) {
const statusInfo = STATUS_LABELS[item.status] ?? { label: item.status, color: 'text-slate-400' }
const isProcessing = ['pending', 'converting', 'processing', 'searching', 'uploading'].includes(item.status)
const canAdjust = !!item.rawPngBlob && !isProcessing
return (
<div className="bg-slate-900 border border-slate-700 rounded-xl overflow-hidden flex flex-col">
{/* Miniatures avant / après */}
<div
className="relative h-40 bg-slate-800 cursor-pointer"
onClick={() => onOpenLightbox(item)}
>
<div className="absolute inset-0">
<BlobThumbnail blob={item.originalBlob} alt="Original" />
</div>
{item.processedBlob && (
<div className="absolute inset-0 border-l-2 border-indigo-500 left-1/2 w-1/2">
<BlobThumbnail blob={item.processedBlob} alt="Traité" />
</div>
)}
{/* Barre de progression pendant l'IA */}
{item.status === 'processing' && item.progress && (
<div className="absolute bottom-0 inset-x-0 bg-black/60 px-2 py-1">
<div className="h-1 bg-slate-700 rounded">
<div
className="h-1 bg-indigo-500 rounded transition-all"
style={{ width: `${item.progress.percent}%` }}
/>
</div>
<p className="text-xs text-slate-400 mt-0.5">{item.progress.key} {item.progress.percent}%</p>
</div>
)}
</div>
{/* Infos */}
<div className="p-3 flex flex-col gap-2 flex-1">
<div>
<p className="text-xs font-mono text-slate-300 truncate">{item.ref}</p>
{item.shopifyProductTitle && (
<p className="text-xs text-slate-500 truncate">{item.shopifyProductTitle}</p>
)}
<p className={`text-xs font-semibold mt-0.5 ${statusInfo.color}`}>{statusInfo.label}</p>
{item.error && <p className="text-xs text-red-400 mt-0.5 truncate" title={item.error}>{item.error}</p>}
</div>
{/* Contrôles feather + rotation */}
<div className={canAdjust ? '' : 'opacity-40 pointer-events-none'}>
<label className="block text-xs text-slate-500 mb-1">
Bords : {item.feather} px
</label>
<input
type="range"
min={0}
max={5}
step={1}
value={item.feather}
onChange={(e) => onFeatherChange(item.id, Number(e.target.value))}
className="w-full accent-indigo-500"
/>
<div className="flex gap-1 mt-2">
<button
onClick={() => onRotate(item.id, 'ccw')}
className="flex-1 py-1 text-xs bg-slate-800 hover:bg-slate-700 rounded text-slate-300"
> -90°</button>
<button
onClick={() => onRotate(item.id, 'cw')}
className="flex-1 py-1 text-xs bg-slate-800 hover:bg-slate-700 rounded text-slate-300"
> +90°</button>
</div>
</div>
{/* Boutons d'action */}
<div className="flex gap-2 mt-auto pt-1">
{item.status === 'ready' && (
<button
onClick={() => onUpload(item.id)}
className="flex-1 py-1.5 text-xs bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded"
>
Uploader
</button>
)}
{item.status === 'uploaded' && item.uploadedUrl && (
<a
href={item.uploadedUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-1 py-1.5 text-xs bg-green-800 hover:bg-green-700 text-white font-semibold rounded text-center"
>
Voir sur Shopify
</a>
)}
<button
onClick={() => onRemove(item.id)}
className="py-1.5 px-2 text-xs bg-slate-800 hover:bg-red-900 text-slate-400 hover:text-red-300 rounded"
></button>
</div>
</div>
</div>
)
}