Cómo construir una TodoApp usando ReactJS y Firebase

Hola amigos, bienvenidos a este tutorial. Antes de comenzar, debe estar familiarizado con los conceptos básicos de ReactJS. Si no es así, le recomendaría que revise la documentación de ReactJS.

Usaremos los siguientes componentes en esta aplicación:

  1. ReactJS
  2. UI de material
  3. Firebase
  4. ExpressJS
  5. Cartero

Cómo se verá nuestra aplicación:

Arquitectura de la aplicación:

Entendiendo nuestros componentes:

Quizás se pregunte por qué usamos firebase en esta aplicación. Bueno, proporciona autenticación segura , una base de datos en tiempo real , un componente sin servidor y un depósito de almacenamiento .

Estamos usando Express aquí para no tener que manejar excepciones HTTP. Vamos a utilizar todos los paquetes de firebase en nuestro componente de funciones. Esto se debe a que no queremos que nuestra aplicación cliente sea demasiado grande, lo que tiende a ralentizar el proceso de carga de la interfaz de usuario.

Nota: voy a dividir este tutorial en cuatro secciones separadas. Al comienzo de cada sección, encontrará un git commit que tiene el código desarrollado en esa sección. Además, si desea ver el código completo, está disponible en este repositorio.

Sección 1: Desarrollo de API de Todo

En estosección , vamos a desarrollar estos elementos:

  1. Configure las funciones de la base de fuego.
  2. Instale el marco Express y cree las API de Todo.
  3. Configurando firestore como base de datos.

El código Todo API implementado en esta sección se puede encontrar en este compromiso.

Configure las funciones de Firebase:

Ve a la consola de Firebase.

Seleccione la opción Agregar proyecto . Después de eso, siga el gif a continuación paso a paso para configurar el proyecto de base de fuego.

Vaya a la pestaña de funciones y haga clic en el botón Comenzar :

Verá un cuadro de diálogo que tiene instrucciones sobre cómo configurar las funciones de Firebase . Vaya a su entorno local. Abra una herramienta de línea de comandos. Para instalar las herramientas de base de fuego en su máquina, use el siguiente comando:

 npm install -g firebase-tools

Una vez hecho esto, use el comando firebase initpara configurar las funciones de base de fuego en su entorno local. Seleccione las siguientes opciones al inicializar la función firebase en el entorno local:

  1. ¿Qué funciones de Firebase CLI desea configurar para esta carpeta? Presione la barra espaciadora para seleccionar funciones, luego Enter para confirmar sus opciones => Funciones: configurar e implementar Cloud Functions
  2. Primero, asociemos este directorio de proyecto con un proyecto de Firebase…. => Utilizar un proyecto existente
  3. Seleccione un proyecto de Firebase predeterminado para este directorio => nombre_aplicación
  4. ¿Qué idioma te gustaría usar para escribir Cloud Functions? => JavaScript
  5. ¿Quiere utilizar ESLint para detectar posibles errores y aplicar el estilo? => N
  6. ¿Quieres instalar dependencias con npm ahora? (Y / n) => Y

Una vez realizada la configuración, aparecerá el siguiente mensaje:

✔ Firebase initialization complete!

Esta será nuestra estructura de directorio una vez que se complete la inicialización:

+-- firebase.json +-- functions | +-- index.js | +-- node_modules | +-- package-lock.json | +-- package.json

Ahora abra el index.jsdirectorio de funciones bajo y copie y pegue el siguiente código:

const functions = require('firebase-functions'); exports.helloWorld = functions.https.onRequest((request, response) => { response.send("Hello from Firebase!"); });

Implemente el código en las funciones de firebase con el siguiente comando:

firebase deploy

Una vez finalizada la implementación, obtendrá el siguiente logline al final de su línea de comando:

> ✔ Deploy complete! > Project Console: //console.firebase.google.com/project/todoapp-/overview

Vaya a la Consola del proyecto> Funciones y allí encontrará la URL de la API. La URL se verá así:

//-todoapp-.cloudfunctions.net/helloWorld

Copie esta URL y péguela en el navegador. Obtendrá la siguiente respuesta:

Hello from Firebase!

Esto confirma que nuestra función de Firebase se ha configurado correctamente.

Instale Express Framework:

Ahora instalemos el Expressmarco en nuestro proyecto usando el siguiente comando:

npm i express

Ahora creemos un directorio de API dentro del directorio de funciones . Dentro de ese directorio, crearemos un archivo llamado todos.js. Elimine todo del index.jsy luego copie y pegue el siguiente código:

//index.js const functions = require('firebase-functions'); const app = require('express')(); const { getAllTodos } = require('./APIs/todos') app.get('/todos', getAllTodos); exports.api = functions.https.onRequest(app);

Hemos asignado la función getAllTodos a la ruta / todos . Entonces, todas las llamadas a la API en esta ruta se ejecutarán a través de la función getAllTodos. Ahora vaya al todos.jsarchivo en el directorio de API y aquí escribiremos la función getAllTodos.

//todos.js exports.getAllTodos = (request, response) => { todos = [ { 'id': '1', 'title': 'greeting', 'body': 'Hello world from sharvin shah' }, { 'id': '2', 'title': 'greeting2', 'body': 'Hello2 world2 from sharvin shah' } ] return response.json(todos); }

Aquí hemos declarado un objeto JSON de muestra. Más tarde lo obtendremos del Firestore. Pero por el momento devolveremos esto. Ahora implemente esto en su función de base de fuego usando el comando firebase deploy. Preguntarápara obtener permiso para eliminar el módulo helloworld , simplemente ingrese y .

The following functions are found in your project but do not exist in your local source code: helloWorld Would you like to proceed with deletion? Selecting no will continue the rest of the deployments. (y/N) y

Una vez hecho esto, vaya a la Consola del proyecto> Funciones y allí encontrará la URL de la API. La API se verá así:

//-todoapp-.cloudfunctions.net/api

Ahora vaya al navegador y copie y pegue la URL y agregue / todos al final de esta URL. Obtendrá el siguiente resultado:

[ { 'id': '1', 'title': 'greeting', 'body': 'Hello world from sharvin shah' }, { 'id': '2', 'title': 'greeting2', 'body': 'Hello2 world2 from sharvin shah' } ]

Firebase Firestore:

Usaremos un firebase firestore como base de datos en tiempo real para nuestra aplicación. Ahora vaya a la Consola> Base de datos en Firebase Console. Para configurar Firestore, siga el gif a continuación:

Una vez realizada la configuración, haga clic en el botón Iniciar colección y configure el ID de colección como todos . Haga clic en Siguiente y obtendrá la siguiente ventana emergente:

Ignore la clave DocumentID. Para el campo, el tipo y el valor , consulte el JSON a continuación. Actualice el valor en consecuencia:

{ Field: title, Type: String, Value: Hello World }, { Field: body, Type: String, Value: Hello folks I hope you are staying home... }, { Field: createtAt, type: timestamp, value: Add the current date and time here }

Presione el botón guardar. Verás que se crea la colección y el documento. Regrese al entorno local. Necesitamos instalar firebase-admincuál tiene el paquete firestore que necesitamos. Utilice este comando para instalarlo:

npm i firebase-admin

Cree un directorio llamado util en el directorio de funciones .Vaya a este directorio y cree un nombre de archivo admin.js. En este archivo, importaremos el paquete de administración de firebase e inicializaremos el objeto de la base de datos de firestore. Exportaremos esto para que otros módulos puedan usarlo.

//admin.js const admin = require('firebase-admin'); admin.initializeApp(); const db = admin.firestore(); module.exports = { admin, db };

Ahora escribamos una API para obtener estos datos. Ir a la todos.jsvirtud de la funciones API> directorio. Elimine el código anterior y copie y pegue el siguiente código:

//todos.js const { db } = require('../util/admin'); exports.getAllTodos = (request, response) => { db .collection('todos') .orderBy('createdAt', 'desc') .get() .then((data) => { let todos = []; data.forEach((doc) => { todos.push({ todoId: doc.id, title: doc.data().title, body: doc.data().body, createdAt: doc.data().createdAt, }); }); return response.json(todos); }) .catch((err) => { console.error(err); return response.status(500).json({ error: err.code}); }); };

Aquí obtenemos todos los todos de la base de datos y los reenviamos al cliente en una lista.

También puede ejecutar la aplicación localmente usando el firebase servecomando en lugar de implementarlo cada vez. Cuando ejecuta ese comando, puede obtener un error con respecto a las credenciales. Para solucionarlo, siga los pasos que se mencionan a continuación:

  1. Vaya a la configuración del proyecto (icono de configuración en la parte superior izquierda)
  2. Vaya a la pestaña de cuentas de servicio  
  3. Abajo estará la opción de Generar una nueva clave . Haga clic en esa opción y descargará un archivo con extensión JSON.
  4. Necesitamos exportar estas credenciales a nuestra sesión de línea de comandos. Utilice el siguiente comando para hacer eso:
export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/[FILE_NAME].json"

Después de eso, ejecute el comando firebase serve. Si sigue apareciendo el error a continuación, utilizar el siguiente comando: firebase login --reauth. Abrirá la página de inicio de sesión de Google en un navegador. Una vez que haya realizado el inicio de sesión, funcionará sin ningún error.

Encontrará una URL en los registros de su herramienta de línea de comandos cuando ejecute un comando de servicio de base de fuego. Abra esta URL en el navegador y agregue /todosdespués.

✔ functions[api]: http function initialized (//localhost:5000/todoapp-//api).

Obtendrá la siguiente salida JSON en su navegador:

[ { "todoId":"W67t1kSMO0lqvjCIGiuI", "title":"Hello World", "body":"Hello folks I hope you are staying home...", "createdAt":{"_seconds":1585420200,"_nanoseconds":0 } } ]

Escribir otras API:

Es hora de escribir todas las demás API de todo que vamos a requerir para nuestra aplicación.

  1. Crear elemento Todo: vaya al index.jsdirectorio de funciones. Importe el método postOneTodo bajo el getAllTodos existente. Además, asigne la ruta POST a ese método.
//index.js const { .., postOneTodo } = require('./APIs/todos') app.post('/todo', postOneTodo);

Vaya al todos.jsinterior del directorio de funciones y agregue un nuevo método postOneTodobajo el getAllTodosmétodo existente .

//todos.js exports.postOneTodo = (request, response) => { if (request.body.body.trim() === '') { return response.status(400).json({ body: 'Must not be empty' }); } if(request.body.title.trim() === '') { return response.status(400).json({ title: 'Must not be empty' }); } const newTodoItem = { title: request.body.title, body: request.body.body, createdAt: new Date().toISOString() } db .collection('todos') .add(newTodoItem) .then((doc)=>{ const responseTodoItem = newTodoItem; responseTodoItem.id = doc.id; return response.json(responseTodoItem); }) .catch((err) => { response.status(500).json({ error: 'Something went wrong' }); console.error(err); }); };

En este método, estamos agregando un nuevo Todo a nuestra base de datos. Si los elementos de nuestro cuerpo están vacíos, devolveremos una respuesta de 400 o de lo contrario agregaremos los datos.

Ejecute el comando firebase serve y abra la aplicación cartero. Cree una nueva solicitud y seleccione el tipo de método POST . Agregue la URL y un cuerpo de tipo JSON.

URL: //localhost:5000/todoapp-//api/todo METHOD: POST Body: { "title":"Hello World", "body": "We are writing this awesome API" }

Presione el botón enviar y obtendrá la siguiente respuesta:

{ "title": "Hello World", "body": "We are writing this awesome API", "createdAt": "2020-03-29T12:30:48.809Z", "id": "nh41IgARCj8LPWBYzjU0" }

2. Eliminar elemento Todo: vaya al index.jsdirectorio de funciones. Importe el método deleteTodo en el archivo postOneTodo existente. Además, asigne la ruta DELETE a ese método.

//index.js const { .., deleteTodo } = require('./APIs/todos') app.delete('/todo/:todoId', deleteTodo);

Vaya a todos.jsy agregue un nuevo método deleteTodobajo el postOneTodométodo existente .

//todos.js exports.deleteTodo = (request, response) => { const document = db.doc(`/todos/${request.params.todoId}`); document .get() .then((doc) => { if (!doc.exists) { return response.status(404).json({ error: 'Todo not found' }) } return document.delete(); }) .then(() => { response.json({ message: 'Delete successfull' }); }) .catch((err) => { console.error(err); return response.status(500).json({ error: err.code }); }); };

En este método, estamos eliminando un Todo de nuestra base de datos. Ejecute el comando de servicio de la base de fuego y vaya al cartero. Cree una nueva solicitud, seleccione el tipo de método como DELETE y agregue la URL.

URL: //localhost:5000/todoapp-//api/todo/ METHOD: DELETE

Presione el botón enviar y obtendrá la siguiente respuesta:

{ "message": "Delete successfull" }

3. Editar elemento Todo: vaya al index.jsdirectorio de funciones. Importe el método editTodo bajo el archivo deleteTodo existente. Además, asigne la ruta PUT a ese método.

//index.js const { .., editTodo } = require('./APIs/todos') app.put('/todo/:todoId', editTodo);

Vaya a todos.jsy agregue un nuevo método editTodobajo el deleteTodométodo existente .

//todos.js exports.editTodo = ( request, response ) => { if(request.body.todoId || request.body.createdAt){ response.status(403).json({message: 'Not allowed to edit'}); } let document = db.collection('todos').doc(`${request.params.todoId}`); document.update(request.body) .then(()=> { response.json({message: 'Updated successfully'}); }) .catch((err) => { console.error(err); return response.status(500).json({ error: err.code }); }); };

En este método, estamos editando un Todo de nuestra base de datos. Recuerde que aquí no permitimos al usuario editar los campos todoId o createdAt. Ejecute el comando de servicio de la base de fuego y vaya al cartero. Cree una nueva solicitud, seleccione el tipo de método como PUT y agregue la URL.

URL: //localhost:5000/todoapp-//api/todo/ METHOD: PUT

Presione el botón enviar y obtendrá la siguiente respuesta:

{ "message": "Updated successfully" }

Estructura de directorio hasta ahora:

+-- firebase.json +-- functions | +-- API | +-- +-- todos.js | +-- util | +-- +-- admin.js | +-- index.js | +-- node_modules | +-- package-lock.json | +-- package.json | +-- .gitignore

Con esto, hemos completado la primera sección de la aplicación. Puede tomar un café, tomar un descanso y luego trabajaremos en el desarrollo de las API de usuario.

Sección 2: Desarrollo de API de usuario

En estosección , vamos a desarrollar estos componentes:

  1. API de autenticación de usuario (inicio de sesión y registro).
  2. OBTENGA y actualice la API de detalles del usuario.
  3. Actualice la API de imágenes de perfil de usuario.
  4. Asegurar la API Todo existente.

El código de API de usuario implementado en esta sección se puede encontrar en este compromiso.

Entonces, comencemos a construir la API de autenticación de usuario. Ve a la consola de Firebase> Autenticación.

Haga clic en el botón Configurar método de inicio de sesión . Usaremos correo electrónico y contraseña para la validación del usuario. Habilite la opción Correo electrónico / Contraseña .

Ahora mismo crearemos manualmente nuestro usuario. Primero, crearemos la API de inicio de sesión. Después de eso, crearemos la API de registro.

Vaya a la pestaña Usuarios en Autenticación, complete los detalles del usuario y haga clic en el botón Agregar usuario .

1. API de inicio de sesión de usuario:

Primero, necesitamos instalar el firebasepaquete, que consiste en la biblioteca de autenticación de Firebase, usando el siguiente comando:

npm i firebase

Una vez finalizada la instalación, vaya al directorio funciones> API . Aquí crearemos un users.jsarchivo. Ahora en el interior index.jsimportamos un método loginUser y le asignamos la ruta POST.

//index.js const { loginUser } = require('./APIs/users') // Users app.post('/login', loginUser);

Vaya a Configuración del proyecto> General y allí encontrará la siguiente tarjeta:

Seleccione el ícono Web y luego siga el gif a continuación:

Seleccione la opción continuar con la consola . Una vez hecho esto, verá un JSON con configuración de base de fuego. Vaya al directorio functions> util y cree un   config.jsarchivo. Copie y pegue el siguiente código en este archivo:

// config.js module.exports = { apiKey: "............", authDomain: "........", databaseURL: "........", projectId: ".......", storageBucket: ".......", messagingSenderId: "........", appId: "..........", measurementId: "......." };

Reemplace ............con los valores que obtiene en Firebase console> Configuración del proyecto> General> sus aplicaciones> Fragmento de SD de Firebase> config .

Copie y pegue el siguiente código en el users.jsarchivo:

// users.js const { admin, db } = require('../util/admin'); const config = require('../util/config'); const firebase = require('firebase'); firebase.initializeApp(config); const { validateLoginData, validateSignUpData } = require('../util/validators'); // Login exports.loginUser = (request, response) => { const user = { email: request.body.email, password: request.body.password } const { valid, errors } = validateLoginData(user); if (!valid) return response.status(400).json(errors); firebase .auth() .signInWithEmailAndPassword(user.email, user.password) .then((data) => { return data.user.getIdToken(); }) .then((token) => { return response.json({ token }); }) .catch((error) => { console.error(error); return response.status(403).json({ general: 'wrong credentials, please try again'}); }) };

Aquí estamos usando un módulo signInWithEmailAndPassword de firebase para verificar si las credenciales enviadas por el usuario son correctas. Si tienen razón, enviamos el token de ese usuario o un estado 403 con un mensaje de "credenciales incorrectas".

Ahora vamos a crear validators.jsen virtud de las funciones> util directorio. Copie y pegue el siguiente código en este archivo:

// validators.js const isEmpty = (string) => { if (string.trim() === '') return true; else return false; }; exports.validateLoginData = (data) => { let errors = {}; if (isEmpty(data.email)) errors.email = 'Must not be empty'; if (isEmpty(data.password)) errors.password = 'Must not be empty'; return { errors, valid: Object.keys(errors).length === 0 ? true : false }; };

Con esto se completa nuestro LoginAPI . Ejecute el firebase servecomando y vaya al cartero. Cree una nueva solicitud, seleccione el tipo de método POST y agregue la URL y el cuerpo.

URL: //localhost:5000/todoapp-//api/login METHOD: POST Body: { "email":"Add email that is assigned for user in console", "password": "Add password that is assigned for user in console" }

Presione el botón enviar solicitud en cartero y obtendrá el siguiente resultado:

{ "token": ".........." }

Usaremos este token en una próxima parte para obtener los detalles del usuario . Recuerde que este token caduca en 60 minutos . Para generar un nuevo token, use esta API nuevamente.

2. API de registro de usuario:

El mecanismo de autenticación predeterminado de firebase solo le permite almacenar información como correo electrónico, contraseña, etc. Pero necesitamos más información para identificar si este usuario es el propietario de esa tarea para que pueda realizar operaciones de lectura, actualización y eliminación en ella.

Para lograr este objetivo vamos a crear una nueva colección denominada usuarios . En esta colección, almacenaremos los datos del usuario que se asignarán a la tarea en función del nombre de usuario. Cada nombre de usuario será único para todos los usuarios de la plataforma.

Vaya al index.js. Importamos un método signUpUser y le asignamos la ruta POST.

//index.js const { .., signUpUser } = require('./APIs/users') app.post('/signup', signUpUser);

Ahora vaya al validators.jsy agregue el siguiente código debajo del validateLoginDatamétodo.

// validators.js const isEmail = (email) => { const emailRegEx = /^(([^()\[\]\\.,;:\[email protected]"]+(\.[^()\[\]\\.,;:\[email protected]"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; if (email.match(emailRegEx)) return true; else return false; }; exports.validateSignUpData = (data) => { let errors = {}; if (isEmpty(data.email)) { errors.email = 'Must not be empty'; } else if (!isEmail(data.email)) { errors.email = 'Must be valid email address'; } if (isEmpty(data.firstName)) errors.firstName = 'Must not be empty'; if (isEmpty(data.lastName)) errors.lastName = 'Must not be empty'; if (isEmpty(data.phoneNumber)) errors.phoneNumber = 'Must not be empty'; if (isEmpty(data.country)) errors.country = 'Must not be empty'; if (isEmpty(data.password)) errors.password = 'Must not be empty'; if (data.password !== data.confirmPassword) errors.confirmPassword = 'Passowrds must be the same'; if (isEmpty(data.username)) errors.username = 'Must not be empty'; return { errors, valid: Object.keys(errors).length === 0 ? true : false }; };

Ahora vaya al users.jsy agregue el siguiente código debajo del loginUsermódulo.

// users.js exports.signUpUser = (request, response) => { const newUser = { firstName: request.body.firstName, lastName: request.body.lastName, email: request.body.email, phoneNumber: request.body.phoneNumber, country: request.body.country, password: request.body.password, confirmPassword: request.body.confirmPassword, username: request.body.username }; const { valid, errors } = validateSignUpData(newUser); if (!valid) return response.status(400).json(errors); let token, userId; db .doc(`/users/${newUser.username}`) .get() .then((doc) => { if (doc.exists) { return response.status(400).json({ username: 'this username is already taken' }); } else { return firebase .auth() .createUserWithEmailAndPassword( newUser.email, newUser.password ); } }) .then((data) => { userId = data.user.uid; return data.user.getIdToken(); }) .then((idtoken) => { token = idtoken; const userCredentials = { firstName: newUser.firstName, lastName: newUser.lastName, username: newUser.username, phoneNumber: newUser.phoneNumber, country: newUser.country, email: newUser.email, createdAt: new Date().toISOString(), userId }; return db .doc(`/users/${newUser.username}`) .set(userCredentials); }) .then(()=>{ return response.status(201).json({ token }); }) .catch((err) => { console.error(err); if (err.code === 'auth/email-already-in-use') { return response.status(400).json({ email: 'Email already in use' }); } else { return response.status(500).json({ general: 'Something went wrong, please try again' }); } }); }

Validamos nuestros datos de usuario y luego enviamos un correo electrónico y una contraseña al módulo firebase createUserWithEmailAndPassword para crear el usuario. Una vez que el usuario se crea con éxito, guardamos las credenciales del usuario en la base de datos.

Con esto se completa nuestra API de SignUp . Ejecute el firebase servecomando y vaya al cartero. Cree una nueva solicitud, seleccione el tipo de método como POST . Agrega la URL y el cuerpo.

URL: //localhost:5000/todoapp-//api/signup METHOD: POST Body: { "firstName": "Add a firstName here", "lastName": "Add a lastName here", "email":"Add a email here", "phoneNumber": "Add a phone number here", "country": "Add a country here", "password": "Add a password here", "confirmPassword": "Add same password here", "username": "Add unique username here" }

Presione el botón enviar solicitud en cartero y obtendrá el siguiente resultado:

{ "token": ".........." }

Ahora vaya a la consola de Firebase> Base de datos y allí verá el siguiente resultado:

Como puede ver, la colección de nuestro usuario se creó con éxito con un documento en ella.

3. Cargar imagen de perfil de usuario:

Nuestros usuarios podrán subir su foto de perfil. Para lograr esto, usaremos Storage bucket. Vaya a Firebase console> Almacenamiento y haga clic en el botón Comenzar . Siga el GIF a continuación para la configuración:

Ahora vaya a la pestaña Reglas en Almacenamiento y actualice el permiso para el acceso al depósito según la imagen a continuación:

Para subir la foto de perfil usaremos el paquete llamado busboy. Para instalar este paquete, use el siguiente comando:

npm i busboy

Ir a index.js. Importe el método uploadProfilePhoto debajo del método signUpUser existente. También asigne la ruta POST a ese método.

//index.js const auth = require('./util/auth'); const { .., uploadProfilePhoto } = require('./APIs/users') app.post('/user/image', auth, uploadProfilePhoto);

Aquí hemos agregado una capa de autenticación para que solo un usuario asociado con esa cuenta pueda cargar la imagen. Ahora cree un archivo llamado auth.jsen funciones> directorio utils . Copie y pegue el siguiente código en ese archivo:

// auth.js const { admin, db } = require('./admin'); module.exports = (request, response, next) => { let idToken; if (request.headers.authorization && request.headers.authorization.startsWith('Bearer ')) { idToken = request.headers.authorization.split('Bearer ')[1]; } else { console.error('No token found'); return response.status(403).json({ error: 'Unauthorized' }); } admin .auth() .verifyIdToken(idToken) .then((decodedToken) => { request.user = decodedToken; return db.collection('users').where('userId', '==', request.user.uid).limit(1).get(); }) .then((data) => { request.user.username = data.docs[0].data().username; request.user.imageUrl = data.docs[0].data().imageUrl; return next(); }) .catch((err) => { console.error('Error while verifying token', err); return response.status(403).json(err); }); };

Aquí estamos usando el módulo firebase verifyIdToken para verificar el token. Después de eso, estamos decodificando los detalles del usuario y pasándolos en la solicitud existente.

Vaya a users.jsy agregue el siguiente código debajo del signupmétodo:

// users.js deleteImage = (imageName) => { const bucket = admin.storage().bucket(); const path = `${imageName}` return bucket.file(path).delete() .then(() => { return }) .catch((error) => { return }) } // Upload profile picture exports.uploadProfilePhoto = (request, response) => { const BusBoy = require('busboy'); const path = require('path'); const os = require('os'); const fs = require('fs'); const busboy = new BusBoy({ headers: request.headers }); let imageFileName; let imageToBeUploaded = {}; busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { if (mimetype !== 'image/png' && mimetype !== 'image/jpeg') { return response.status(400).json({ error: 'Wrong file type submited' }); } const imageExtension = filename.split('.')[filename.split('.').length - 1]; imageFileName = `${request.user.username}.${imageExtension}`; const filePath = path.join(os.tmpdir(), imageFileName); imageToBeUploaded = { filePath, mimetype }; file.pipe(fs.createWriteStream(filePath)); }); deleteImage(imageFileName); busboy.on('finish', () => { admin .storage() .bucket() .upload(imageToBeUploaded.filePath, { resumable: false, metadata: { metadata: { contentType: imageToBeUploaded.mimetype } } }) .then(() => { const imageUrl = `//firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${imageFileName}?alt=media`; return db.doc(`/users/${request.user.username}`).update({ imageUrl }); }) .then(() => { return response.json({ message: 'Image uploaded successfully' }); }) .catch((error) => { console.error(error); return response.status(500).json({ error: error.code }); }); }); busboy.end(request.rawBody); };

Con esto se completa nuestra API de Cargar Imagen de Perfil . Ejecute el firebase servecomando y vaya al cartero. Cree una nueva solicitud, seleccione el tipo de método POST , agregue la URL y, en la sección del cuerpo, seleccione el tipo como datos de formulario.

La solicitud está protegida, por lo que también deberá enviar el token de portador . Para enviar el token de portador, inicie sesión nuevamente si el token ha expirado. Después de eso, en Postman App> pestaña Autorización> Tipo> Bearer Token y en la sección de token pegue el token.

URL: //localhost:5000/todoapp-//api/user/image METHOD: GET Body: { REFER THE IMAGE down below }

Presione el botón enviar solicitud en cartero y obtendrá el siguiente resultado:

{ "message": "Image uploaded successfully" }

4. Obtenga detalles de usuario:

Aquí estamos obteniendo los datos de nuestro usuario de la base de datos. Vaya a index.jse importe el método getUserDetail y asígnele la ruta GET.

// index.js const { .., getUserDetail } = require('./APIs/users') app.get('/user', auth, getUserDetail);

Ahora vaya al users.jsy agregue el siguiente código después del uploadProfilePhotomódulo:

// users.js exports.getUserDetail = (request, response) => { let userData = {}; db .doc(`/users/${request.user.username}`) .get() .then((doc) => { if (doc.exists) { userData.userCredentials = doc.data(); return response.json(userData); } }) .catch((error) => { console.error(error); return response.status(500).json({ error: error.code }); }); }

Estamos usando el módulo firebase doc (). Get () para derivar los detalles del usuario. Con esto se completa nuestra API GET User Details . Ejecute el firebase servecomando y vaya al cartero. Cree una nueva solicitud, seleccione el tipo de método: GET y agregue la URL y el cuerpo.

La solicitud está protegida, por lo que también deberá enviar el token de portador . Para enviar el token de portador, inicie sesión nuevamente si el token ha expirado.

URL: //localhost:5000/todoapp-//api/user METHOD: GET

Presione el botón enviar solicitud en cartero y obtendrá el siguiente resultado:

{ "userCredentials": { "phoneNumber": "........", "email": "........", "country": "........", "userId": "........", "username": "........", "createdAt": "........", "lastName": "........", "firstName": "........" } }

5. Actualice los detalles del usuario:

Ahora agreguemos la funcionalidad para actualizar los detalles del usuario. Vaya a index.jsy copie y pegue el siguiente código:

// index.js const { .., updateUserDetails } = require('./APIs/users') app.post('/user', auth, updateUserDetails);

Ahora vaya al users.jsy agregue el updateUserDetailsmódulo debajo del existente getUserDetails:

// users.js exports.updateUserDetails = (request, response) => { let document = db.collection('users').doc(`${request.user.username}`); document.update(request.body) .then(()=> { response.json({message: 'Updated successfully'}); }) .catch((error) => { console.error(error); return response.status(500).json({ message: "Cannot Update the value" }); }); }

Aquí estamos usando el método de actualización de base de fuego . Con esto se completa nuestra API Actualizar detalles de usuario . Siga el mismo procedimiento para una solicitud que con la API Obtener detalles del usuario anterior con un cambio. Agregue el cuerpo en la solicitud aquí y el método como POST.

URL: //localhost:5000/todoapp-//api/user METHOD: POST Body : { // You can edit First Name, last Name and country // We will disable other Form Tags from our UI }

Presione el botón enviar solicitud en cartero y obtendrá el siguiente resultado:

{ "message": "Updated successfully" }

6. Asegurar las API de Todo:

Para asegurar la API de Todo de modo que solo el usuario elegido pueda acceder a ella, haremos algunos cambios en nuestro código existente. En primer lugar, actualizaremos nuestro de la index.jssiguiente manera:

// index.js // Todos app.get('/todos', auth, getAllTodos); app.get('/todo/:todoId', auth, getOneTodo); app.post('/todo',auth, postOneTodo); app.delete('/todo/:todoId',auth, deleteTodo); app.put('/todo/:todoId',auth, editTodo);

Hemos actualizado todas las rutas de Todo agregando authpara que todas las llamadas a la API requieran un token y solo el usuario en particular pueda acceder a ellas.

Después de que vaya a la todos.jsvirtud de la funciones API> directorio.

  1. Crear API Todo: Abra todos.jsy debajo del método postOneTodo agregue la clave de nombre de usuario de la siguiente manera:
const newTodoItem = { .., username: request.user.username, .. }

2. GET All Todos API: abra todos.jsy bajo el método getAllTodos agregue la cláusula where de la siguiente manera:

db .collection('todos') .where('username', '==', request.user.username) .orderBy('createdAt', 'desc')

Ejecute el servicio de firebase y pruebe nuestra API GET. No olvide enviar el token de portador. Aquí obtendrá un error de respuesta de la siguiente manera:

{ "error": 9 }

Vaya a la línea de comando y verá las siguientes líneas registradas:

i functions: Beginning execution of "api"> Error: 9 FAILED_PRECONDITION: The query requires an index. You can create it here: > at callErrorFromStatus

Abre esto en el navegador y haga clic en crear índice.

Una vez que se haya creado el índice, envíe la solicitud nuevamente y obtendrá el siguiente resultado:

[ { "todoId": "......", "title": "......", "username": "......", "body": "......", "createdAt": "2020-03-30T13:01:58.478Z" } ]

3.   Eliminar la API de Todo: Abra todos.jsy en el método deleteTodo agregue la siguiente condición. Agregue esta condición dentro de la consulta document.get (). Then () debajo de la condición ! Doc.exists .

.. if(doc.data().username !== request.user.username){ return response.status(403).json({error:"UnAuthorized"}) }

Estructura de directorios hasta ahora:

+-- firebase.json +-- functions | +-- API | +-- +-- todos.js | +-- +-- users.js | +-- util | +-- +-- admin.js | +-- +-- auth.js | +-- +-- validators.js | +-- index.js | +-- node_modules | +-- package-lock.json | +-- package.json | +-- .gitignore

Con esto hemos completado nuestra API backend. Tómese un descanso, tome un café y luego comenzaremos a construir la interfaz de nuestra aplicación.

Sección 3: Panel de usuario

En estosección , vamos a desarrollar estos componentes:

  1. Configure ReactJS y Material UI.
  2. Formulario de inicio de sesión y registro del edificio.
  3. Sección de cuenta de construcción.

El código del panel de usuario implementado en esta sección se puede encontrar en este compromiso.

1. Configure ReactJS y Material UI:

Usaremos la plantilla create-react-app. Nos da una estructura fundamental para desarrollar la aplicación. Para instalarlo, use el siguiente comando:

npm install -g create-react-app

Vaya a la carpeta raíz del proyecto donde está presente el directorio de funciones. Inicialice nuestra aplicación de interfaz con el siguiente comando:

create-react-app view

Recuerde utilizar la versión v16.13.1 dela biblioteca ReactJS .

Una vez que se complete la instalación, verá lo siguiente en los registros de la línea de comandos:

cd view npm start Happy hacking!

Con esto, hemos configurado nuestra aplicación React. Obtendrá la siguiente estructura de directorio:

+-- firebase.json +-- functions { This Directory consists our API logic } +-- view { This Directory consists our FrontEnd Compoenents } +-- .firebaserc +-- .gitignore

Ahora ejecute la aplicación usando el comando npm start. Vaya al navegador //localhost:3000/y verá el siguiente resultado:

Ahora eliminaremos todos los componentes innecesarios. Vaya al directorio de vista y luego elimine todos los archivosque tienen [Eliminar] delante de ellos. Para ello, consulte la estructura de árbol de directorios a continuación.

+-- README.md [ Remove ] +-- package-lock.json +-- package.json +-- node_modules +-- .gitignore +-- public | +-- favicon.ico [ Remove ] | +-- index.html | +-- logo192.png [ Remove ] | +-- logo512.png [ Remove ] | +-- manifest.json | +-- robots.txt +-- src | +-- App.css | +-- App.test.js | +-- index.js | +-- serviceWorker.js | +-- App.js | +-- index.css [ Remove ] | +-- logo.svg [ Remove ] | +-- setupTests.js

Vaya a index.htmldebajo del directorio público y elimine las siguientes líneas:

Ahora vaya al App.jsdirectorio src y reemplace el código anterior con el siguiente código:

import React from 'react'; function App() { return ( ); } export default App;

Vaya a index.jsy elimine la siguiente importación:

import './index.css'

No he eliminado App.cssni lo estoy usando en esta aplicación. Pero si desea eliminarlo o utilizarlo, puede hacerlo.

Vaya al navegador //localhost:3000/y obtendrá una salida de pantalla en blanco.

Para instalar Material UI, vaya al directorio de visualización y copie y pegue este comando en la terminal:

npm install @material-ui/core

Recuerde usar la versión v4.9.8 de la biblioteca Material UI.

2. Formulario de inicio de sesión:

Para desarrollar el formulario de inicio de sesión, vaya a App.js. En la parte superior de App.jsagregue las siguientes importaciones:

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import login from './pages/login';

Estamos usando Switch y Route para asignar rutas para nuestra TodoApp. Ahora mismo agregaremos solo la ruta / login y le asignaremos un componente de inicio de sesión.

// App.js 

Cree un directorio de páginas en el directorio de vistas existente y un archivo con el nombre login.jsdel directorio de páginas .

Importaremos los componentes Material UI y el paquete Axios en login.js:

// login.js // Material UI components import React, { Component } from 'react'; import Avatar from '@material-ui/core/Avatar'; import Button from '@material-ui/core/Button'; import CssBaseline from '@material-ui/core/CssBaseline'; import TextField from '@material-ui/core/TextField'; import Link from '@material-ui/core/Link'; import Grid from '@material-ui/core/Grid'; import LockOutlinedIcon from '@material-ui/icons/LockOutlined'; import Typography from '@material-ui/core/Typography'; import withStyles from '@material-ui/core/styles/withStyles'; import Container from '@material-ui/core/Container'; import CircularProgress from '@material-ui/core/CircularProgress'; import axios from 'axios';

Agregaremos los siguientes estilos a nuestra página de inicio de sesión:

// login.js const styles = (theme) => ({ paper: { marginTop: theme.spacing(8), display: 'flex', flexDirection: 'column', alignItems: 'center' }, avatar: { margin: theme.spacing(1), backgroundColor: theme.palette.secondary.main }, form: { width: '100%', marginTop: theme.spacing(1) }, submit: { margin: theme.spacing(3, 0, 2) }, customError: { color: 'red', fontSize: '0.8rem', marginTop: 10 }, progess: { position: 'absolute' } });

Crearemos una clase llamada login que tiene un formulario y un controlador de envío dentro.

// login.js class login extends Component { constructor(props) { super(props); this.state = { email: '', password: '', errors: [], loading: false }; } componentWillReceiveProps(nextProps) { if (nextProps.UI.errors) { this.setState({ errors: nextProps.UI.errors }); } } handleChange = (event) => { this.setState({ [event.target.name]: event.target.value }); }; handleSubmit = (event) => { event.preventDefault(); this.setState({ loading: true }); const userData = { email: this.state.email, password: this.state.password }; axios .post('/login', userData) .then((response) => { localStorage.setItem('AuthToken', `Bearer ${response.data.token}`); this.setState({ loading: false, }); this.props.history.push('/'); }) .catch((error) => { this.setState({ errors: error.response.data, loading: false }); }); }; render() { const { classes } = this.props; const { errors, loading } = this.state; return ( Login      Sign In {loading && }     {"Don't have an account? Sign Up"}    {errors.general && (  {errors.general}  )} ); } }

Al final de este archivo, agregue la siguiente exportación:

export default withStyles(styles)(login); 

Agregue nuestra URL de funciones de base de fuego para ver> package.json de la siguiente manera:

Recuerde: agregue una clave llamada proxy debajo del objeto JSON de la lista de navegadores existente
"proxy": "//-todoapp-.cloudfunctions.net/api"

Instale el paquete Axios y el icono de material usando los siguientes comandos:

// Axios command: npm i axios // Material Icons: npm install @material-ui/icons

Hemos agregado una ruta de inicio de sesión en App.js. En el login.js, hemos creado un componente de clase que maneja el estado, envía la solicitud de publicación a la API de inicio de sesión utilizando el paquete Axios. Si la solicitud tiene éxito, almacenamos el token. Si obtenemos errores en la respuesta, simplemente los representamos en la interfaz de usuario.

Vaya al navegador en //localhost:3000/loginy verá la siguiente interfaz de usuario de inicio de sesión.

Intente completar las credenciales incorrectas o enviar una solicitud vacía y obtendrá los errores. Envíe una solicitud válida. Vaya a la consola del desarrollador> Aplicación . Verá que el token de los usuarios se almacena en el almacenamiento local. Una vez que el inicio de sesión sea exitoso, seremos redirigidos a la página de inicio.

3. Formulario de registro:

Para desarrollar el formulario de registro, vaya a App.jsy actualice el Routecomponente existente con la línea siguiente:

// App.js 

No olvide importar:

// App.js import signup from './pages/signup';

Cree un archivo con el nombre signup.jsdel directorio de páginas .

Dentro de signup.js importaremos la interfaz de usuario de material y el paquete Axios:

// signup.js import React, { Component } from 'react'; import Avatar from '@material-ui/core/Avatar'; import Button from '@material-ui/core/Button'; import CssBaseline from '@material-ui/core/CssBaseline'; import TextField from '@material-ui/core/TextField'; import Link from '@material-ui/core/Link'; import Grid from '@material-ui/core/Grid'; import LockOutlinedIcon from '@material-ui/icons/LockOutlined'; import Typography from '@material-ui/core/Typography'; import Container from '@material-ui/core/Container'; import withStyles from '@material-ui/core/styles/withStyles'; import CircularProgress from '@material-ui/core/CircularProgress'; import axios from 'axios';

Agregaremos los siguientes estilos a nuestra página de registro:

// signup.js const styles = (theme) => ({ paper: { marginTop: theme.spacing(8), display: 'flex', flexDirection: 'column', alignItems: 'center' }, avatar: { margin: theme.spacing(1), backgroundColor: theme.palette.secondary.main }, form: { width: '100%', // Fix IE 11 issue. marginTop: theme.spacing(3) }, submit: { margin: theme.spacing(3, 0, 2) }, progess: { position: 'absolute' } }); 

Crearemos una clase llamada signup que tiene un formulario y un controlador de envío dentro.

// signup.js class signup extends Component { constructor(props) { super(props); this.state = { firstName: '', lastName: '', phoneNumber: '', country: '', username: '', email: '', password: '', confirmPassword: '', errors: [], loading: false }; } componentWillReceiveProps(nextProps) { if (nextProps.UI.errors) { this.setState({ errors: nextProps.UI.errors }); } } handleChange = (event) => { this.setState({ [event.target.name]: event.target.value }); }; handleSubmit = (event) => { event.preventDefault(); this.setState({ loading: true }); const newUserData = { firstName: this.state.firstName, lastName: this.state.lastName, phoneNumber: this.state.phoneNumber, country: this.state.country, username: this.state.username, email: this.state.email, password: this.state.password, confirmPassword: this.state.confirmPassword }; axios .post('/signup', newUserData) .then((response) => { localStorage.setItem('AuthToken', `${response.data.token}`); this.setState({ loading: false, }); this.props.history.push('/'); }) .catch((error) => { this.setState({ errors: error.response.data, loading: false }); }); }; render() { const { classes } = this.props; const { errors, loading } = this.state; return ( Sign up                              Sign Up {loading && }     Already have an account? Sign in ); } }

Al final de este archivo, agregue la siguiente exportación:

export default withStyles(styles)(signup); 

La lógica del componente de registro es la misma que la del componente de inicio de sesión. Vaya al navegador en //localhost:3000/signupy verá la siguiente interfaz de usuario de registro. Una vez que el registro sea exitoso, seremos redirigidos a la página de inicio.

Intente completar las credenciales incorrectas o enviar una solicitud vacía y obtendrá los errores. Envíe una solicitud válida. Vaya a la consola del desarrollador> Aplicación . Verá que el token de los usuarios se almacena en el almacenamiento local.

4. Sección de cuenta:

Para crear la página de la cuenta, primero necesitaremos crear nuestra página de inicio desde donde cargaremos la sección de la cuenta . Vaya a App.jsy actualice la siguiente ruta:

// App.js 

No olvide la importación:

// App.js import home from './pages/home';

Cree un nuevo archivo llamado home.js. Este archivo será el índice de nuestra aplicación. Las secciones Cuenta y Todo se cargan en esta página según el clic del botón.

Importe los paquetes Material UI, el paquete Axios, nuestra cuenta personalizada, los componentes de tareas pendientes y el middleware de autenticación.

// home.js import React, { Component } from 'react'; import axios from 'axios'; import Account from '../components/account'; import Todo from '../components/todo'; import Drawer from '@material-ui/core/Drawer'; import AppBar from '@material-ui/core/AppBar'; import CssBaseline from '@material-ui/core/CssBaseline'; import Toolbar from '@material-ui/core/Toolbar'; import List from '@material-ui/core/List'; import Typography from '@material-ui/core/Typography'; import Divider from '@material-ui/core/Divider'; import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import withStyles from '@material-ui/core/styles/withStyles'; import AccountBoxIcon from '@material-ui/icons/AccountBox'; import NotesIcon from '@material-ui/icons/Notes'; import Avatar from '@material-ui/core/avatar'; import ExitToAppIcon from '@material-ui/icons/ExitToApp'; import CircularProgress from '@material-ui/core/CircularProgress'; import { authMiddleWare } from '../util/auth'

Configuraremos nuestro DrawerWidth de la siguiente manera:

const drawerWidth = 240;

Agregaremos el siguiente estilo a nuestra página de inicio:

const styles = (theme) => ({ root: { display: 'flex' }, appBar: { zIndex: theme.zIndex.drawer + 1 }, drawer: { width: drawerWidth, flexShrink: 0 }, drawerPaper: { width: drawerWidth }, content: { flexGrow: 1, padding: theme.spacing(3) }, avatar: { height: 110, width: 100, flexShrink: 0, flexGrow: 0, marginTop: 20 }, uiProgess: { position: 'fixed', zIndex: '1000', height: '31px', width: '31px', left: '50%', top: '35%' }, toolbar: theme.mixins.toolbar });

Crearemos una clase llamada hogar. Esta clase tendrá una llamada a la API para obtener la foto de perfil del usuario, el nombre y el apellido. También tendrá lógica para elegir qué componente mostrar, ya sea Todo o Cuenta:

class home extends Component { state = { render: false }; loadAccountPage = (event) => { this.setState({ render: true }); }; loadTodoPage = (event) => { this.setState({ render: false }); }; logoutHandler = (event) => { localStorage.removeItem('AuthToken'); this.props.history.push('/login'); }; constructor(props) { super(props); this.state = { firstName: '', lastName: '', profilePicture: '', uiLoading: true, imageLoading: false }; } componentWillMount = () => { authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios .get('/user') .then((response) => { console.log(response.data); this.setState({ firstName: response.data.userCredentials.firstName, lastName: response.data.userCredentials.lastName, email: response.data.userCredentials.email, phoneNumber: response.data.userCredentials.phoneNumber, country: response.data.userCredentials.country, username: response.data.userCredentials.username, uiLoading: false, profilePicture: response.data.userCredentials.imageUrl }); }) .catch((error) => { if(error.response.status === 403) { this.props.history.push('/login') } console.log(error); this.setState({ errorMsg: 'Error in retrieving the data' }); }); }; render() { const { classes } = this.props; if (this.state.uiLoading === true) { return ( {this.state.uiLoading && } ); } else { return ( TodoApp 

{' '} {this.state.firstName} {this.state.lastName}

{' '} {' '} {' '} {' '} {' '} {' '} {this.state.render ? : } ); } } }

Aquí en el código, verá que authMiddleWare(this.props.history);se usa. Este middleware comprueba si el authToken es nulo. En caso afirmativo, devolverá al usuario al archivo login.js. Esto se agrega para que nuestro usuario no pueda acceder a la /ruta sin Registrarse o iniciar sesión. Al final de este archivo agregue la siguiente exportación:

export default withStyles(styles)(home); 

¿Ahora te preguntas qué hace este código home.js?

 {this.state.render ?  : } 

Está comprobando el estado de renderizado que estamos configurando al hacer clic en el botón. Creemos el directorio de componentes, y bajo ese directorio creemos dos archivos: account.jsy todo.js.

Creemos un directorio llamado util y un archivo llamado auth.jsbajo ese directorio. Copie y pegue el siguiente código en auth.js:

export const authMiddleWare = (history) => { const authToken = localStorage.getItem('AuthToken'); if(authToken === null){ history.push('/login') } }

Por tiempo estar dentro del todo.jsarchivo, simplemente escribiremos una clase que muestre el texto Hola, estoy todo . Trabajaremos en nuestros todos en la siguiente sección:

import React, { Component } from 'react' import withStyles from '@material-ui/core/styles/withStyles'; import Typography from '@material-ui/core/Typography'; const styles = ((theme) => ({ content: { flexGrow: 1, padding: theme.spacing(3), }, toolbar: theme.mixins.toolbar, }) ); class todo extends Component { render() { const { classes } = this.props; return ( Hello I am todo   ) } } export default (withStyles(styles)(todo));

Ahora es el momento de la sección de cuentas. Importe la utilidad Material UI, clsx, axios y authmiddleWare en nuestro account.js.

// account.js import React, { Component } from 'react'; import withStyles from '@material-ui/core/styles/withStyles'; import Typography from '@material-ui/core/Typography'; import CircularProgress from '@material-ui/core/CircularProgress'; import CloudUploadIcon from '@material-ui/icons/CloudUpload'; import { Card, CardActions, CardContent, Divider, Button, Grid, TextField } from '@material-ui/core'; import clsx from 'clsx'; import axios from 'axios'; import { authMiddleWare } from '../util/auth';

Agregaremos el siguiente estilo a nuestra página de Cuenta:

// account.js const styles = (theme) => ({ content: { flexGrow: 1, padding: theme.spacing(3) }, toolbar: theme.mixins.toolbar, root: {}, details: { display: 'flex' }, avatar: { height: 110, width: 100, flexShrink: 0, flexGrow: 0 }, locationText: { paddingLeft: '15px' }, buttonProperty: { position: 'absolute', top: '50%' }, uiProgess: { position: 'fixed', zIndex: '1000', height: '31px', width: '31px', left: '50%', top: '35%' }, progess: { position: 'absolute' }, uploadButton: { marginLeft: '8px', margin: theme.spacing(1) }, customError: { color: 'red', fontSize: '0.8rem', marginTop: 10 }, submitButton: { marginTop: '10px' } });

Crearemos un componente de clase llamado cuenta. Por el momento, simplemente copie y pegue el siguiente código:

// account.js class account extends Component { constructor(props) { super(props); this.state = { firstName: '', lastName: '', email: '', phoneNumber: '', username: '', country: '', profilePicture: '', uiLoading: true, buttonLoading: false, imageError: '' }; } componentWillMount = () => { authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios .get('/user') .then((response) => { console.log(response.data); this.setState({ firstName: response.data.userCredentials.firstName, lastName: response.data.userCredentials.lastName, email: response.data.userCredentials.email, phoneNumber: response.data.userCredentials.phoneNumber, country: response.data.userCredentials.country, username: response.data.userCredentials.username, uiLoading: false }); }) .catch((error) => { if (error.response.status === 403) { this.props.history.push('/login'); } console.log(error); this.setState({ errorMsg: 'Error in retrieving the data' }); }); }; handleChange = (event) => { this.setState({ [event.target.name]: event.target.value }); }; handleImageChange = (event) => { this.setState({ image: event.target.files[0] }); }; profilePictureHandler = (event) => { event.preventDefault(); this.setState({ uiLoading: true }); authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); let form_data = new FormData(); form_data.append('image', this.state.image); form_data.append('content', this.state.content); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios .post('/user/image', form_data, { headers: { 'content-type': 'multipart/form-data' } }) .then(() => { window.location.reload(); }) .catch((error) => { if (error.response.status === 403) { this.props.history.push('/login'); } console.log(error); this.setState({ uiLoading: false, imageError: 'Error in posting the data' }); }); }; updateFormValues = (event) => { event.preventDefault(); this.setState({ buttonLoading: true }); authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; const formRequest = { firstName: this.state.firstName, lastName: this.state.lastName, country: this.state.country }; axios .post('/user', formRequest) .then(() => { this.setState({ buttonLoading: false }); }) .catch((error) => { if (error.response.status === 403) { this.props.history.push('/login'); } console.log(error); this.setState({ buttonLoading: false }); }); }; render() { const { classes, ...rest } = this.props; if (this.state.uiLoading === true) { return ( {this.state.uiLoading && }  ); } else { return ( {this.state.firstName} {this.state.lastName}  

Al final de este archivo, agregue la siguiente exportación:

export default withStyles(styles)(account); 

En account.jshay gran cantidad de componentes utilizados. Primero veamos cómo se ve nuestra aplicación. Después de eso, explicaré todos los componentes que se utilizan y por qué se utilizan.

Vaya al navegador y si su token está vencido, lo redireccionará a la   loginpágina. Agregue sus datos e inicie sesión nuevamente. Una vez que haya hecho eso, vaya a la pestaña Cuenta y encontrará la siguiente interfaz de usuario:

Hay 3 controladores en la sección de cuenta:

  1. componentWillMount : este es el método de ciclo de vida incorporado de React. Lo estamos usando para cargar los datos antes del ciclo de vida de renderizado y actualizar nuestros valores de estado.
  2. ProfilePictureUpdate: este es nuestro controlador personalizado que estamos usando para que cuando nuestro usuario haga clic en el botón Cargar foto, enviará los datos a un servidor y volverá a cargar la página para mostrar la nueva imagen de perfil del usuario.
  3. updateFormValues: este también es nuestro controlador personalizado para actualizar los detalles del usuario. Aquí, el usuario puede actualizar su nombre, apellido y país. No permitimos actualizaciones de correo electrónico y nombre de usuario porque nuestra lógica de backend depende de esas claves.

Aparte de estos 3 controladores, es una página de formulario con estilo en la parte superior. Aquí está la estructura del directorio hasta este punto dentro de la carpeta de vista:

+-- public +-- src | +-- components | +-- +-- todo.js | +-- +-- account.js | +-- pages | +-- +-- home.js | +-- +-- login.js | +-- +-- signup.js | +-- util | +-- +-- auth.js | +-- README.md | +-- package-lock.json | +-- package.json | +-- .gitignore

Con esto hemos completado nuestro Panel de Cuenta. Ahora ve a tomar un café, tómate un descanso y en la siguiente sección, crearemos el Tablero de Todo.

Sección 4: Tablero de Todo

En estosección , vamos a desarrollar la interfaz de usuario para estas características del Panel de Todos:

  1. Agregar un Todo:
  2. Obtener todos:
  3. Eliminar una tarea
  4. Editar una tarea
  5. Conseguir un todo
  6. Aplicación de tema

El código de Todo Dashboard implementado en esta sección se puede encontrar en este compromiso.

Ir a todos.jsbajo el componentes de directorio. Agregue las siguientes importaciones a las importaciones existentes:

import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import AddCircleIcon from '@material-ui/icons/AddCircle'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import IconButton from '@material-ui/core/IconButton'; import CloseIcon from '@material-ui/icons/Close'; import Slide from '@material-ui/core/Slide'; import TextField from '@material-ui/core/TextField'; import Grid from '@material-ui/core/Grid'; import Card from '@material-ui/core/Card'; import CardActions from '@material-ui/core/CardActions'; import CircularProgress from '@material-ui/core/CircularProgress'; import CardContent from '@material-ui/core/CardContent'; import MuiDialogTitle from '@material-ui/core/DialogTitle'; import MuiDialogContent from '@material-ui/core/DialogContent'; import axios from 'axios'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import { authMiddleWare } from '../util/auth';

También necesitamos agregar los siguientes elementos CSS en los componentes de estilo existentes:

const styles = (theme) => ({ .., // Existing CSS elements title: { marginLeft: theme.spacing(2), flex: 1 }, submitButton: { display: 'block', color: 'white', textAlign: 'center', position: 'absolute', top: 14, right: 10 }, floatingButton: { position: 'fixed', bottom: 0, right: 0 }, form: { width: '98%', marginLeft: 13, marginTop: theme.spacing(3) }, toolbar: theme.mixins.toolbar, root: { minWidth: 470 }, bullet: { display: 'inline-block', margin: '0 2px', transform: 'scale(0.8)' }, pos: { marginBottom: 12 }, uiProgess: { position: 'fixed', zIndex: '1000', height: '31px', width: '31px', left: '50%', top: '35%' }, dialogeStyle: { maxWidth: '50%' }, viewRoot: { margin: 0, padding: theme.spacing(2) }, closeButton: { position: 'absolute', right: theme.spacing(1), top: theme.spacing(1), color: theme.palette.grey[500] } });

Agregaremos la transición para el cuadro de diálogo emergente:

const Transition = React.forwardRef(function Transition(props, ref) { return ; });

Elimine la clase de tareas pendientes existente y copie y pegue la siguiente clase:

class todo extends Component { constructor(props) { super(props); this.state = { todos: '', title: '', body: '', todoId: '', errors: [], open: false, uiLoading: true, buttonType: '', viewOpen: false }; this.deleteTodoHandler = this.deleteTodoHandler.bind(this); this.handleEditClickOpen = this.handleEditClickOpen.bind(this); this.handleViewOpen = this.handleViewOpen.bind(this); } handleChange = (event) => { this.setState({ [event.target.name]: event.target.value }); }; componentWillMount = () => { authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios .get('/todos') .then((response) => { this.setState({ todos: response.data, uiLoading: false }); }) .catch((err) => { console.log(err); }); }; deleteTodoHandler(data) { authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; let todoId = data.todo.todoId; axios .delete(`todo/${todoId}`) .then(() => { window.location.reload(); }) .catch((err) => { console.log(err); }); } handleEditClickOpen(data) { this.setState({ title: data.todo.title, body: data.todo.body, todoId: data.todo.todoId, buttonType: 'Edit', open: true }); } handleViewOpen(data) { this.setState({ title: data.todo.title, body: data.todo.body, viewOpen: true }); } render() { const DialogTitle = withStyles(styles)((props) => { const { children, classes, onClose, ...other } = props; return (  {children} {onClose ? (    ) : null}  ); }); const DialogContent = withStyles((theme) => ({ viewRoot: { padding: theme.spacing(2) } }))(MuiDialogContent); dayjs.extend(relativeTime); const { classes } = this.props; const { open, errors, viewOpen } = this.state; const handleClickOpen = () => { this.setState({ todoId: '', title: '', body: '', buttonType: '', open: true }); }; const handleSubmit = (event) => { authMiddleWare(this.props.history); event.preventDefault(); const userTodo = { title: this.state.title, body: this.state.body }; let options = {}; if (this.state.buttonType === 'Edit') { options = { url: `/todo/${this.state.todoId}`, method: 'put', data: userTodo }; } else { options = { url: '/todo', method: 'post', data: userTodo }; } const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios(options) .then(() => { this.setState({ open: false }); window.location.reload(); }) .catch((error) => { this.setState({ open: true, errors: error.response.data }); console.log(error); }); }; const handleViewClose = () => { this.setState({ viewOpen: false }); }; const handleClose = (event) => { this.setState({ open: false }); }; if (this.state.uiLoading === true) { return ( {this.state.uiLoading && }  ); } else { return ( {this.state.buttonType === 'Edit' ? 'Edit Todo' : 'Create a new Todo'}   {this.state.buttonType === 'Edit' ? 'Save' : 'Submit'}                {this.state.todos.map((todo) => (     {todo.title}   {dayjs(todo.createdAt).fromNow()}   {`${todo.body.substring(0, 65)}`}     this.handleViewOpen({ todo })}> {' '} View{' '}   this.handleEditClickOpen({ todo })}> Edit   this.deleteTodoHandler({ todo })}> Delete     ))}    {this.state.title}       ); } } }

Al final de este archivo, agregue la siguiente exportación:

export default withStyles(styles)(todo); 

Primero entenderemos cómo funciona nuestra IU y luego entenderemos el código. Vaya al navegador y obtendrá la siguiente interfaz de usuario:

Haga clic en el botón Agregar en la esquina inferior derecha y obtendrá la siguiente pantalla:

Agregue el título y los detalles de Todo y presione el botón Enviar. Obtendrá la siguiente pantalla:

Después de esto, haga clic en el botón Ver y podrá ver los detalles completos de Todo:

Haga clic en el botón Editar y podrá editar la tarea:

Haga clic en el botón Eliminar y podrá eliminar Todo. Ahora que sabemos cómo funciona Dashboard, entenderemos los componentes que se utilizan en él.

1. Agregar Todo: Para implementar el agregar todo usaremos el componente Diálogo de Material UI. Este componente implementa una funcionalidad de gancho. Estamos usando las clases, así que eliminaremos esa funcionalidad.

// This sets the state to open and buttonType flag to add: const handleClickOpen = () => { this.setState({ todoId: '', title: '', body: '', buttonType: '', open: true }); }; // This sets the state to close: const handleClose = (event) => { this.setState({ open: false }); };

Aparte de esto, también cambiaremos la ubicación del botón Agregar todo.

// Position our button floatingButton: { position: 'fixed', bottom: 0, right: 0 }, 

Ahora reemplazaremos la etiqueta de la lista con un formulario dentro de este Diálogo. Nos ayudará a agregar la nueva tarea.

// Show Edit or Save depending on buttonType state {this.state.buttonType === 'Edit' ? 'Save' : 'Submit'} // Our Form to add a todo    // TextField here   // TextField here   

loshandleSubmitconsta de lógica para leer el buttonTypeestado. Si el estado es una cadena vacía (“”), se publicará en la API Add Todo. Si el estado es un, Editentonces en ese escenario actualizará Editar Todo.

2. Get Todos: Para mostrar los todos usaremos el Grid containery dentro de él colocamos el Grid item. Dentro de eso, usaremos un Cardcomponente para mostrar los datos.

 {this.state.todos.map((todo) => (    // Here will show Todo with view, edit and delete button   ))} 

Usamos el mapa para mostrar el elemento de tareas pendientes a medida que la API los envía en una lista. Usaremos el ciclo de vida componentWillMount para obtener y establecer el estado antes de que se ejecute el render. Hay 3 botones ( ver, editar y eliminar ), por lo que necesitaremos 3 controladores para manejar la operación cuando se hace clic en el botón. Aprenderemos sobre estos botones en sus respectivas subsecciones.

3. Editar todo: Para editar todo, estamos reutilizando el código emergente de diálogo que se usa en agregar todo. Para diferenciar entre los clics de los botones, estamos usando un buttonTypeestado. Para Add Todo el   buttonTypeestado es (“”)mientras que para editar todo lo es Edit.

handleEditClickOpen(data) { this.setState({ .., buttonType: 'Edit', .. }); }

En el handleSubmitmétodo leemos el buttonTypeestado y luego enviamos la solicitud en consecuencia.

4. Delete Todo: Cuando se hace clic en este botón, enviamos el objeto de todo a nuestro deleteTodoHandler y luego envía la solicitud al backend.

 this.deleteTodoHandler({ todo })}>Delete

5. Ver todo: Al mostrar los datos lo hemos truncado para que el usuario pueda vislumbrar de qué se trata el todo. Pero si un usuario quiere saber más al respecto, debe hacer clic en el botón Ver.

Para ello, usaremos el diálogo Personalizado. Dentro de eso, usamos DialogTitle y DialogContent. Muestra nuestro título y contenido. En DialougeContent usaremos el formulario para mostrar el contenido que el usuario ha publicado. (Esta es una solución que encontré, hay muchas y puede probar otras).

// This is used to remove the underline of the Form InputProps={{ disableUnderline: true }} // This is used so that user cannot edit the data readonly

6. Aplicación del tema: este es el último paso de nuestra aplicación. Aplicaremos un tema en nuestra aplicación. Para ello estamos utilizando createMuiThemey ThemeProviderdesde material UI. Copie y pegue el siguiente código en App.js:

import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles'; import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; const theme = createMuiTheme({ palette: { primary: { light: '#33c9dc', main: '#FF5722', dark: '#d50000', contrastText: '#fff' } } }); function App() { return (  // Router and switch will be here.  ); }

Perdimos aplicar un tema a nuestro botón todo.jsen el CardActions. Agregue la etiqueta de color para el botón de visualización, edición y eliminación.

Vaya al navegador y encontrará que todo es igual, excepto que la aplicación es de un color diferente.

¡Y hemos terminado! Hemos construido una TodoApp usando ReactJS y Firebase. Si lo ha construido hasta este punto, le felicito enormemente por este logro.

No dudes en conectarte conmigo en Twitter y Github.