├── .gitignore ├── db.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── actions ├── alertaActions.js └── productoActions.js ├── components ├── EditarProducto.js ├── Header.js ├── NuevoProducto.js ├── Producto.js └── Productos.js ├── config └── axios.js ├── index.css ├── index.js ├── reducers ├── alertaReducer.js ├── index.js └── productosReducer.js ├── serviceWorker.js ├── setupTests.js ├── store.js └── types └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "productos": [ 3 | { 4 | "nombre": "Rib Eye", 5 | "precio": "500", 6 | "id": 7 7 | }, 8 | { 9 | "nombre": "Jugo de Limón", 10 | "precio": 100, 11 | "id": 9 12 | }, 13 | { 14 | "nombre": "1", 15 | "precio": 2, 16 | "id": 10 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crudredux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.4.0", 8 | "@testing-library/user-event": "^7.1.2", 9 | "axios": "^0.19.0", 10 | "react": "^16.12.0", 11 | "react-dom": "^16.12.0", 12 | "react-redux": "^7.1.3", 13 | "react-router-dom": "^5.1.2", 14 | "react-scripts": "3.3.0", 15 | "redux": "^4.0.4", 16 | "redux-thunk": "^2.3.0", 17 | "sweetalert2": "^9.5.3" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codigoconjuan/react_reduxcrud/6cdbe316f8a08d11265dfd9925a21372d292275e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React, Redux, React Router DOM 28 | 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codigoconjuan/react_reduxcrud/6cdbe316f8a08d11265dfd9925a21372d292275e/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codigoconjuan/react_reduxcrud/6cdbe316f8a08d11265dfd9925a21372d292275e/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from './components/Header'; 3 | import Productos from './components/Productos'; 4 | import NuevoProducto from './components/NuevoProducto'; 5 | import EditarProducto from './components/EditarProducto'; 6 | 7 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 8 | 9 | // Redux 10 | import { Provider } from 'react-redux'; 11 | import store from './store'; 12 | 13 | function App() { 14 | return ( 15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | ); 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /src/actions/alertaActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | MOSTRAR_ALERTA, 3 | OCULTAR_ALERTA 4 | } from '../types'; 5 | 6 | // Muestra alerta 7 | export function mostrarAlerta(alerta) { 8 | return (dispatch) => { 9 | dispatch( crearAlerta(alerta) ) 10 | } 11 | } 12 | const crearAlerta = alerta => ({ 13 | type: MOSTRAR_ALERTA, 14 | payload: alerta 15 | }) 16 | 17 | // ocultar alerta 18 | export function ocultarAlertaAction() { 19 | return (dispatch) => { 20 | dispatch( ocultarAlerta() ) 21 | } 22 | } 23 | 24 | const ocultarAlerta = () => ({ 25 | type: OCULTAR_ALERTA 26 | }) -------------------------------------------------------------------------------- /src/actions/productoActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | AGREGAR_PRODUCTO, 3 | AGREGAR_PRODUCTO_EXITO, 4 | AGREGAR_PRODUCTO_ERROR, 5 | COMENZAR_DESCARGA_PRODUCTOS, 6 | DESCARGA_PRODUCTOS_EXITO, 7 | DESCARGA_PRODUCTOS_ERROR, 8 | OBTENER_PRODUCTO_ELIMINAR, 9 | PRODUCTO_ELIMINADO_EXITO, 10 | PRODUCTO_ELIMINADO_ERROR, 11 | OBTENER_PRODUCTO_EDITAR, 12 | COMENZAR_EDICION_PRODUCTO, 13 | PRODUCTO_EDITADO_EXITO, 14 | PRODUCTO_EDITADO_ERROR 15 | } from '../types'; 16 | import clienteAxios from '../config/axios'; 17 | import Swal from 'sweetalert2'; 18 | 19 | // Crear nuevos productos 20 | export function crearNuevoProductoAction(producto) { 21 | return async (dispatch) => { 22 | dispatch( agregarProducto() ); 23 | 24 | try { 25 | // insertar en la API 26 | await clienteAxios.post('/productos', producto); 27 | 28 | // Si todo sale bien, actualizar el state 29 | dispatch( agregarProductoExito(producto) ); 30 | 31 | // Alerta 32 | Swal.fire( 33 | 'Correcto', 34 | 'El producto se agregó correctamente', 35 | 'success' 36 | ); 37 | 38 | } catch (error) { 39 | console.log(error); 40 | // si hay un error cambiar el state 41 | dispatch( agregarProductoError(true) ); 42 | 43 | // alerta de error 44 | Swal.fire({ 45 | icon: 'error', 46 | title: 'Hubo un error', 47 | text: 'Hubo un error, intenta de nuevo' 48 | }) 49 | } 50 | } 51 | } 52 | 53 | const agregarProducto = () => ({ 54 | type: AGREGAR_PRODUCTO, 55 | payload: true 56 | }); 57 | 58 | // si el producto se guarda en la base de datos 59 | const agregarProductoExito = producto => ({ 60 | type: AGREGAR_PRODUCTO_EXITO, 61 | payload: producto 62 | }) 63 | 64 | // si hubo un error 65 | const agregarProductoError = estado => ({ 66 | type: AGREGAR_PRODUCTO_ERROR, 67 | payload: estado 68 | }); 69 | 70 | 71 | // Función que descarga los productos de la base de datos 72 | export function obtenerProductosAction() { 73 | return async (dispatch) => { 74 | dispatch( descargarProductos() ); 75 | 76 | try { 77 | const respuesta = await clienteAxios.get('/productos'); 78 | dispatch( descargaProductosExitosa(respuesta.data) ) 79 | } catch (error) { 80 | console.log(error); 81 | dispatch( descargaProductosError() ) 82 | } 83 | } 84 | } 85 | 86 | const descargarProductos = () => ({ 87 | type: COMENZAR_DESCARGA_PRODUCTOS, 88 | payload: true 89 | }); 90 | 91 | const descargaProductosExitosa = productos => ({ 92 | type: DESCARGA_PRODUCTOS_EXITO, 93 | payload: productos 94 | }) 95 | const descargaProductosError = () => ({ 96 | type: DESCARGA_PRODUCTOS_ERROR, 97 | payload: true 98 | }); 99 | 100 | // Selecciona y elimina el producto 101 | export function borrarProductoAction(id) { 102 | return async (dispatch) => { 103 | dispatch(obtenerProductoEliminar(id) ); 104 | 105 | try { 106 | await clienteAxios.delete(`/productos/${id}`); 107 | dispatch( eliminarProductoExito() ); 108 | 109 | // Si se elimina, mostrar alerta 110 | Swal.fire( 111 | 'Eliminado', 112 | 'El producto se eliminó correctamente', 113 | 'success' 114 | ) 115 | } catch (error) { 116 | console.log(error); 117 | dispatch( eliminarProductoError() ); 118 | } 119 | } 120 | } 121 | 122 | const obtenerProductoEliminar = id => ({ 123 | type: OBTENER_PRODUCTO_ELIMINAR, 124 | payload: id 125 | }); 126 | const eliminarProductoExito = () => ({ 127 | type: PRODUCTO_ELIMINADO_EXITO 128 | }) 129 | const eliminarProductoError = () => ({ 130 | type: PRODUCTO_ELIMINADO_ERROR, 131 | payload: true 132 | }); 133 | 134 | // Colocar producto en edición 135 | export function obtenerProductoEditar(producto) { 136 | return (dispatch) => { 137 | dispatch( obtenerProductoEditarAction(producto) ) 138 | } 139 | } 140 | 141 | const obtenerProductoEditarAction = producto => ({ 142 | type: OBTENER_PRODUCTO_EDITAR, 143 | payload: producto 144 | }) 145 | 146 | // Edita un registro en la api y state 147 | export function editarProductoAction(producto) { 148 | return async (dispatch) => { 149 | dispatch( editarProducto() ); 150 | 151 | try { 152 | await clienteAxios.put(`/productos/${producto.id}`, producto); 153 | dispatch( editarProductoExito(producto) ); 154 | } catch (error) { 155 | console.log(error); 156 | dispatch( editarProductoError() ); 157 | } 158 | } 159 | } 160 | const editarProducto = () => ({ 161 | type: COMENZAR_EDICION_PRODUCTO 162 | }); 163 | 164 | const editarProductoExito = producto => ({ 165 | type: PRODUCTO_EDITADO_EXITO, 166 | payload: producto 167 | }); 168 | 169 | const editarProductoError = () => ({ 170 | type: PRODUCTO_EDITADO_ERROR, 171 | payload: true 172 | }) -------------------------------------------------------------------------------- /src/components/EditarProducto.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { editarProductoAction } from '../actions/productoActions'; 4 | import { useHistory } from 'react-router-dom'; 5 | 6 | const EditarProducto = () => { 7 | 8 | const history = useHistory(); 9 | const dispatch = useDispatch(); 10 | 11 | // nuevo state de producto 12 | const [ producto, guardarProducto] = useState({ 13 | nombre: '', 14 | precio: '' 15 | }) 16 | 17 | // producto a editar 18 | const productoeditar = useSelector(state => state.productos.productoeditar); 19 | 20 | // llenar el state automaticamente 21 | useEffect( () => { 22 | guardarProducto(productoeditar); 23 | }, [productoeditar]); 24 | 25 | // Leer los datos del formulario 26 | const onChangeFormulario = e => { 27 | guardarProducto({ 28 | ...producto, 29 | [e.target.name] : e.target.value 30 | }) 31 | } 32 | 33 | 34 | const { nombre, precio} = producto; 35 | 36 | const submitEditarProducto = e => { 37 | e.preventDefault(); 38 | 39 | dispatch( editarProductoAction(producto) ); 40 | 41 | history.push('/'); 42 | } 43 | 44 | return ( 45 |
46 |
47 |
48 |
49 |

50 | Editar Producto 51 |

52 | 53 |
56 |
57 | 58 | 66 |
67 | 68 |
69 | 70 | 78 |
79 | 80 | 84 |
85 |
86 |
87 |
88 |
89 | ); 90 | } 91 | 92 | export default EditarProducto; -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const Header = () => { 5 | return ( 6 | 19 | ); 20 | } 21 | 22 | export default Header; -------------------------------------------------------------------------------- /src/components/NuevoProducto.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | // Actions de Redux 5 | import { crearNuevoProductoAction } from '../actions/productoActions'; 6 | import { mostrarAlerta, ocultarAlertaAction } from '../actions/alertaActions'; 7 | 8 | const NuevoProductos = ({history}) => { 9 | 10 | // state del componente 11 | const [nombre, guardarNombre] = useState(''); 12 | const [precio, guardarPrecio] = useState(0); 13 | 14 | // utilizar use dispatch y te crea una función 15 | const dispatch = useDispatch(); 16 | 17 | // Acceder al state del store 18 | const cargando = useSelector( state => state.productos.loading ); 19 | const error = useSelector(state => state.productos.error); 20 | const alerta = useSelector(state => state.alerta.alerta); 21 | 22 | 23 | // mandar llamar el action de productoAction 24 | const agregarProducto = producto => dispatch( crearNuevoProductoAction(producto) ); 25 | 26 | // cuando el usuario haga submit 27 | const submitNuevoProducto = e => { 28 | e.preventDefault(); 29 | 30 | // validar formulario 31 | if(nombre.trim() === '' || precio <= 0) { 32 | 33 | const alerta = { 34 | msg: 'Ambos campos son obligatorios', 35 | classes: 'alert alert-danger text-center text-uppercase p3' 36 | } 37 | dispatch( mostrarAlerta(alerta) ); 38 | 39 | return; 40 | } 41 | 42 | // si no hay errores 43 | dispatch( ocultarAlertaAction() ); 44 | 45 | // crear el nuevo producto 46 | agregarProducto({ 47 | nombre, 48 | precio 49 | }); 50 | 51 | // redireccionar 52 | history.push('/'); 53 | } 54 | 55 | 56 | return ( 57 |
58 |
59 |
60 |
61 |

62 | Agregar Nuevo Producto 63 |

64 | 65 | {alerta ?

{alerta.msg}

: null } 66 | 67 |
70 |
71 | 72 | guardarNombre(e.target.value)} 79 | /> 80 |
81 | 82 |
83 | 84 | guardarPrecio( Number(e.target.value) )} 91 | /> 92 |
93 | 94 | 98 |
99 | 100 | { cargando ?

Cargando...

: null } 101 | 102 | { error ?

Hubo un error

: null } 103 |
104 |
105 |
106 |
107 | ); 108 | } 109 | 110 | export default NuevoProductos; -------------------------------------------------------------------------------- /src/components/Producto.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import Swal from 'sweetalert2'; 4 | 5 | 6 | // Redux 7 | import { useDispatch } from 'react-redux'; 8 | import { borrarProductoAction, obtenerProductoEditar } from '../actions/productoActions'; 9 | 10 | const Producto = ({producto}) => { 11 | const { nombre, precio, id } = producto; 12 | 13 | const dispatch = useDispatch(); 14 | const history = useHistory(); // habilitar history para redirección 15 | 16 | // Confirmar si desea eliminarlo 17 | const confirmarEliminarProducto = id => { 18 | 19 | // preguntar al usuario 20 | Swal.fire({ 21 | title: '¿Estas seguro?', 22 | text: "Un producto que se elimina no se puede recuperar", 23 | icon: 'warning', 24 | showCancelButton: true, 25 | confirmButtonColor: '#3085d6', 26 | cancelButtonColor: '#d33', 27 | confirmButtonText: 'Si, eliminar!!', 28 | cancelButtonText: 'Cancelar' 29 | }).then((result) => { 30 | if (result.value) { 31 | // pasarlo al action 32 | dispatch( borrarProductoAction(id) ); 33 | } 34 | }); 35 | } 36 | 37 | // función que redirige de forma programada 38 | const redireccionarEdicion = producto => { 39 | dispatch( obtenerProductoEditar(producto) ); 40 | history.push(`/productos/editar/${producto.id}`) 41 | } 42 | 43 | return ( 44 | 45 | {nombre} 46 | $ {precio} 47 | 48 | 54 | 59 | 60 | 61 | ); 62 | } 63 | 64 | export default Producto; -------------------------------------------------------------------------------- /src/components/Productos.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect } from 'react'; 2 | import Producto from './Producto'; 3 | 4 | // Redux 5 | import { useSelector, useDispatch } from 'react-redux'; 6 | import { obtenerProductosAction } from '../actions/productoActions'; 7 | 8 | const Productos = () => { 9 | 10 | const dispatch = useDispatch(); 11 | 12 | useEffect( ()=> { 13 | 14 | // Consultar la api 15 | const cargarProductos = () => dispatch( obtenerProductosAction() ); 16 | cargarProductos(); 17 | // eslint-disable-next-line 18 | }, []); 19 | 20 | // obtener el state 21 | const productos = useSelector( state => state.productos.productos ); 22 | const error = useSelector(state => state.productos.error); 23 | const cargando = useSelector(state => state.productos.loading); 24 | 25 | return ( 26 | 27 |

Listado de Productos

28 | 29 | { error ?

Hubo un error

: null } 30 | 31 | { cargando ?

Cargando....

: null } 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | { productos.length === 0 ? 'No hay productos' : ( 43 | productos.map(producto => ( 44 | 48 | )) 49 | ) } 50 | 51 |
NombrePrecioAcciones
52 |
53 | ); 54 | } 55 | 56 | export default Productos; -------------------------------------------------------------------------------- /src/config/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const clienteAxios = axios.create({ 4 | baseURL: 'http://localhost:4000/' 5 | }); 6 | 7 | export default clienteAxios; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-weight: 400; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | font-size: 16px; 11 | } 12 | 13 | h1 a { 14 | font-size: 1.8rem; 15 | } 16 | h1 a.text-light:hover { 17 | color: white!important; 18 | text-decoration: none!important; 19 | } 20 | .nuevo-post{ 21 | font-weight: bold; 22 | text-transform: uppercase; 23 | } 24 | 25 | .btn { 26 | font-weight: bold; 27 | } 28 | .form-group label, 29 | .list-group-item { 30 | font-size: 1.4rem; 31 | } 32 | 33 | .navbar .btn { 34 | flex:1; 35 | } 36 | @media (min-width:768px){ 37 | .navbar .btn { 38 | flex: 0 1 auto; 39 | } 40 | } 41 | .acciones { 42 | margin: 1rem 0; 43 | } 44 | @media (min-width:768px) { 45 | .acciones{ 46 | margin: 0; 47 | } 48 | } 49 | .acciones > *{ 50 | flex: 1; 51 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/reducers/alertaReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | MOSTRAR_ALERTA, 3 | OCULTAR_ALERTA 4 | } from '../types'; 5 | 6 | // Cada reducer tiene su state 7 | const initialState = { 8 | alerta: null 9 | } 10 | 11 | export default function(state = initialState, action) { 12 | switch(action.type) { 13 | case MOSTRAR_ALERTA: 14 | return { 15 | ...state, 16 | alerta: action.payload 17 | } 18 | case OCULTAR_ALERTA: 19 | return { 20 | ...state, 21 | alerta: null 22 | } 23 | default: 24 | return state; 25 | } 26 | } -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import productosReducer from './productosReducer'; 3 | import alertaReducer from './alertaReducer'; 4 | 5 | export default combineReducers({ 6 | productos: productosReducer, 7 | alerta: alertaReducer 8 | }); -------------------------------------------------------------------------------- /src/reducers/productosReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | AGREGAR_PRODUCTO, 3 | AGREGAR_PRODUCTO_EXITO, 4 | AGREGAR_PRODUCTO_ERROR, 5 | COMENZAR_DESCARGA_PRODUCTOS, 6 | DESCARGA_PRODUCTOS_EXITO, 7 | DESCARGA_PRODUCTOS_ERROR, 8 | OBTENER_PRODUCTO_ELIMINAR, 9 | PRODUCTO_ELIMINADO_EXITO, 10 | PRODUCTO_ELIMINADO_ERROR, 11 | OBTENER_PRODUCTO_EDITAR, 12 | PRODUCTO_EDITADO_EXITO, 13 | PRODUCTO_EDITADO_ERROR 14 | } from '../types'; 15 | 16 | // cada reducer tiene su propio state 17 | const initialState = { 18 | productos: [], 19 | error: null, 20 | loading: false, 21 | productoeliminar: null, 22 | productoeditar: null 23 | } 24 | 25 | export default function(state = initialState, action) { 26 | switch(action.type) { 27 | case COMENZAR_DESCARGA_PRODUCTOS: 28 | case AGREGAR_PRODUCTO: 29 | return { 30 | ...state, 31 | loading: action.payload 32 | } 33 | case AGREGAR_PRODUCTO_EXITO: 34 | return { 35 | ...state, 36 | loading: false, 37 | productos: [...state.productos, action.payload] 38 | } 39 | case AGREGAR_PRODUCTO_ERROR: 40 | case DESCARGA_PRODUCTOS_ERROR: 41 | case PRODUCTO_ELIMINADO_ERROR: 42 | case PRODUCTO_EDITADO_ERROR: 43 | return { 44 | ...state, 45 | loading: false, 46 | error: action.payload 47 | } 48 | case DESCARGA_PRODUCTOS_EXITO: 49 | return { 50 | ...state, 51 | loading: false, 52 | error: null, 53 | productos: action.payload 54 | } 55 | case OBTENER_PRODUCTO_ELIMINAR: 56 | return { 57 | ...state, 58 | productoeliminar: action.payload 59 | } 60 | case PRODUCTO_ELIMINADO_EXITO: 61 | return { 62 | ...state, 63 | productos: state.productos.filter( producto => producto.id !== state.productoeliminar ), 64 | productoeliminar: null 65 | } 66 | case OBTENER_PRODUCTO_EDITAR: 67 | return { 68 | ...state, 69 | productoeditar: action.payload 70 | } 71 | case PRODUCTO_EDITADO_EXITO: 72 | return { 73 | ...state, 74 | productoeditar: null, 75 | productos: state.productos.map( producto => 76 | producto.id === action.payload.id ? producto = action.payload : producto 77 | ) 78 | } 79 | default: 80 | return state; 81 | } 82 | } -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import reducer from './reducers'; 4 | 5 | const store = createStore( 6 | reducer, 7 | compose( applyMiddleware(thunk), 8 | 9 | typeof window === 'object' && 10 | typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined' ? 11 | window.__REDUX_DEVTOOLS_EXTENSION__() : f => f 12 | ) 13 | ); 14 | 15 | export default store; -------------------------------------------------------------------------------- /src/types/index.js: -------------------------------------------------------------------------------- 1 | export const AGREGAR_PRODUCTO = 'AGREGAR_PRODUCTO'; 2 | export const AGREGAR_PRODUCTO_EXITO = 'AGREGAR_PRODUCTO_EXITO'; 3 | export const AGREGAR_PRODUCTO_ERROR = 'AGREGAR_PRODUCTO_ERROR'; 4 | 5 | export const COMENZAR_DESCARGA_PRODUCTOS = 'COMENZAR_DESCARGA_PRODUCTOS'; 6 | export const DESCARGA_PRODUCTOS_EXITO = 'DESCARGA_PRODUCTOS_EXITO'; 7 | export const DESCARGA_PRODUCTOS_ERROR = 'DESCARGA_PRODUCTOS_ERROR'; 8 | 9 | export const OBTENER_PRODUCTO_ELIMINAR = 'OBTENER_PRODUCTO_ELIMINAR'; 10 | export const PRODUCTO_ELIMINADO_EXITO = 'PRODUCTO_ELIMINADO_EXITO'; 11 | export const PRODUCTO_ELIMINADO_ERROR = 'PRODUCTO_ELIMINADO_ERROR'; 12 | 13 | export const OBTENER_PRODUCTO_EDITAR = 'OBTENER_PRODUCTO_EDITAR'; 14 | export const COMENZAR_EDICION_PRODUCTO = 'COMENZAR_EDICION_PRODUCTO'; 15 | export const PRODUCTO_EDITADO_EXITO = 'PRODUCTO_EDITADO_EXITO'; 16 | export const PRODUCTO_EDITADO_ERROR = 'PRODUCTO_EDITADO_ERROR'; 17 | 18 | export const MOSTRAR_ALERTA = 'MOSTRAR_ALERTA'; 19 | export const OCULTAR_ALERTA = 'OCULTAR_ALERTA'; --------------------------------------------------------------------------------