├── src ├── App.css ├── index.css ├── assets │ ├── pruebaTecnica.doc │ └── react.svg ├── App.jsx ├── main.jsx ├── components │ ├── FiltroProductos.jsx │ ├── OrdenadorProductos.jsx │ ├── ProductoItem.jsx │ ├── ListaProductos.jsx │ └── FormularioProducto.jsx ├── store │ └── useProductStore.js ├── __tests__ │ └── ListaProductos.test.jsx └── pages │ └── Home.jsx ├── .gitignore ├── vite.config.js ├── index.html ├── vitest.config.js ├── eslint.config.js ├── package.json ├── public └── vite.svg └── README.md /src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /src/assets/pruebaTecnica.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohanBoDev/crud-productos-react/HEAD/src/assets/pruebaTecnica.doc -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import Home from '@/pages/Home'; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.jsx' 5 | 6 | createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | import tailwindcss from '@tailwindcss/vite' 4 | import path from 'path'; 5 | 6 | 7 | 8 | // https://vite.dev/config/ 9 | export default defineConfig({ 10 | plugins: [react(), tailwindcss()], 11 | resolve: { 12 | alias: { 13 | '@': path.resolve(__dirname, './src'), 14 | }, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import path from 'path'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | environment: 'jsdom', // ✅ Esto es crucial para que expect funcione bien con Testing Library 7 | globals: true, // ✅ Esto hace que Vitest registre globalmente `expect`, `describe`, `it`, etc. 8 | }, 9 | resolve: { 10 | alias: { 11 | '@': path.resolve(__dirname, './src'), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/FiltroProductos.jsx: -------------------------------------------------------------------------------- 1 | import { useProductStore } from '@/store/useProductStore'; 2 | 3 | 4 | const FiltroProductos = () => { 5 | const filtro = useProductStore((state) => state.filtro); 6 | const setFiltro = useProductStore((state) => state.setFiltro); 7 | 8 | const handleChange = (e) => { 9 | setFiltro(e.target.value); 10 | }; 11 | 12 | return ( 13 |
14 | 21 |
22 | ); 23 | }; 24 | 25 | export default FiltroProductos; -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | 6 | export default [ 7 | { ignores: ['dist'] }, 8 | { 9 | files: ['**/*.{js,jsx}'], 10 | languageOptions: { 11 | ecmaVersion: 2020, 12 | globals: globals.browser, 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | ecmaFeatures: { jsx: true }, 16 | sourceType: 'module', 17 | }, 18 | }, 19 | plugins: { 20 | 'react-hooks': reactHooks, 21 | 'react-refresh': reactRefresh, 22 | }, 23 | rules: { 24 | ...js.configs.recommended.rules, 25 | ...reactHooks.configs.recommended.rules, 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | 'react-refresh/only-export-components': [ 28 | 'warn', 29 | { allowConstantExport: true }, 30 | ], 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crud-productos", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "test": "vitest" 12 | }, 13 | "dependencies": { 14 | "@tailwindcss/vite": "^4.1.4", 15 | "framer-motion": "^12.9.2", 16 | "react": "^19.0.0", 17 | "react-dom": "^19.0.0", 18 | "react-icons": "^5.5.0", 19 | "tailwindcss": "^4.1.4", 20 | "zustand": "^5.0.3" 21 | }, 22 | "devDependencies": { 23 | "@eslint/js": "^9.22.0", 24 | "@testing-library/jest-dom": "^6.6.3", 25 | "@testing-library/react": "^16.3.0", 26 | "@types/react": "^19.0.10", 27 | "@types/react-dom": "^19.0.4", 28 | "@vitejs/plugin-react-swc": "^3.8.0", 29 | "eslint": "^9.22.0", 30 | "eslint-plugin-react-hooks": "^5.2.0", 31 | "eslint-plugin-react-refresh": "^0.4.19", 32 | "globals": "^16.0.0", 33 | "jest": "^29.7.0", 34 | "jsdom": "^26.1.0", 35 | "vite": "^6.3.1", 36 | "vitest": "^3.1.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/OrdenadorProductos.jsx: -------------------------------------------------------------------------------- 1 | import { useProductStore } from '@/store/useProductStore'; 2 | import { useState } from 'react'; 3 | 4 | const OrdenadorProductos = () => { 5 | // Traemos la función de ordenar productos del store 6 | const ordenarProductos = useProductStore((state) => state.ordenarProductos); 7 | 8 | // Estado local para guardar el criterio seleccionado (solo para UI) 9 | const [criterio, setCriterio] = useState(''); 10 | 11 | // Función que se ejecuta cuando el usuario cambia la opción del select 12 | const handleOrdenar = (e) => { 13 | const nuevoCriterio = e.target.value; 14 | setCriterio(nuevoCriterio); 15 | ordenarProductos(nuevoCriterio); 16 | }; 17 | 18 | return ( 19 |
20 | {/* Label para el select */} 21 | 24 | 25 | {/* Select de criterios */} 26 | 38 |
39 | 40 | ); 41 | }; 42 | 43 | export default OrdenadorProductos; 44 | -------------------------------------------------------------------------------- /src/store/useProductStore.js: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { persist } from 'zustand/middleware'; 3 | 4 | // Definimos la tienda Zustand con persistencia automática en localStorage 5 | export const useProductStore = create( 6 | persist( 7 | (set, get) => ({ 8 | productos: [], 9 | 10 | // Crear producto 11 | agregarProducto: (productoNuevo) => { 12 | set((state) => ({ 13 | productos: [...state.productos, productoNuevo], 14 | })); 15 | }, 16 | 17 | // Eliminar producto 18 | eliminarProducto: (codigoProducto) => { 19 | set((state) => ({ 20 | productos: state.productos.filter((producto) => producto.codigo !== codigoProducto), 21 | })); 22 | }, 23 | 24 | // Filtrar productos por nombre (se puede usar directamente en el componente también) 25 | filtrarProductos: (nombre) => { 26 | return get().productos.filter((producto) => 27 | producto.nombre.toLowerCase().includes(nombre.toLowerCase()) 28 | ); 29 | }, 30 | filtro: '', 31 | setFiltro: (texto) => set({ filtro: texto }), 32 | 33 | // Ordenar productos 34 | ordenarProductos: (criterio) => { 35 | const productosOrdenados = [...get().productos].sort((a, b) => { 36 | if (criterio === 'nombre') { 37 | return a.nombre.localeCompare(b.nombre); 38 | } else if (criterio === 'codigo') { 39 | return a.codigo - b.codigo; 40 | } else if (criterio === 'cantidad') { 41 | return a.cantidad - b.cantidad; 42 | } else if (criterio === 'creacion') { 43 | return new Date(a.creacion) - new Date(b.creacion); 44 | } 45 | return 0; 46 | }); 47 | 48 | set({ productos: productosOrdenados }); 49 | }, 50 | }), 51 | { 52 | name: 'productos-storage', // Nombre de la clave en localStorage 53 | } 54 | ) 55 | ); 56 | -------------------------------------------------------------------------------- /src/__tests__/ListaProductos.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom'; 3 | import { render, screen } from '@testing-library/react'; 4 | import ListaProductos from '@/components/ListaProductos'; 5 | import { vi } from 'vitest'; 6 | import { useProductStore } from '@/store/useProductStore'; 7 | 8 | vi.mock('@/store/useProductStore', () => ({ 9 | useProductStore: vi.fn((selector) => 10 | selector({ 11 | productos: [], 12 | filtro: '', 13 | }) 14 | ), 15 | })); 16 | 17 | describe('ListaProductos', () => { 18 | it('debería mostrar el título "Lista de Productos"', () => { 19 | render(); 20 | const titulo = screen.getByText(/Lista de Productos/i); 21 | expect(titulo).toBeInTheDocument(); // ✅ ahora será reconocido 22 | }); 23 | }); 24 | 25 | 26 | it('debería mostrar un producto si está presente en el store', () => { 27 | const productoFalso = { 28 | codigo: 12345, 29 | nombre: 'Laptop Gamer', 30 | descripcion: 'Poderosa y rápida', 31 | cantidad: 5, 32 | creacion: new Date().toISOString(), 33 | }; 34 | 35 | useProductStore.mockImplementation((selector) => 36 | selector({ 37 | productos: [productoFalso], 38 | filtro: '', 39 | }) 40 | ); 41 | 42 | render(); 43 | 44 | expect(screen.getByText(/Laptop Gamer/i)).toBeInTheDocument(); 45 | 46 | expect(screen.getByText((content, element) => 47 | element.textContent === 'Cantidad: 5' 48 | )).toBeInTheDocument(); 49 | }); 50 | 51 | it('no debería mostrar productos que no coincidan con el filtro', () => { 52 | const productoFalso = { 53 | codigo: 9876, 54 | nombre: 'Laptop Gamer', 55 | descripcion: '16GB RAM', 56 | cantidad: 3, 57 | creacion: new Date().toISOString(), 58 | }; 59 | 60 | useProductStore.mockImplementation((selector) => 61 | selector({ 62 | productos: [productoFalso], 63 | filtro: 'monitor', // no coincide con el nombre 64 | }) 65 | ); 66 | 67 | render(); 68 | 69 | // Verifica que el producto NO esté en pantalla 70 | const nombre = screen.queryByText(/Laptop Gamer/i); 71 | expect(nombre).not.toBeInTheDocument(); 72 | }); -------------------------------------------------------------------------------- /src/components/ProductoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useProductStore } from '@/store/useProductStore'; 3 | import { motion } from 'framer-motion'; 4 | 5 | const ProductoItem = ({ producto }) => { 6 | const eliminarProducto = useProductStore((state) => state.eliminarProducto); 7 | 8 | const handleEliminar = () => { 9 | if (confirm(`¿Estás seguro de eliminar el producto "${producto.nombre}"?`)) { 10 | eliminarProducto(producto.codigo); 11 | } 12 | }; 13 | 14 | return ( 15 | 22 | {/* Icono decorativo flotante */} 23 |
24 | 📦 25 |
26 | 27 | {/* Contenido principal */} 28 |
29 |

{producto.nombre}

30 |

31 | Código: {producto.codigo} 32 |

33 |

34 | Cantidad: {producto.cantidad} 35 |

36 | {producto.descripcion && ( 37 |

{producto.descripcion}

38 | )} 39 |
40 | 41 | {/* Fecha de creación */} 42 |
43 | Creado el: {new Date(producto.creacion).toLocaleDateString()} 44 |
45 | 46 | {/* Botón de eliminar */} 47 | 60 |
61 | 62 | ); 63 | }; 64 | 65 | export default ProductoItem; 66 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 🛍️ CRUD de Productos - Prueba Técnica Frontend Junior 3 | 4 | Aplicación construida con **React** para la prueba técnica de desarrollador frontend junior. Permite realizar operaciones CRUD sobre una lista de productos, incluyendo persistencia en localStorage, búsqueda, ordenamiento y tests unitarios. 5 | 6 | --- 7 | 8 | ## 🚀 Demo en Vercel 9 | 10 | > [ https://crud-productos.vercel.app/](https://crud-productos-react.vercel.app/) 11 | 12 | --- 13 | 14 | ## ✨ Funcionalidades 15 | 16 | - ✅ Crear productos con los campos requeridos. 17 | - ✅ Visualizar lista de productos. 18 | - ✅ Eliminar productos. 19 | - ✅ Ordenar por **cantidad**, **creación**, **código** y **nombre**. 20 | - ✅ Filtrar productos por **nombre** con un input de búsqueda. 21 | - ✅ Persistencia local mediante **localStorage**. 22 | - ✅ Vista tipo **tabla** o **tarjeta** con botón para alternar. 23 | - ✅ Paginación (4 productos en vista card, 10 en vista tabla). 24 | - ✅ Interfaz totalmente **responsiva** (móvil y desktop). 25 | - ✅ **Accesibilidad básica** con `aria-label` y `focus`. 26 | - ✅ Animaciones y estilo moderno con **TailwindCSS**. 27 | - ✅ **Tests unitarios** con `Vitest` + `Testing Library`. 28 | 29 | --- 30 | 31 | ## 🧠 Decisiones Técnicas 32 | 33 | ### 🪄 ¿Por qué Zustand? 34 | Elegí [Zustand](https://github.com/pmndrs/zustand) para el manejo de estado por su: 35 | - API simple y directa. 36 | - Menor boilerplate comparado con Context API o Redux. 37 | - Excelente rendimiento para apps pequeñas y escalables. 38 | 39 | ### 🎨 ¿Por qué TailwindCSS? 40 | Usé [TailwindCSS](https://tailwindcss.com/) porque: 41 | - Permite una construcción rápida y flexible de la UI. 42 | - Facilita crear una interfaz profesional con clases utilitarias. 43 | - Compatible con diseño responsivo y animaciones CSS avanzadas. 44 | 45 | ### 💾 ¿Por qué localStorage? 46 | - Se especifica en la prueba que no debe haber backend. 47 | - Es la forma más rápida y limpia de garantizar persistencia sin servidor. 48 | 49 | ### 🧪 ¿Por qué Vitest + Testing Library? 50 | - `Vitest` es ligero, rápido y compatible con proyectos Vite. 51 | - `@testing-library/react` permite simular el uso real de componentes. 52 | - Se añadieron **3 tests** que cubren renderizado, filtro y visibilidad de productos. 53 | 54 | --- 55 | 56 | ## 📂 Estructura del Proyecto 57 | 58 | ``` 59 | src/ 60 | ├── assets/ # Archivos estáticos 61 | ├── components/ # Componentes reutilizables (ej. ListaProductos, ProductoItem) 62 | ├── store/ # Estado global usando Zustand 63 | ├── utils/ # Funciones auxiliares (ordenamiento, fechas, etc.) 64 | ├── __tests__/ # Tests unitarios 65 | ├── App.jsx # Componente principal 66 | ├── main.jsx # Entrada principal 67 | ``` 68 | 69 | --- 70 | 71 | ## ⚙️ Instalación local 72 | 73 | 1. Clona el proyecto: 74 | ```bash 75 | git clone https://github.com/JohanBoDev/crud-productos.git 76 | cd crud-productos 77 | ``` 78 | 79 | 2. Instala dependencias: 80 | ```bash 81 | npm install 82 | ``` 83 | 84 | 3. Corre el proyecto en desarrollo: 85 | ```bash 86 | npm run dev 87 | ``` 88 | 89 | --- 90 | 91 | ## 🧪 Ejecutar tests 92 | 93 | Este proyecto incluye **3 pruebas unitarias** con Zustand mockeado: 94 | 95 | ```bash 96 | npm run test 97 | ``` 98 | 99 | > Tests ubicados en: `src/__tests__/ListaProductos.test.jsx` 100 | 101 | --- 102 | 103 | ## 📝 Autor 104 | 105 | **Johan Bohorquez** 106 | 📫 [bohorquezjohan958@gmail.com](mailto:bohorquezjohan958@gmail.com) 107 | 🔗 [LinkedIn](https://www.linkedin.com/in/johanbodev/) 108 | 💻 [Portafolio Web](https://www.johanbodev.online/) 109 | 🐙 [GitHub](https://github.com/JohanBoDev) 110 | 111 | --- 112 | 113 | ## 📌 Notas Finales 114 | 115 | - Aplicación construida sin backend, como lo exige la prueba. 116 | - Código limpio, modular y listo para ser escalado. 117 | - La estructura permite añadir nuevas funcionalidades sin fricción. 118 | - El proyecto ha sido probado y validado para cumplir con todos los requisitos obligatorios y varios extras deseables de la prueba técnica. 119 | 120 | --- 121 | 122 | ### ✔️ Checklist de requisitos cumplidos 123 | 124 | - [x] Crear producto 125 | - [x] Ver lista de productos 126 | - [x] Eliminar producto 127 | - [x] Filtrar por nombre 128 | - [x] Ordenar por cantidad, creación, código y nombre 129 | - [x] Persistencia con localStorage 130 | - [x] Estado global con Zustand 131 | - [x] Componentes reutilizables 132 | - [x] TailwindCSS 133 | - [x] App responsiva 134 | - [x] Accesibilidad básica 135 | - [x] Tests unitarios 136 | - [x] Buen manejo de carpetas 137 | - [x] Animaciones y mejoras visuales 138 | 139 | --- 140 | 141 | ¡Gracias por revisar este proyecto! 🚀 142 | -------------------------------------------------------------------------------- /src/components/ListaProductos.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useProductStore } from '@/store/useProductStore'; 4 | import ProductoItem from './ProductoItem'; 5 | import { useState } from 'react'; 6 | 7 | const ListaProductos = () => { 8 | const productos = useProductStore((state) => state.productos); 9 | const filtro = useProductStore((state) => state.filtro); 10 | const [paginaActual, setPaginaActual] = useState(1); 11 | const [modoVista, setModoVista] = useState('card'); // 'card' o 'tabla' 12 | 13 | const productosFiltrados = productos.filter((producto) => 14 | producto.nombre.toLowerCase().includes(filtro.toLowerCase()) 15 | ); 16 | 17 | const productosPorPagina = modoVista === 'tabla' ? 10 : 4; 18 | const totalPaginas = Math.ceil(productosFiltrados.length / productosPorPagina); 19 | const indiceInicio = (paginaActual - 1) * productosPorPagina; 20 | const productosVisibles = productosFiltrados.slice(indiceInicio, indiceInicio + productosPorPagina); 21 | 22 | const irPaginaAnterior = () => { 23 | if (paginaActual > 1) setPaginaActual(paginaActual - 1); 24 | }; 25 | 26 | const irPaginaSiguiente = () => { 27 | if (paginaActual < totalPaginas) setPaginaActual(paginaActual + 1); 28 | }; 29 | 30 | const alternarVista = () => { 31 | setModoVista(modoVista === 'card' ? 'tabla' : 'card'); 32 | }; 33 | 34 | return ( 35 |
36 |
37 |

Lista de Productos

38 | 44 |
45 | 46 | {productos.length === 0 ? ( 47 |

No hay productos aún.

48 | ) : modoVista === 'card' ? ( 49 |
    50 | {productosVisibles.map((producto) => ( 51 | 52 | ))} 53 |
54 | ) : ( 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {productosVisibles.map((p) => ( 68 | 69 | 70 | 71 | 72 | 73 | 76 | 77 | ))} 78 | 79 |
CódigoNombreCantidadDescripciónCreado
{p.codigo}{p.nombre}{p.cantidad}{p.descripcion} 74 | {new Date(p.creacion).toLocaleDateString()} 75 |
80 |
81 | )} 82 | 83 | {/* Paginación */} 84 |
85 | 92 | 93 | 94 | Página {paginaActual} de {totalPaginas} 95 | 96 | 97 | 104 |
105 | 106 |
107 | ); 108 | }; 109 | 110 | export default ListaProductos; 111 | -------------------------------------------------------------------------------- /src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import FormularioProducto from '@/components/FormularioProducto'; 2 | import ListaProductos from '@/components/ListaProductos'; 3 | import FiltroProductos from '@/components/FiltroProductos'; 4 | import OrdenadorProductos from '@/components/OrdenadorProductos'; 5 | import { FaGithub, FaLinkedin, FaGlobe, FaEnvelope } from 'react-icons/fa'; 6 | 7 | const Home = () => { 8 | return ( 9 |
10 | 11 | {/* Fondo granulado */} 12 |
13 | 14 | {/* Luz radial sutil */} 15 |
16 | 17 | {/* Contenido principal */} 18 |
19 | {/* Columna izquierda */} 20 |
21 | {/* Filtro + Formulario */} 22 |

23 | Prueba Técnica - Desarrollador Junior React 24 |

25 | 26 | 27 | 86 |
87 | 88 | {/* Columna derecha */} 89 |
90 | {/* Ordenador + Lista de Productos */} 91 | 92 | 93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default Home; 100 | -------------------------------------------------------------------------------- /src/components/FormularioProducto.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useProductStore } from '@/store/useProductStore'; 3 | 4 | const FormularioProducto = () => { 5 | // Estado local para manejar los valores del formulario 6 | const [codigo, setCodigo] = useState(''); 7 | const [nombre, setNombre] = useState(''); 8 | const [descripcion, setDescripcion] = useState(''); 9 | const [cantidad, setCantidad] = useState(''); 10 | 11 | // Usamos nuestra store para poder agregar productos 12 | const agregarProducto = useProductStore((state) => state.agregarProducto); 13 | 14 | // Función que maneja el envío del formulario 15 | const handleSubmit = (e) => { 16 | e.preventDefault(); // Prevenir el comportamiento por defecto (recargar la página) 17 | 18 | // Validaciones mínimas 19 | if (!codigo || !nombre || !cantidad) { 20 | alert('Por favor completa los campos obligatorios.'); 21 | return; 22 | } 23 | 24 | // Crear el objeto producto 25 | const nuevoProducto = { 26 | codigo: Number(codigo), 27 | nombre, 28 | descripcion, 29 | cantidad: Number(cantidad), 30 | creacion: new Date().toISOString(), // Fecha de creación automática 31 | }; 32 | 33 | // Agregar el producto al estado global 34 | agregarProducto(nuevoProducto); 35 | 36 | // Limpiar el formulario después de agregar 37 | setCodigo(''); 38 | setNombre(''); 39 | setDescripcion(''); 40 | setCantidad(''); 41 | }; 42 | 43 | return ( 44 |
48 | {/* Input para el Código */} 49 |
50 | 53 | setCodigo(e.target.value)} 58 | className="p-3 rounded-lg border border-gray-700 bg-black text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-sky-500" 59 | placeholder="Ej: 1001" 60 | required 61 | /> 62 |
63 | 64 | {/* Input para el Nombre */} 65 |
66 | 69 | setNombre(e.target.value)} 74 | className="p-3 rounded-lg border border-gray-700 bg-black text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-sky-500" 75 | placeholder="Ej: Producto A" 76 | required 77 | /> 78 |
79 | 80 | {/* Input para la Descripción */} 81 |
82 | 85 |