From b9c809e648e105bfd6ea8247ed195760cf96b8c0 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 8 Jan 2026 17:14:29 -0600 Subject: [PATCH] bugs arreglados de FileDropZone --- bun.lock | 9 + eslint.config.js | 7 +- package.json | 1 + .../wizard/PasoDetallesPanel/FileDropZone.tsx | 92 ++--- .../PasoDetallesPanel/PasoDetallesPanel.tsx | 111 ++++-- .../PasoDetallesPanel/ReferenciasParaIA.tsx | 24 +- .../planes/wizard/PasoResumenCard.tsx | 328 +++++++++++------- src/features/planes/nuevo/types.ts | 23 +- 8 files changed, 402 insertions(+), 193 deletions(-) diff --git a/bun.lock b/bun.lock index a195a69..60f0e3b 100644 --- a/bun.lock +++ b/bun.lock @@ -46,6 +46,7 @@ "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-unused-imports": "^4.3.0", "jsdom": "^27.0.0", "prettier": "^3.5.3", @@ -770,6 +771,8 @@ "eslint-plugin-n": ["eslint-plugin-n@17.23.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.5.0", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", "globals": "^15.11.0", "globrex": "^0.1.2", "ignore": "^5.3.2", "semver": "^7.6.3", "ts-declaration-location": "^1.0.6" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-68PealUpYoHOBh332JLLD9Sj7OQUDkFpmcfqt8R9sySfFSeuGJjMTJQvCRRB96zO3A/PELRLkPrzsHmzEFQQ5A=="], + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], + "eslint-plugin-unused-imports": ["eslint-plugin-unused-imports@4.3.0", "", { "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin"] }, "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], @@ -864,6 +867,10 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -1318,6 +1325,8 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], diff --git a/eslint.config.js b/eslint.config.js index 386af91..9bd9ca1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,7 @@ import { tanstackConfig } from '@tanstack/eslint-config' import eslintConfigPrettier from 'eslint-config-prettier' import jsxA11y from 'eslint-plugin-jsx-a11y' +import reactHooks from 'eslint-plugin-react-hooks' import unusedImports from 'eslint-plugin-unused-imports' export default [ @@ -24,9 +25,12 @@ export default [ // 3. TUS REGLAS Y CONFIGURACIÓN "PRO" { + // Opcional: Puedes ser explícito sobre dónde aplicar esto + files: ['**/*.{ts,tsx,js,jsx}'], plugins: { 'jsx-a11y': jsxA11y, 'unused-imports': unusedImports, + 'react-hooks': reactHooks, }, // Configuración robusta del Resolver (La versión de Copilot) settings: { @@ -44,7 +48,8 @@ export default [ // --- REGLAS DE ACCESIBILIDAD (A11Y) --- // Activamos las recomendadas manualmente ...jsxA11y.configs.recommended.rules, - + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', // --- ORDEN DE IMPORTS --- 'sort-imports': 'off', // Apagamos el nativo 'import/order': [ diff --git a/package.json b/package.json index e70e08b..a39fd45 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-unused-imports": "^4.3.0", "jsdom": "^27.0.0", "prettier": "^3.5.3", diff --git a/src/components/planes/wizard/PasoDetallesPanel/FileDropZone.tsx b/src/components/planes/wizard/PasoDetallesPanel/FileDropZone.tsx index 119163b..65b9d83 100644 --- a/src/components/planes/wizard/PasoDetallesPanel/FileDropZone.tsx +++ b/src/components/planes/wizard/PasoDetallesPanel/FileDropZone.tsx @@ -1,5 +1,5 @@ import { Upload, File, X, FileText } from 'lucide-react' -import { useState, useCallback } from 'react' +import { useState, useCallback, useEffect, useRef } from 'react' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' @@ -28,33 +28,7 @@ export function FileDropzone({ }: FileDropzoneProps) { const [isDragging, setIsDragging] = useState(false) const [files, setFiles] = useState>([]) - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault() - setIsDragging(true) - }, []) - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault() - setIsDragging(false) - }, []) - - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault() - setIsDragging(false) - const droppedFiles = Array.from(e.dataTransfer.files) - addFiles(droppedFiles) - }, []) - - const handleFileInput = useCallback( - (e: React.ChangeEvent) => { - if (e.target.files) { - const selectedFiles = Array.from(e.target.files) - addFiles(selectedFiles) - } - }, - [], - ) + const onFilesChangeRef = useRef(onFilesChange) const addFiles = useCallback( (newFiles: Array) => { @@ -70,24 +44,59 @@ export function FileDropzone({ setFiles((prev) => { const room = Math.max(0, maxFiles - prev.length) const next = [...prev, ...toUpload.slice(0, room)].slice(0, maxFiles) - if (onFilesChange) onFilesChange(next) return next }) }, - [maxFiles, onFilesChange], + [maxFiles], ) - const removeFile = useCallback( - (fileId: string) => { - setFiles((prev) => { - const next = prev.filter((f) => f.id !== fileId) - if (onFilesChange) onFilesChange(next) - return next - }) + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + setIsDragging(true) + }, []) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + }, []) + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + const droppedFiles = Array.from(e.dataTransfer.files) + addFiles(droppedFiles) }, - [onFilesChange], + [addFiles], ) + const handleFileInput = useCallback( + (e: React.ChangeEvent) => { + if (e.target.files) { + const selectedFiles = Array.from(e.target.files) + addFiles(selectedFiles) + } + }, + [addFiles], + ) + + const removeFile = useCallback((fileId: string) => { + setFiles((prev) => { + const next = prev.filter((f) => f.id !== fileId) + return next + }) + }, []) + + // Keep latest callback in a ref to avoid retriggering effect on identity change + useEffect(() => { + onFilesChangeRef.current = onFilesChange + }, [onFilesChange]) + + // Only emit when files actually change to avoid parent update loops + useEffect(() => { + if (onFilesChangeRef.current) onFilesChangeRef.current(files) + }, [files]) + const formatFileSize = (bytes: number): string => { if (bytes < 1024) return bytes + ' B' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB' @@ -116,7 +125,6 @@ export function FileDropzone({ 'border-border hover:border-primary/50 cursor-pointer rounded-xl border-2 border-dashed p-8 text-center transition-all duration-300', isDragging && 'active', )} - style={{ background: 'var(--gradient-subtle)' }} > -