├── 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 |
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 | | Código |
60 | Nombre |
61 | Cantidad |
62 | Descripción |
63 | Creado |
64 |
65 |
66 |
67 | {productosVisibles.map((p) => (
68 |
69 | | {p.codigo} |
70 | {p.nombre} |
71 | {p.cantidad} |
72 | {p.descripcion} |
73 |
74 | {new Date(p.creacion).toLocaleDateString()}
75 | |
76 |
77 | ))}
78 |
79 |
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 |
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 |
147 |
148 | );
149 | };
150 |
151 | export default FormularioProducto;
152 |
--------------------------------------------------------------------------------