# Front-end Pistas Deportivas Front en React para la API REST Flask Mongo de gestión de reservas de instalaciones deportivas. ## Diseño de la aplicación En este enlace puedes ver el [back-end](https://gitlab.iesvirgendelcarmen.com/juangu/adt07-flask-mongo-praetorian-reservas). ## Creación del proyecto Creamos el proyecto y añadimos las dependencias con: ```bash npm create vite@latest front-pistas-deportivas cd front-pistas-deportivas npm install react-router-dom axios react-bootstrap bootstrap npm install ``` Creamos las carpetas necesarias para el proyecto: Carpeta | Uso --------|---- pages | para las páginas components | para los componentes services | para la api (peticiones al back) Buscamos cómo hacer un NavBar con react-bootstrap y [de este ejemplo sacamos la nuestra](https://react-bootstrap.netlify.app/docs/components/navbar/): ```javascript import Container from 'react-bootstrap/Container'; import Nav from 'react-bootstrap/Nav'; import Navbar from 'react-bootstrap/Navbar'; function NavBar() { return ( <> <Navbar bg="primary" data-bs-theme="dark"> <Container> <Navbar.Brand href="#home">Navbar</Navbar.Brand> <Nav className="me-auto"> <Nav.Link href="/">Home</Nav.Link> </Nav> </Container> </Navbar> </> ); } export default NavBar; ``` [Aquí puedes ver](./src/components/NavBar.jsx) el componente NavBar terminado. Modificamos el App.js para que cargue bootstrap [(tal y como viene en la documentación oficial)](https://react-bootstrap.netlify.app/docs/getting-started/introduction) y además contenga el router: ```javascript import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import NavBar from './components/NavBar'; import HomePage from './pages/HomePage'; import 'bootstrap/dist/css/bootstrap.min.css'; /** * Con React Router-v7 siempre usaremos esta manera de definir rutas. * No obstante es compatible (hacia atrás) con la manera antigua de v5. */ const router = createBrowserRouter([ { path: "/", element: <HomePage />, } ]); const App = () => { return ( <> <NavBar /> <RouterProvider router={router} /> </> ); }; export default App ``` Borramos los CSS de App.jsx, main.jsx e index.html porque queremos usar sólo react-bootstrap (por eso el import que ves en el código anterior para cargarlo). Creamos la página [HomePage](./src/pages/HomePage.jsx). Creamos la [página LoginPage](./src/pages/LoginPage.jsx) y su [componente Login](./src/components/Login.jsx). Para que el NavBar funcione bien (no haga que la página se recargue) necesitamos que esté dentro del Router, por eso ahora vamos a crear un componente "RootLayout" que será la base del resto de componentes: ```javascript import { Outlet } from "react-router-dom"; import NavBar from "./NavBar"; import Container from "react-bootstrap/Container"; import Row from "react-bootstrap/Row"; import Col from "react-bootstrap/Col"; const RootLayout = () => { return ( <> <NavBar /> {/* Contenedor principal para el contenido de la página */} <Container className="my-4"> <Outlet /> </Container> {/* Footer */} <footer className="bg-dark text-white py-3 mt-auto"> <Container> <Row> <Col className="text-center"> © {new Date().getFullYear()} IES Virgen del Carmen. CFGS Desarrollo de Aplicaciones Multiplataforma. </Col> </Row> </Container> </footer> </> ); }; export default RootLayout; ``` Ya podemos empezar con el sistema de Login. ## El sistema de Login Para el sistema de login vamos a necesitar crear, por un lado un par de servicios que se encarguen de comunicarse con el backend, obtener el token JWT y almacenarlo en el cliente, y por otro el servicio que se encargue de autenticar todas las peticiones gracias al token almacenado. Adicionalmente necesitamos un componente `Login.jsx` que es un formulario de inicio de sesión que permite a los usuarios autenticarse en la aplicación. Este componente se encuentra dentro de la página [`LoginPage.jsx`](src/pages/LoginPage.jsx), la cual es una de las rutas definidas en el enrutador principal de la aplicación en [`App.jsx`](src/App.jsx). ### Servicio `api.js` El archivo `api.js` se encarga de configurar una instancia de Axios para realizar peticiones HTTP a la API de la aplicación. Este archivo se encuentra en la ruta [`src/services/api.js`](src/services/api.js). #### Configuración de Axios Se importa Axios y se crea una instancia con una configuración base que incluye la URL base de la API. ```javascript // filepath: src/services/api.js import axios from 'axios'; const api = axios.create({ baseURL: 'http://localhost:5000/api', // URL base de la API }); export default api; ``` ### Servicio `auth.js` El archivo `auth.js` se encarga de manejar el token de autenticación en el almacenamiento local del navegador. Este archivo se encuentra en la ruta [`src/services/auth.js`](src/services/auth.js). #### Funciones de Autenticación El archivo define dos funciones principales: `setToken` y `clearToken`. - `setToken(token)`: Almacena el token JWT en el almacenamiento local. - `clearToken()`: Elimina el token JWT del almacenamiento local. ```javascript // filepath: src/services/auth.js export const setToken = (token) => { localStorage.setItem('token', token); }; export const clearToken = () => { localStorage.removeItem('token'); }; ``` ### Componente `Login.jsx` #### Importaciones En el componente `Login.jsx` cargamos estas librerías y módulos: - `react-bootstrap` para los componentes de formulario y botones. - `react` para manejar el estado del componente. - `react-router-dom` para la navegación. - `api` de [`src/services/api.js`](src/services/api.js) para realizar peticiones HTTP con axios. - `clearToken` y `setToken` de [`src/services/auth.js`](src/services/auth.js) para manejar el token de autenticación en el almacenamiento local. #### Estado del Componente El componente utiliza el hook `useState` para manejar tres estados: - `username`: Almacena el nombre de usuario ingresado. - `password`: Almacena la contraseña ingresada. - `error`: Almacena cualquier mensaje de error que ocurra durante el proceso de inicio de sesión. #### Navegación El hook `useNavigate` de `react-router-dom` se utiliza para redirigir al usuario a diferentes rutas después de un intento de inicio de sesión. #### Función `manejaLogin` Esta función se ejecuta cuando el usuario envía el formulario de inicio de sesión. Realiza los siguientes pasos: 1. Previene el comportamiento por defecto del formulario. 2. Limpia cualquier mensaje de error previo. 3. Intenta realizar una petición POST a la ruta `/auth/login` con el `username` y `password` ingresados. 4. Si la petición es exitosa, almacena el token JWT recibido en el almacenamiento local y redirige al usuario a la página principal (`/`). 5. Si ocurre un error, limpia el token del almacenamiento local y muestra un mensaje de error. #### Renderizado del Formulario El formulario incluye dos campos de entrada para el nombre de usuario y la contraseña, y un botón para enviar el formulario. Si hay un mensaje de error, este se muestra debajo del formulario. ```jsx import { Button, Form } from "react-bootstrap"; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import api from "../services/api"; import { clearToken, setToken } from "../services/auth"; const Login = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const navigate = useNavigate(); const manejaLogin = async (event) => { event.preventDefault(); setError(''); try { const response = await api.post('/auth/login', { username, password }); setToken(response.data.jwt); navigate('/'); } catch (err) { console.log(err); clearToken(); setError('Credenciales no válidas'); } } return( <Form> <Form.Group className="mb-3"> <Form.Label>Username:</Form.Label> <Form.Control type="text" placeholder="Nombre de Usuario" aria-label="Nombre de Usuario" value={username} onChange={(e) => setUsername(e.target.value)} /> </Form.Group> <Form.Group className="mb-3"> <Form.Label>Password</Form.Label> <Form.Control type="password" placeholder="Contraseña" aria-label="Password" value={password} onChange={(e) => setPassword(e.target.value)} /> </Form.Group> <Form.Group className="mb-3"> <Button onClick={manejaLogin}> Enviar! </Button> </Form.Group> {error && <p style={{ color: 'red' }}>{error}</p>} </Form> ); }; export default Login; ``` ### Integración en `LoginPage.jsx` El componente `Login.jsx` se utiliza en la página [`LoginPage.jsx`](src/pages/LoginPage.jsx), la cual se define de la siguiente manera: ```jsx import { Container } from "react-bootstrap"; import Login from "../components/Login"; const LoginPage = () => { return( <Container> <h3>Inicio de Sesión</h3> <Login /> </Container> ); }; export default LoginPage; ``` ### Ruta en `App.jsx` La página `LoginPage.jsx` está configurada como una de las rutas en el enrutador principal definido en [`App.jsx`](src/App.jsx): ```jsx const router = createBrowserRouter([ { path: "/", element: <RootLayout />, children: [ { index: true, element: <HomePage />, }, { path: "login", element: <LoginPage />, }, // Otras rutas... ], }, ]); const App = () => { return <RouterProvider router={router} />; }; export default App; ``` Con esta configuración, cuando el usuario navega a la ruta `/login`, se renderiza la página `LoginPage.jsx`, que a su vez muestra el componente `Login.jsx`. ### Integración de `api.jsx` y `auth.jsx` en `Login.jsx` El componente `Login.jsx` utiliza estos servicios para manejar el proceso de autenticación, puedes verlo en las importaciones: ```javascript import api from "../services/api"; import { clearToken, setToken } from "../services/auth"; ``` #### Uso en la Función `manejaLogin` En la función `manejaLogin`, se realiza una petición POST a la ruta `/auth/login` utilizando la instancia de Axios configurada en `api.js`. Si la petición es exitosa, se almacena el token JWT utilizando la función `setToken` de `auth.js`. Si ocurre un error, se limpia el token utilizando la función `clearToken`. ```javascript const manejaLogin = async (event) => { event.preventDefault(); setError(''); try { const response = await api.post('/auth/login', { username, password }); setToken(response.data.jwt); navigate('/'); } catch (err) { console.log(err); clearToken(); setError('Credenciales no válidas'); } }; ``` Con esta configuración, el componente `Login.jsx` puede manejar el proceso de autenticación de manera efectiva utilizando los servicios `api.js` y `auth.js`. ## CRUD de Instalaciones El CRUD de instalaciones se maneja a través de tres páginas principales: `InstalacionesPage`, `InstalacionFormPage` e `InstalacionDeletePage`. Estas páginas se han añadido al enrutador en el archivo `App.js` y reutilizan el componente `InstalacionForm` para añadir, borrar y editar instalaciones. ### Páginas del CRUD #### `InstalacionesPage` Esta página muestra una lista de todas las instalaciones disponibles. Permite a los usuarios ver los detalles de cada instalación y proporciona enlaces para editar o eliminar instalaciones. #### `InstalacionFormPage` Esta página se utiliza tanto para añadir nuevas instalaciones como para editar instalaciones existentes. Reutiliza el componente `InstalacionForm` y muestra diferentes partes del formulario en función de la ruta y los parámetros proporcionados. #### `InstalacionDeletePage` Esta página se utiliza para confirmar y realizar la eliminación de una instalación. Muestra un mensaje de confirmación y un botón para proceder con la eliminación. ### Configuración del Router en `App.js` Las rutas para estas páginas se configuran en el archivo `App.js` de la siguiente manera: ```javascript // filepath: src/App.js const router = createBrowserRouter([ { path: "/", element: <RootLayout />, children: [ { index: true, element: <HomePage />, }, { path: "login", element: <LoginPage />, }, { path: "instalaciones", element: <InstalacionesPage />, }, { path: "instalaciones/add", element: <InstalacionFormPage />, }, { path: "instalaciones/edit/:id", element: <InstalacionFormPage />, }, { path: "instalaciones/del/:id", element: <InstalacionDeletePage />, }, // Otras rutas... ], }, ]); const App = () => { return <RouterProvider router={router} />; }; export default App; ``` ### Componente `InstalacionForm` El componente `InstalacionForm` se reutiliza tanto para añadir como para editar instalaciones. Dependiendo de la ruta, se muestran diferentes partes del formulario. #### Ejemplo de `InstalacionForm` ```javascript // filepath: src/components/InstalacionForm.jsx import { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import api from '../services/api'; const InstalacionForm = () => { const { id } = useParams(); const navigate = useNavigate(); const [nombre, setNombre] = useState(''); const [descripcion, setDescripcion] = useState(''); useEffect(() => { if (id) { // Si hay un ID, estamos editando una instalación existente api.get(`/instalaciones/${id}`).then(response => { setNombre(response.data.nombre); setDescripcion(response.data.descripcion); }); } }, [id]); const handleSubmit = async (event) => { event.preventDefault(); if (id) { // Editar instalación existente await api.put(`/instalaciones/${id}`, { nombre, descripcion }); } else { // Añadir nueva instalación await api.post('/instalaciones', { nombre, descripcion }); } navigate('/instalaciones'); }; return ( <form onSubmit={handleSubmit}> <div> <label>Nombre:</label> <input type="text" value={nombre} onChange={(e) => setNombre(e.target.value)} /> </div> <div> <label>Descripción:</label> <textarea value={descripcion} onChange={(e) => setDescripcion(e.target.value)} /> </div> <button type="submit">{id ? 'Editar' : 'Añadir'} Instalación</button> </form> ); }; export default InstalacionForm; ``` ### Uso de `InstalacionForm` en `InstalacionFormPage` La página `InstalacionFormPage` reutiliza el componente `InstalacionForm` para manejar tanto la creación como la edición de instalaciones. ```javascript // filepath: src/pages/InstalacionFormPage.jsx import { Container } from 'react-bootstrap'; import InstalacionForm from '../components/InstalacionForm'; const InstalacionFormPage = () => { return ( <Container> <h3>{useParams().id ? 'Editar Instalación' : 'Nueva Instalación'}</h3> <InstalacionForm /> </Container> ); }; export default InstalacionFormPage; ``` ### Confirmación de Eliminación en `InstalacionDeletePage` La página `InstalacionDeletePage` maneja la eliminación de una instalación mostrando un mensaje de confirmación. ```javascript // filepath: src/pages/InstalacionDeletePage.jsx import { useParams, useNavigate } from 'react-router-dom'; import api from '../services/api'; const InstalacionDeletePage = () => { const { id } = useParams(); const navigate = useNavigate(); const handleDelete = async () => { await api.delete(`/instalaciones/${id}`); navigate('/instalaciones'); }; return ( <div> <h3>¿Estás seguro de que deseas eliminar esta instalación?</h3> <button onClick={handleDelete}>Eliminar</button> <button onClick={() => navigate('/instalaciones')}>Cancelar</button> </div> ); }; export default InstalacionDeletePage; ```