feat(F-135): completed feature

This commit is contained in:
chattie
2026-08-21 18:48:24 +02:00
parent c028802787
commit 8ef3faad42
9 changed files with 239 additions and 26 deletions

View File

@@ -93,18 +93,54 @@ export function ImagesSection({ productId }: ImagesSectionProps) {
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) uploadFile(file);
if (file) {
if (productId) uploadFile(file);
else queueFile(file);
}
};
// Cola de archivos dropeados antes de tener productId — se suben automáticamente
// cuando productId se setea desde el padre (F-135).
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
const queueFile = (file: File) => {
setPendingFiles(prev => [...prev, file]);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
uploadFile(file);
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
if (files.length === 0) return;
if (productId) {
// Subir inmediatamente (en serie para evitar carreras)
void (async () => {
for (const f of files) {
await uploadFile(f);
}
})();
} else {
// Pendiente: encolar para subir tras crear el producto
setPendingFiles(prev => [...prev, ...files]);
}
};
// Efecto: cuando productId pasa de undefined a definido, subir la cola
const prevProductIdRef = useRef<string | undefined>(productId);
useEffect(() => {
const prev = prevProductIdRef.current;
prevProductIdRef.current = productId;
if (!productId || prev === productId) return;
if (pendingFiles.length === 0) return;
const queue = pendingFiles;
setPendingFiles([]);
void (async () => {
for (const f of queue) {
await uploadFile(f);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [productId]);
const setMain = async (imageId: string) => {
// Reorder: put this image first
await fetch(`/api/products/${productId}/images/reorder`, {
@@ -126,12 +162,13 @@ export function ImagesSection({ productId }: ImagesSectionProps) {
};
if (!productId) {
// Pending state: render UI with disabled actions and a caption.
// Pending state: render UI con drop zone funcional — los archivos dropeados
// se encolan y se suben automáticamente cuando productId se setea (F-135).
return (
<div className="space-y-5">
<div className="flex items-center justify-between">
<h3 className="text-sm font-bold text-gray-900">Imágenes</h3>
<span className="text-xs text-gray-400">Se guardarán al crear el producto</span>
<span className="text-xs text-gray-400">Se subirán al crear el producto</span>
</div>
<div className="flex flex-col gap-3 sm:flex-row opacity-50 pointer-events-none">
<input
@@ -156,10 +193,22 @@ export function ImagesSection({ productId }: ImagesSectionProps) {
📤 Subir imagen
</button>
</div>
<div className="border-2 border-dashed border-gray-200 rounded-xl p-8 text-center">
<p className="text-gray-400 text-sm">
<div
onDragOver={e => { e.preventDefault(); setDragOver(true); }}
onDragLeave={() => setDragOver(false)}
onDrop={handleDrop}
className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
dragOver ? 'border-[#2D6A4F] bg-[#2D6A4F]/5' : 'border-gray-300'
}`}
>
<p className="text-gray-600 text-sm">
🖼 Arrastra imágenes aquí para añadirlas al producto
</p>
{pendingFiles.length > 0 && (
<p className="mt-2 text-xs text-amber-700">
{pendingFiles.length} imagen{pendingFiles.length === 1 ? '' : 'es'} en cola se subirán al crear el producto
</p>
)}
</div>
</div>
);