En este tutorial vamos a crear un blog React de pila completa junto con un administrador de blog back-end.
Lo guiaré a través de todos los pasos en detalle.
Al final de este tutorial, tendrá el conocimiento suficiente para crear aplicaciones de pila completa bastante complejas utilizando herramientas modernas: React, Express y una base de datos PostgreSQL.
Para mantener las cosas concisas, haré el mínimo estilo / diseño y lo dejaré en manos del lector.
Proyecto completado:
//github.com/iqbal125/react-hooks-complete-fullstack
Aplicación de administración:
//github.com/iqbal125/react-hooks-admin-app-fullstack
Proyecto de inicio:
//github.com/iqbal125/react-hooks-routing-auth-starter
Cómo construir el proyecto de inicio:
//www.freecodecamp.org/news/build-a-react-hooks-front-end-app-with-routing-and-authentication/
Cómo agregar un motor de búsqueda Fullstack a este proyecto:
//www.freecodecamp.org/news/react-express-fullstack-search-engine-with-psql/
Puedes ver una versión en video de este tutorial aquí.
//www.youtube.com/playlist?list=PLMc67XEAt-yzxRboCFHza4SBOxNr7hDD5
Conéctese conmigo en Twitter para obtener más actualizaciones sobre futuros tutoriales: //twitter.com/iqbal125sfSección 1: Configuración de Express Server y base de datos PSQL
- Estructura del proyecto
- Configuración básica Express
- Conectando al lado del cliente
axios vs react-router vs express router
¿Por qué no usar un ORM como Sequelize?
- Configurando la base de datos
Claves externas de PSQL
Shell de PSQL
- Configuración de Express Routes y consultas PSQL
Sección 2: Configuración del front-end de React
- Configurando un estado global con reductores, acciones y contexto.
Guardar datos de perfil de usuario en nuestra base de datos
Configuración de acciones y reductores
- Aplicación React del lado del cliente
addpost.js
editpost.js
posts.js
showpost.js
profile.js
showuser.js
Sección 3: Aplicación de administración
- Autenticación de la aplicación de administrador
- Privilegios globales de edición y eliminación
- Panel de administración
- Eliminar usuarios junto con sus publicaciones y comentarios
Estructura del proyecto
Comenzaremos discutiendo la estructura del directorio. Tendremos 2 directorios, el directorio Cliente y Servidor . El directorio de clientes contendrá el contenido de nuestra aplicación React que configuramos en el último tutorial y el servidor contendrá el contenido de nuestro express
servidor y mantendrá la lógica de nuestras llamadas API a nuestra base de datos. El directorio del servidor también mantendrá nuestro esquema de nuestra base de datos SQL .
La estructura del directorio final se verá así.

Configuración básica de Express
Si aún no lo ha hecho, puede instalar el express-generator
con el comando:
npm install -g express-generator
Esta es una herramienta simple que generará un proyecto express básico con un comando simple, similar a create-react-app
. Nos ahorrará un poco de tiempo de tener que configurar todo desde cero.
Podemos comenzar ejecutando el express
comando en el directorio del servidor . Esto nos dará una aplicación express predeterminada, pero no usaremos la configuración predeterminada, tendremos que modificarla.
Primero eliminemos la carpeta de rutas , la carpeta de vistas y la carpeta pública . No los necesitaremos. Debería tener solo 3 archivos restantes. El archivo www en el directorio bin , el app.js
archivo y el package.json
archivo. Si eliminó accidentalmente alguno de estos archivos, simplemente genere otro proyecto rápido. Dado que eliminamos esas carpetas, también tendremos que modificar un poco el código. Refactorice su app.js
archivo de la siguiente manera:
var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var app = express(); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); module.exports = app;
También podemos colocarlo app.js
en una carpeta llamada main .
A continuación, debemos cambiar el puerto predeterminado en el archivo www a otro que no sea el puerto 3000, ya que este es el puerto predeterminado en el que se ejecutará nuestra aplicación de interfaz de usuario React.
/** * Get port from environment and store in Express. */ var port = normalizePort(process.env.PORT || '5000'); app.set('port', port);
Además de las dependencias que obtuvimos al generar la aplicación express, también agregaremos 3 bibliotecas más para ayudarnos:
cors
: esta es la biblioteca que usaremos para ayudar a la comunicación entre la aplicación React y el servidor Express. Haremos esto a través de un proxy en la aplicación React. Sin esto, recibiríamos un error de recurso de origen cruzado en el navegador.
helmet
: Una biblioteca de seguridad que actualiza los encabezados http. Esta biblioteca hará que nuestras solicitudes http sean más seguras.
pg
: Esta es la biblioteca principal que usaremos para comunicarnos con nuestra base de datos psql. Sin esta biblioteca, la comunicación con la base de datos no será posible.
podemos seguir adelante e instalar estas bibliotecas
npm install pg helmet cors
Hemos terminado de configurar nuestro servidor mínimo y deberíamos tener una estructura de proyecto que se vea así.

Ahora podemos probar para ver si nuestro servidor está funcionando. Ejecuta el servidor sin una aplicación del lado del cliente . Express es una aplicación en pleno funcionamiento y se ejecutará independientemente de una aplicación del lado del cliente . Si lo hace correctamente, debería ver esto en su terminal.

Podemos mantener el servidor en funcionamiento porque lo usaremos en breve.
Conexión con el lado del cliente
Conectar nuestra aplicación del lado del cliente a nuestro servidor es muy fácil y solo necesitamos una línea de código. Vaya a su package.json
archivo en su directorio de clientes e ingrese lo siguiente:
“proxy”: “//localhost:5000"
¡Y eso es! Nuestro cliente ahora puede comunicarse con nuestro servidor a través de un proxy.
** Nota: Recuerde que si configura otro puerto además del puerto: 5000 en el www
archivo, use ese puerto en el proxy.
Aquí hay un diagrama para desglosar y explicar qué está sucediendo y cómo funciona.

Nuestro localhost: 3000 está esencialmente haciendo solicitudes como si fuera localhost: 5000 a través de un intermediario proxy que es lo que permite que nuestro servidor se comunique con nuestro cliente .
Nuestro lado del cliente ahora está conectado a nuestro servidor y ahora queremos probar nuestra aplicación.
Ahora tenemos que volver al lado del servidor y configurar el express
enrutamiento. En su carpeta principal en el directorio del servidor , cree un nuevo archivo llamado routes.js
. Este archivo contendrá todas las express
rutas. que nos permiten enviar datos a nuestra aplicación del lado del cliente . Podemos establecer una ruta muy simple por ahora:
var express = require('express') var router = express.Router() router.get('/api/hello', (req, res) => { res.json('hello world') }) module.exports = router
Básicamente, si se realiza una llamada API a la /hello
ruta, nuestro servidor Express responderá con una cadena de "hola mundo" en formato json.
También tenemos que refactorizar nuestro app.js
archivo para usar las rutas rápidas.
var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var indexRouter = require('./routes') var app = express(); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter) module.exports = app;
Ahora para nuestro código del lado del cliente en nuestro home.js
componente:
import React, { useState, useEffect } from 'react' import axios from 'axios'; const Home = props => { useEffect(() => { axios.get('/api/hello') .then(res => setState(res.data)) }, []) const [state, setState] = useState('') return( Home {state}
) }; export default Home;
Estamos haciendo una axios
solicitud de obtención básica a nuestro express
servidor en ejecución , si funciona, deberíamos ver "hola mundo" en la pantalla.
Y sí, está funcionando, ¡hemos configurado con éxito una aplicación React Node Fullstack!

Antes de continuar me gustaría para hacer frente a un par de preguntas que pueda tener, que es lo que es la diferencia entre axios
, react router
y express router
y por qué no estoy usando un ORM como Sequelize .
Axios vs Express Router vs React Router
TLDR; Usamos react router
para navegar dentro de nuestra aplicación, usamos axios
para comunicarnos con nuestro express
servidor y usamos nuestro express
servidor para comunicarnos con nuestra base de datos.
Quizás se esté preguntando en este punto cómo estas 3 bibliotecas funcionan juntas. Usamos axios
para comunicarnos con nuestro express
servidor backend, significaremos una llamada a nuestro express
servidor al incluir "/ api /" en el URI. axios
también se puede utilizar para realizar solicitudes http directas a cualquier punto final de backend. Sin embargo, por razones de seguridad, no se recomienda realizar solicitudes desde el cliente a la base de datos.
express router
se utiliza principalmente para comunicarnos con nuestra base de datos, ya que podemos pasar consultas SQL en el cuerpo de la express router
función. express
junto con Node se utiliza para ejecutar código fuera del navegador, que es lo que hace posibles las consultas SQL. express
también es una forma más segura de realizar solicitudes http en lugar de axios.
Sin embargo, necesitamos axios
en el lado del cliente React para manejar las solicitudes http asincrónicas, obviamente no podemos usar express router
en nuestro lado del cliente React. axios
se basa en promesas , por lo que también puede manejar automáticamente acciones asincrónicas.
Usamos react-router
para navegar dentro de nuestra aplicación, dado que React es una aplicación de una sola página, el navegador no se recarga con un cambio de página. Nuestra aplicación tiene tecnología detrás de escena que sabrá automáticamente si estamos solicitando una ruta a través de express
o react-router
.
¿Por qué no utilizar una biblioteca ORM como Sequelize?
TLDR; Preferencia por trabajar directamente con SQL que permite más control que ORM. Más recursos de aprendizaje para SQL que un ORM. Las habilidades de ORM no son transferibles, las habilidades de SQL son muy transferibles.
Hay muchos tutoriales que muestran cómo implementar una biblioteca ORM en uso con una base de datos SQL. No hay nada de malo en esto, pero personalmente prefiero interactuar directamente con SQL. Trabajar directamente con SQL le brinda un control más detallado sobre el código y creo que vale la pena el ligero aumento de dificultad cuando se trabaja directamente con SQL.
Hay muchos más recursos en SQL que en cualquier biblioteca ORM determinada, por lo que si tiene una pregunta o un error, es mucho más fácil encontrar una solución.
Además, está agregando otra dependencia y nivel de abstracción con una biblioteca ORM que podría causar errores en el futuro. Si usa un ORM, deberá realizar un seguimiento de las actualizaciones y los cambios importantes cuando se cambie la biblioteca. SQL, por otro lado, es extremadamente maduro y ha existido durante décadas, lo que significa que no es probable que tenga muchos cambios importantes. SQL también ha tenido tiempo de refinarse y perfeccionarse, lo que generalmente no es el caso de las bibliotecas ORM.
Por último, una biblioteca ORM requiere tiempo para aprender y el conocimiento generalmente no es transferible a ninguna otra cosa. SQL es el lenguaje de base de datos más utilizado por un margen muy amplio (la última vez que verifiqué alrededor del 90% de las bases de datos comerciales usaban SQL). Aprender un sistema SQL como PSQL le permitirá transferir directamente esas habilidades y conocimientos a otro sistema SQL como MySQL.
Esas son mis razones para no usar una biblioteca ORM.
Configurar la base de datos
Comencemos por configurar el esquema SQL creando un archivo en la carpeta principal del directorio del servidor llamado schema.sql.
Esto mantendrá la forma y estructura de la base de datos. Para configurar realmente la base de datos, por supuesto, tendrá que ingresar estos comandos en el shell de PSQL. El simple hecho de tener un archivo SQL aquí en nuestro proyecto no hace nada , es simplemente una manera de hacer referencia a cómo se ve nuestra estructura de base de datos y permitir que otros ingenieros tengan acceso a nuestros comandos SQL si quieren usar nuestro código.
Pero para tener una base de datos en funcionamiento, ingresaremos estos mismos comandos en la terminal PSQL.
CREATE TABLE users ( uid SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE, email VARCHAR(255), email_verified BOOLEAN, date_created DATE, last_login DATE ); CREATE TABLE posts ( pid SERIAL PRIMARY KEY, title VARCHAR(255), body VARCHAR, user_id INT REFERENCES users(uid), author VARCHAR REFERENCES users(username), date_created TIMESTAMP like_user_id INT[] DEFAULT ARRAY[]::INT[], likes INT DEFAULT 0 ); CREATE TABLE comments ( cid SERIAL PRIMARY KEY, comment VARCHAR(255), author VARCHAR REFERENCES users(username), user_id INT REFERENCES users(uid), post_id INT REFERENCES posts(pid), date_created TIMESTAMP );
Así que aquí tenemos 3 tablas que contendrán datos para nuestros usuarios, publicaciones y comentarios. De acuerdo con la convención SQL, todo el texto en minúsculas son nombres de columnas o tablas definidos por el usuario, y todo el texto en mayúsculas son comandos SQL.
CLAVE PRIMARIA : número único generado por psql para una columna determinada
VARCHAR (255) : carácter variable o texto y números. 255 establece la longitud de la fila.
BOOLEAN : Verdadero o falso
REFERENCIAS : cómo configurar la clave externa. La clave externa es una clave principal en otra tabla. Explico esto con más detalle a continuación.
ÚNICO : Evita entradas duplicadas en una columna.
DEFAULT : establece un valor predeterminado
INT [] DEFAULT ARRAY [] :: INT [] : este es un comando de aspecto bastante complejo, pero es bastante simple. Primero tenemos una matriz de enteros, luego establecemos esa matriz de enteros en un valor predeterminado de una matriz vacía de tipo matriz de enteros.
Tabla de usuarios
Tenemos una tabla muy básica para los usuarios , la mayoría de estos datos provendrán de auth0, que veremos más en la sección authcheck .
Tabla de publicaciones
A continuación tenemos la tabla de publicaciones. Obtendremos nuestro título y cuerpo del front-end de React y también asociamos cada publicación con un user_id
y y username
. Asociamos cada publicación con un usuario con la clave externa de SQL.
También tenemos nuestra matriz de like_user_id
, esto contendrá todos los identificadores de usuario de las personas a las que les ha gustado una publicación, lo que evita que el mismo usuario tenga varios Me gusta.
Tabla de comentarios
Finalmente tenemos nuestra tabla de comentarios. Obtendremos nuestro comentario del front-end de reacción y también asociaremos a cada usuario con un comentario, por lo que usaremos el campo user id
y username
de nuestra tabla de usuarios . Y también necesitamos el post id
de nuestra tabla de publicaciones, ya que se hace un comentario en una publicación, un comentario no existe de forma aislada. Por lo tanto, cada comentario debe estar asociado tanto con un usuario como con una publicación .
Claves externas PSQL
Una clave externa es esencialmente un campo o columna en otra tabla a la que hace referencia la tabla original. Una clave externa generalmente hace referencia a una clave primaria en otra tabla, pero como puede ver nuestra tabla de publicaciones, también tiene un enlace de clave externa a la username
que necesitamos por razones obvias. Para garantizar la integridad de los datos, puede usar la UNIQUE
restricción en el username
campo que le permite funcionar como una clave externa.
El uso de una columna en una tabla que hace referencia a una columna en una tabla diferente es lo que nos permite tener relaciones entre las tablas en nuestra base de datos, por lo que las bases de datos SQL se denominan "bases de datos relacionales".
La sintaxis que usamos es:
column_name data_type REFERENCES other_table(column_name_in_other_table)
Por lo tanto, una sola fila en la user_id
columna de nuestra tabla de publicaciones tendrá que coincidir con una sola fila en la uid
columna de la tabla de usuarios . Esto nos permitirá hacer cosas como buscar todas las publicaciones que hizo un determinado usuario o buscar todos los comentarios asociados con una publicación.
Restricción de clave externa
También deberá tener en cuenta las restricciones de clave externa de PSQL. Que son restricciones que le impiden eliminar filas a las que hace referencia otra tabla.
Un ejemplo simple es eliminar publicaciones sin eliminar los comentarios asociados con esa publicación . El ID de publicación de la tabla de publicaciones es una clave externa en la tabla de comentarios y se usa para establecer una relación entre las tablas .
No puede simplemente eliminar la publicación sin eliminar primero los comentarios porque luego tendrá un montón de comentarios en su base de datos con una clave externa de identificación de publicación inexistente .
A continuación, se muestra un ejemplo que muestra cómo eliminar un usuario y sus publicaciones y comentarios.

Shell de PSQL
Vamos a abrir la carcasa PSQL y entran en estos comandos que acabamos de crear aquí en nuestro schema.sql
archivo. Este shell de PSQL debería haberse instalado automáticamente cuando instaló PSQL . Si no, simplemente vaya al sitio web de PSQL para descargarlo e instalarlo nuevamente.
Si inicia sesión por primera vez en el shell de PSQL, se le pedirá que configure el servidor, el nombre de la base de datos, el puerto, el nombre de usuario y la contraseña. Deje el puerto en el 5432 predeterminado y configure el resto de las credenciales en lo que desee.
Así que ahora debería estar viendo postgres#
en la terminal o lo que sea que establezca el nombre de la base de datos. Esto significa que estamos listos para comenzar a ingresar comandos SQL . En lugar de usar la base de datos predeterminada, creemos una nueva con el comando CREATE DATABASE database1
y luego conectemos a ella con \c database1
. Si se hace correctamente, debería ver el database#
.
Si desea una lista de todos los comandos, puede escribir help
o \?
en el shell de PSQL . Recuerde siempre terminar sus consultas SQL con ;
uno de los errores más comunes al trabajar con SQL.
De escuchar, podemos simplemente copiar y pegar nuestros comandos desde el schema.sql
archivo.
Para ver una lista de nuestras tablas, usamos el \dt
comando y debería verlo en la terminal.
¡Y hemos configurado correctamente la base de datos!
Ahora necesitamos conectar esta base de datos a nuestro servidor . Hacer esto es extremadamente simple. Podemos hacer esto haciendo uso de la pg
biblioteca. Instale la pg
biblioteca si aún no lo ha hecho y asegúrese de que está en el directorio del servidor, no queremos instalar esta biblioteca en nuestra aplicación React.
Cree un archivo separado llamado db.js
en el directorio principal y configúrelo de la siguiente manera:
const { Pool } = require('pg') const pool = new Pool({ user: 'postgres', host: 'localhost', database: 'postgres', password: '', post: 5432 }) module.exports = pool
Estas serán las mismas credenciales que configuró al configurar el shell de PSQL .
Y eso es todo, hemos configurado nuestra base de datos para usar con nuestro servidor. Ahora podemos comenzar a realizar consultas desde nuestro servidor express.
Configuración de Express Routes y consultas PSQL
Aquí está la configuración de las rutas y consultas. Necesitamos nuestras operaciones CRUD básicas para las publicaciones y comentarios. Todos estos valores provendrán de nuestra interfaz React que configuraremos a continuación.
var express = require('express') var router = express.Router() var pool = require('./db') /* POSTS ROUTES SECTION */ router.get('/api/get/allposts', (req, res, next ) => { pool.query(`SELECT * FROM posts ORDER BY date_created DESC`, (q_err, q_res) => { res.json(q_res.rows) }) }) router.get('/api/get/post', (req, res, next) => { const post_id = req.query.post_id pool.query(`SELECT * FROM posts WHERE pid=$1`, [ post_id ], (q_err, q_res) => { res.json(q_res.rows) }) } ) router.post('/api/post/posttodb', (req, res, next) => { const values = [ req.body.title, req.body.body, req.body.uid, req.body.username] pool.query(`INSERT INTO posts(title, body, user_id, author, date_created) VALUES($1, $2, $3, $4, NOW() )`, values, (q_err, q_res) => { if(q_err) return next(q_err); res.json(q_res.rows) }) }) router.put('/api/put/post', (req, res, next) => { const values = [ req.body.title, req.body.body, req.body.uid, req.body.pid, req.body.username] pool.query(`UPDATE posts SET title= $1, body=$2, user_id=$3, author=$5, date_created=NOW() WHERE pid = $4`, values, (q_err, q_res) => { console.log(q_res) console.log(q_err) }) }) router.delete('/api/delete/postcomments', (req, res, next) => { const post_id = req.body.post_id pool.query(`DELETE FROM comments WHERE post_id = $1`, [post_id], (q_err, q_res) => { res.json(q_res.rows) console.log(q_err) }) }) router.delete('/api/delete/post', (req, res, next) => { const post_id = req.body.post_id pool.query(`DELETE FROM posts WHERE pid = $1`, [ post_id ], (q_err, q_res) => { res.json(q_res.rows) console.log(q_err) }) }) router.put('/api/put/likes', (req, res, next) => { const uid = [req.body.uid] const post_id = String(req.body.post_id) const values = [ uid, post_id ] console.log(values) pool.query(`UPDATE posts SET like_user_id = like_user_id || $1, likes = likes + 1 WHERE NOT (like_user_id @> $1) AND pid = ($2)`, values, (q_err, q_res) => { if (q_err) return next(q_err); console.log(q_res) res.json(q_res.rows); }); }); /* COMMENTS ROUTES SECTION */ router.post('/api/post/commenttodb', (req, res, next) => { const values = [ req.body.comment, req.body.user_id, req.body.username, req.body.post_id] pool.query(`INSERT INTO comments(comment, user_id, author, post_id, date_created) VALUES($1, $2, $3, $4, NOW())`, values, (q_err, q_res ) => { res.json(q_res.rows) console.log(q_err) }) }) router.put('/api/put/commenttodb', (req, res, next) => { const values = [ req.body.comment, req.body.user_id, req.body.post_id, req.body.username, req.body.cid] pool.query(`UPDATE comments SET comment = $1, user_id = $2, post_id = $3, author = $4, date_created=NOW() WHERE cid=$5`, values, (q_err, q_res ) => { res.json(q_res.rows) console.log(q_err) }) }) router.delete('/api/delete/comment', (req, res, next) => { const cid = req.body.comment_id console.log(cid) pool.query(`DELETE FROM comments WHERE cid=$1`, [ cid ], (q_err, q_res ) => { res.json(q_res) console.log(q_err) }) }) router.get('/api/get/allpostcomments', (req, res, next) => { const post_id = String(req.query.post_id) pool.query(`SELECT * FROM comments WHERE post_id=$1`, [ post_id ], (q_err, q_res ) => { res.json(q_res.rows) }) }) /* USER PROFILE SECTION */ router.post('/api/posts/userprofiletodb', (req, res, next) => { const values = [req.body.profile.nickname, req.body.profile.email, req.body.profile.email_verified] pool.query(`INSERT INTO users(username, email, email_verified, date_created) VALUES($1, $2, $3, NOW()) ON CONFLICT DO NOTHING`, values, (q_err, q_res) => { res.json(q_res.rows) }) } ) router.get('/api/get/userprofilefromdb', (req, res, next) => { const email = req.query.email console.log(email) pool.query(`SELECT * FROM users WHERE email=$1`, [ email ], (q_err, q_res) => { res.json(q_res.rows) }) } ) router.get('/api/get/userposts', (req, res, next) => { const user_id = req.query.user_id console.log(user_id) pool.query(`SELECT * FROM posts WHERE user_id=$1`, [ user_id ], (q_err, q_res) => { res.json(q_res.rows) }) } ) // Retrieve another users profile from db based on username router.get('/api/get/otheruserprofilefromdb', (req, res, next) => { // const email = [ "%" + req.query.email + "%"] const username = String(req.query.username) pool.query(`SELECT * FROM users WHERE username = $1`, [ username ], (q_err, q_res) => { res.json(q_res.rows) }); }); //Get another user's posts based on username router.get('/api/get/otheruserposts', (req, res, next) => { const username = String(req.query.username) pool.query(`SELECT * FROM posts WHERE author = $1`, [ username ], (q_err, q_res) => { res.json(q_res.rows) }); }); module.exports = router
Comandos SQL
SELECT * FROM table
: Cómo obtenemos datos de la base de datos. devuelve todas las filas de una tabla.
INSERT INTO table(column1, column2)
: Cómo guardamos datos y agregamos filas a la base de datos.
UPDATE table SET column1 =$1, column2 = $2
: cómo actualizar o modificar filas existentes en una base de datos. La WHERE
cláusula especifica qué filas actualizar.
DELETE FROM table
: elimina filas según las condiciones de la WHERE
cláusula. PRECAUCIÓN : no incluir una WHERE
cláusula elimina toda la tabla.
WHERE
cláusula: Una declaración condicional opcional para agregar a las consultas. Esto funciona de manera similar a una if
declaración en javascript.
WHERE (array @> value)
: Si el valor está contenido en la matriz.
Rutas Express
Para configurar rutas rápidas , primero usamos el router
objeto que definimos en la parte superior con express.Router()
. Luego, el método http que queremos, que pueden ser los métodos estándar como GET, POST, PUT, etc.
Luego, entre paréntesis, primero pasamos la cadena de la ruta que queremos y el segundo argumento es una función para ejecutar cuando la ruta es llamada desde el cliente , Express escucha estas llamadas de ruta desde el cliente automáticamente. Cuando las rutas coinciden, se llama a la función en el cuerpo, que en nuestro caso son consultas PSQL .
También podemos pasar parámetros dentro de nuestra llamada de función. Usamos req, res y next .
req: es la abreviatura de request y contiene los datos de la solicitud de nuestro cliente. Básicamente, así es como obtenemos datos de nuestro front-end a nuestro servidor. Los datos de nuestra interfaz React están contenidos en este objeto req y los usamos aquí en nuestras rutas ampliamente para acceder a los valores. Los datos se proporcionarán a axios como un parámetro como un objeto javascript.
Para las solicitudes GET con un parámetro opcional, los datos estarán disponibles con req.query . Para las solicitudes PUT, POST y DELETE, los datos estarán disponibles directamente en el cuerpo de la solicitud con req.body . Los datos serán un objeto javascript y se puede acceder a cada propiedad con notación de puntos regular.
res: es la abreviatura de respuesta y contiene la respuesta expresa del servidor . Queremos enviar la respuesta que obtenemos de nuestra base de datos al cliente, por lo que pasamos la respuesta de la base de datos a esta función res que luego la envía a nuestro cliente.
siguiente: es un middleware que le permite pasar devoluciones de llamada a la siguiente función.
Observe que dentro de nuestra ruta rápida estamos haciendo pool.query
y este pool
objeto es el mismo que contiene las credenciales de inicio de sesión de nuestra base de datos que configuramos previamente e importamos en la parte superior. La función de consulta nos permite realizar consultas SQL a nuestra base de datos en formato de cadena. También observe que estoy usando `` no citas, lo que me permite tener mi consulta en varias líneas.
Luego tenemos una coma después de nuestra consulta SQL y el siguiente parámetro, que es una función de flecha para ejecutar después de ejecutar la consulta . que primero desaparecen de 2 parámetros a nuestra función de flecha, q_err
y q_res
que significa que el error de la consulta y la respuesta de la consulta . Para enviar datos a la interfaz , pasamos q_res.rows
a la res.json
función. q_res.rows
es la respuesta de la base de datos ya que es SQL y la base de datos nos devolverá filas coincidentes según nuestra consulta. Luego convertimos esas filas al formato json y lo enviamos a nuestra interfaz con el res
parámetro.
También podemos pasar valores opcionales a nuestras consultas SQL pasando una matriz después de la consulta separada por una coma. Luego, podemos acceder a los elementos individuales de esa matriz en la consulta SQL con la sintaxis $1
donde $1
está el primer elemento de la matriz. $2
accedería al segundo elemento de la matriz y así sucesivamente. Tenga en cuenta que no es un sistema basado en 0 como en javascript, no hay$0
Analicemos cada una de estas rutas y describamos brevemente cada una.
Publicaciones Rutas
- / api / get / allposts: recupera todas nuestras publicaciones de la base de datos.
ORDER BY date_created DESC
nos permite mostrar primero las publicaciones más recientes. - / api / post / posttodb: guarda una publicación de usuario en la base de datos. Guardamos los 4 valores que necesitamos: título, cuerpo, identificación de usuario, nombre de usuario en una matriz de valores.
- / api / put / post: edita una publicación existente en la base de datos. Usamos el
UPDATE
comando SQL y pasamos todos los valores de la publicación nuevamente. Buscamos la publicación con la identificación de la publicación que obtenemos de nuestra interfaz. - / api / delete / postcomments: elimina todos los comentarios asociados con una publicación. Debido a la restricción de clave externa de PSQL , tenemos que eliminar todos los comentarios asociados con la publicación antes de poder eliminar la publicación real.
- / api / delete / post: elimina una publicación con la identificación de la publicación.
- / api / put / likes : Hacemos una solicitud put para agregar la identificación de usuario del usuario al que le gustó la publicación a la
like_user_id
matriz y luego aumentamos ellikes
recuento en 1.
Comentarios Rutas
- / api / post / commenttodb: guarda un comentario en la base de datos
- / api / put / commenttodb: edita un comentario existente en la base de datos
- / api / delete / comment: Elimina un solo comentario, esto es diferente a eliminar todos los comentarios asociados con una publicación.
- / api / get / allpostcomments: recupera todos los comentarios asociados con una sola publicación
Rutas de usuario
- / api / posts / userprofiletodb: Guarda los datos de un perfil de usuario de auth0 en nuestra propia base de datos. Si el usuario ya existe, PostgreSQL no hace nada.
- / api / get / userprofilefromdb: recupera a un usuario buscando su correo electrónico
- / api / get / userposts: recupera las publicaciones realizadas por un usuario buscando todas las publicaciones que coincidan con su identificación de usuario.
- / api / get / otheruserprofilefromdb: obtenga los datos del perfil de otro usuario de la base de datos y visualícelos en su página de perfil.
- / api / get / otheruserposts: obtenga publicaciones de otros usuarios cuando vea su página de perfil
Configuración de estado global con Reductores, acciones y contexto.
Guardar datos de perfil de usuario en nuestra base de datos
Antes de que podamos comenzar a configurar el estado global, necesitamos una forma de guardar los datos de nuestro perfil de usuario en nuestra propia base de datos, actualmente solo estamos obteniendo nuestros datos de auth0. Haremos esto en nuestro authcheck.js
componente.
import React, { useEffect, useContext } from 'react'; import history from './history'; import Context from './context'; import axios from 'axios'; const AuthCheck = () => { const context = useContext(Context) useEffect(() => { if(context.authObj.isAuthenticated()) { const profile = context.authObj.userProfile context.handleUserLogin() context.handleUserAddProfile(profile) axios.post('/api/posts/userprofiletodb', profile ) .then(axios.get('/api/get/userprofilefromdb', {params: {email: profile.profile.email}}) .then(res => context.handleAddDBProfile(res.data)) ) .then(history.replace('/') ) } else { context.handleUserLogout() context.handleUserRemoveProfile() context.handleUserRemoveProfile() history.replace('/') } }, [context.authObj.userProfile, context]) return( )} export default AuthCheck;
Configuramos la mayor parte de este componente en el último tutorial, así que recomiendo ver ese tutorial para obtener una explicación detallada, pero aquí estamos haciendo una solicitud de publicación de axios seguida inmediatamente por otra solicitud de obtención de axios para obtener inmediatamente los datos del perfil de usuario que acabamos de guardar en la base de datos.
Hacemos esto porque necesitamos la identificación de clave primaria única que es generada por nuestra base de datos y esto nos permite asociar este usuario con sus comentarios y publicaciones . Y usamos el correo electrónico de los usuarios para buscarlos, ya que no sabemos cuál es su identificación única cuando se registran por primera vez. Finalmente, guardamos los datos del perfil de usuario de la base de datos en nuestro estado global.
* Tenga en cuenta que esto se aplica también a los inicios de sesión de OAuth, como los de Google y Facebook.
Acciones y reductores
Ahora podemos comenzar a configurar las acciones y los reductores junto con el contexto para configurar el estado global de esta aplicación.
Para configurar el contexto desde cero, consulte mi tutorial anterior. Aquí solo necesitaremos el estado para el perfil de la base de datos y todas las publicaciones.
Primero nuestros tipos de acción
export const SET_DB_PROFILE = "SET_DB_PROFILE" export const REMOVE_DB_PROFILE = "REMOVE_DB_PROFILE" export const FETCH_DB_POSTS = "FETCH_DB_POSTS" export const REMOVE_DB_POSTS = "REMOVE_DB_POSTS"
Ahora nuestras acciones
export const set_db_profile = (profile) => { return { type: ACTION_TYPES.SET_DB_PROFILE, payload: profile } } export const remove_db_profile = () => { return { type: ACTION_TYPES.REMOVE_DB_PROFILE } } export const set_db_posts = (posts) => { return { type: ACTION_TYPES.FETCH_DB_POSTS, payload: posts } } export const remove_db_posts = () => { return { type: ACTION_TYPES.REMOVE_DB_POSTS } }
Finalmente nuestro reductor de publicaciones y reductor de autenticación
import * as ACTION_TYPES from '../actions/action_types' export const initialState = { posts: null, } export const PostsReducer = (state = initialState, action) => { switch(action.type) { case ACTION_TYPES.FETCH_DB_POSTS: return { ...state, posts: action.payload } case ACTION_TYPES.REMOVE_DB_POSTS: return { ...state, posts: [] } default: return state } }
import * as ACTION_TYPES from '../actions/action_types' export const initialState = { is_authenticated: false, db_profile: null, profile: null, } export const AuthReducer = (state = initialState, action) => { switch(action.type) { case ACTION_TYPES.LOGIN_SUCCESS: return { ...state, is_authenticated: true } case ACTION_TYPES.LOGIN_FAILURE: return { ...state, is_authenticated: false } case ACTION_TYPES.ADD_PROFILE: return { ...state, profile: action.payload } case ACTION_TYPES.REMOVE_PROFILE: return { ...state, profile: null } case ACTION_TYPES.SET_DB_PROFILE: return { ...state, db_profile: action.payload } case ACTION_TYPES.REMOVE_DB_PROFILE: return { ...state, db_profile: null } default: return state } }
Ahora tenemos que agregarlos al
... /* Posts Reducer */ const [statePosts, dispatchPosts] = useReducer(PostsReducer.PostsReducer, PostsReducer.initialState) const handleSetPosts = (posts) => { dispatchPosts(ACTIONS.set_db_posts(posts) ) } const handleRemovePosts = () => { dispatchPosts(ACTIONS.remove_db_posts() ) } ... /* Auth Reducer */ const [stateAuth, dispatchAuth] = useReducer(AuthReducer.AuthReducer, AuthReducer.initialState) const handleDBProfile = (profile) => { dispatchAuth(ACTIONS.set_db_profile(profile)) } const handleRemoveDBProfile = () => { dispatchAuth(ACTIONS.remove_db_profile()) } ... handleDBProfile(profile), handleRemoveDBProfile: () => handleRemoveDBProfile(), //Posts State postsState: statePostsReducer.posts, handleAddPosts: (posts) => handleSetPosts(posts), handleRemovePosts: () => handleRemovePosts(), ... }}> ...
Esto es todo, ahora estamos listos para usar este estado global en nuestros componentes.
Aplicación React del lado del cliente
A continuación, configuraremos el blog de reacción del lado del cliente. Todas las llamadas a la API en esta sección se configuraron en la sección anterior de rutas rápidas.
Se configurará en 6 componentes de la siguiente manera.
addpost.js : un componente con un formulario para enviar publicaciones.
editpost.js : un componente para editar publicaciones con un formulario que ya tiene campos llenos .
posts.js : un componente para representar todas las publicaciones, como en un foro típico.
showpost.js : un componente para representar una publicación individual después de que un usuario haya hecho clic en una publicación.
profile.js : un componente que muestra las publicaciones asociadas con un usuario. El panel de usuario.
showuser.js : un componente que muestra los datos y publicaciones del perfil de otro usuario.
¿Por qué no usar Redux Form?
TDLR; Redux Form es excesivo para la mayoría de los casos de uso.
Redux Form es una biblioteca popular que se usa comúnmente en las aplicaciones React. Entonces, ¿por qué no usarlo aquí? Probé Redux Form, pero simplemente no pude encontrar un caso de uso aquí. Siempre tenemos que tener en cuenta el uso final, y no se me ocurrió un escenario para esta aplicación en el que tendríamos que guardar los datos del formulario en el estado global redux.
En esta aplicación, simplemente tomamos los datos de un formulario regular y se los pasamos a Axios, que luego los pasa al servidor exprés que finalmente los guarda en la base de datos. El otro caso de uso posible es para un componente de publicación de edición, que manejo pasando los datos de la publicación a una propiedad del elemento Enlace.
Pruebe Redux Form y vea si puede encontrar un uso inteligente para él, pero no lo necesitaremos en esta aplicación. Además, cualquier funcionalidad ofrecida por Redux Form puede lograrse relativamente más fácilmente sin ella.
La forma Redux es simplemente excesiva para la mayoría de los casos de uso.
Al igual que con un ORM, no hay razón para agregar otra capa innecesaria de complejidad a nuestra aplicación.
Es más fácil configurar formularios con React normal.
addpost.js
import React, { useContext} from 'react'; import axios from 'axios'; import history from '../utils/history'; import Context from '../utils/context'; import TextField from '@material-ui/core/TextField'; const AddPost = () => { const context = useContext(Context) const handleSubmit = (event) => { event.preventDefault() const user_id = context.dbProfileState[0].uid const username = context.dbProfileState[0].username const data = {title: event.target.title.value, body: event.target.body.value, username: username, uid: user_id} axios.post('/api/post/posttodb', data) .then(response => console.log(response)) .catch((err) => console.log(err)) .then(setTimeout(() => history.replace('/'), 700) ) } return(
Submit
history.replace('/posts')}> Cancel )} export default AddPost;
En el componente addpost tenemos un formulario simple de 2 campos donde un usuario puede ingresar un título y un cuerpo. El formulario se envía utilizando la handlesubmit()
función que creamos. la handleSubmit()
función toma una palabra clave de parámetro de evento que contiene los datos del formulario enviados por el usuario.
Usaremos event.preventDefault()
para detener la recarga de la página ya que React es una aplicación de una sola página y eso sería innecesario.
El método axios post toma un parámetro de "datos" que se utilizará para contener los datos que se almacenarán en la base de datos. Obtenemos el nombre de usuario y el user_id del estado global que discutimos en la última sección.
En realidad, la publicación de los datos en la base de datos se maneja en la función de rutas rápidas con consultas SQL que vimos antes. Nuestra llamada a la API de axios luego pasa los datos a nuestro servidor expreso que guardará la información en la base de datos.
editpost.js
A continuación tenemos nuestro editpost.js
componente. Este será un componente básico para editar las publicaciones de los usuarios. Solo será accesible a través de la página de perfil de los usuarios.
import React, { useContext, useState } from 'react'; import axios from 'axios'; import history from '../utils/history'; import Context from '../utils/context'; import TextField from '@material-ui/core/TextField'; import Button from "@material-ui/core/Button"; const EditPost = (props) => { const context = useContext(Context) const [stateLocal, setState] = useState({ title: props.location.state.post.post.title, body: props.location.state.post.post.body }) const handleTitleChange = (event) => { setState({...stateLocal, title: event.target.value }) } const handleBodyChange = (event) => { setState({...stateLocal, body: event.target.value }) } const handleSubmit = (event) => { event.preventDefault() const user_id = context.dbProfileState[0].uid const username = context.dbProfileState[0].username const pid = props.location.state.post.post.pid const title = event.target.title.value const body = event.target.body.value const data = {title: title, body: body, pid: pid, uid: user_id, username: username } axios.put("/api/put/post", data) .then(res => console.log(res)) .catch(err => console.log(err)) .then(setTimeout(() => history.replace('/profile'), 700 )) } return(
Submit
history.goBack()}> Cancel )} export default EditPost;
props.location.state.posts.posts.title
: es una funcionalidad ofrecida por react-router . Cuando un usuario hace clic en una publicación desde su página de perfil, los datos de la publicación en los que hicieron clic se guardan en una propiedad estatal en el elemento de enlace y esto es diferente del estado del componente local en React from the useState
hook.
Este enfoque nos ofrece una forma más fácil de guardar los datos en comparación con el contexto y también nos ahorra una solicitud de API. Veremos cómo funciona esto en el profile.js
componente.
Después de esto, tenemos un formulario de componente controlado básico y guardamos los datos en cada pulsación de tecla en el estado React.
En nuestra handleSubmit()
función combinamos todos nuestros datos antes de enviarlos a nuestro servidor en una petición put axios.
posts.js
import React, { useContext, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import axios from 'axios'; import moment from 'moment'; import Context from '../utils/context'; import Button from '@material-ui/core/Button'; import TextField from '@material-ui/core/TextField'; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import '../App.css'; import '../styles/pagination.css'; const Posts = (props) => { const context = useContext(Context) const [stateLocal, setState] = useState({ posts: [], fetched: false, first_page_load: false, pages_slice: [1, 2, 3, 4, 5], max_page: null, items_per_page: 3, currentPage: 1, num_posts: null, posts_slice: null, posts_search: [], posts_per_page: 3 }) useEffect(() => { if(!context.postsState) { axios.get('/api/get/allposts') .then(res => context.handleAddPosts(res.data) ) .catch((err) => console.log(err)) } if (context.postsState && !stateLocal.fetched) { const indexOfLastPost = 1 * stateLocal.posts_per_page const indexOfFirstPost = indexOfLastPost - stateLocal.posts_per_page const last_page = Math.ceil(context.postsState.length/stateLocal.posts_per_page) setState({...stateLocal, fetched: true, posts: [...context.postsState], num_posts: context.postsState.length, max_page: last_page, posts_slice: context.postsState.slice(indexOfFirstPost, indexOfLastPost) }) } }, [context, stateLocal]) useEffect(() => { let page = stateLocal.currentPage let indexOfLastPost = page * 3; let indexOfFirstPost = indexOfLastPost - 3; setState({...stateLocal, posts_slice: stateLocal.posts.slice(indexOfFirstPost, indexOfLastPost) }) }, [stateLocal.currentPage]) //eslint-disable-line const add_search_posts_to_state = (posts) => { setState({...stateLocal, posts_search: []}); setState({...stateLocal, posts_search: [...posts]}); } const handleSearch = (event) => { setState({...stateLocal, posts_search: []}); const search_query = event.target.value axios.get('/api/get/searchpost', {params: {search_query: search_query} }) .then(res => res.data.length !== 0 ? add_search_posts_to_state(res.data) : null ) .catch(function (error) { console.log(error); }) } const RenderPosts = post => ( thumb_up {post.post.likes} } />{post.post.body} ) const page_change = (page) => { window.scrollTo({top:0, left: 0, behavior: 'smooth'}) //variables for page change let next_page = page + 1 let prev_page = page - 1 //handles general page change //if(state.max_page 2 && page < stateLocal.max_page - 1) { setState({...stateLocal, currentPage: page, pages_slice: [prev_page - 1, prev_page, page, next_page, next_page + 1], }) } if(page === 2 ) { setState({...stateLocal, currentPage: page, pages_slice: [prev_page, page, next_page, next_page + 1, next_page + 2], }) } //handles use case for user to go back to first page from another page if(page === 1) { setState({...stateLocal, currentPage: page, pages_slice: [page, next_page, next_page + 1, next_page + 2, next_page + 3], }) } //handles last page change if(page === stateLocal.max_page) { setState({...stateLocal, currentPage: page, pages_slice: [prev_page - 3, prev_page - 2, prev_page - 1, prev_page, page], }) } if(page === stateLocal.max_page - 1) { setState({...stateLocal, currentPage: page, pages_slice: [prev_page - 2, prev_page - 1, prev_page, page, next_page], }) } } return(
{ context.authState ? Add Post : Sign Up to Add Post }
{stateLocal.posts_search ? stateLocal.posts_search.map(post => ) : null }
Posts
{stateLocal.posts_slice ? stateLocal.posts_slice.map(post => ) : null } page_change(1) }> First page_change(stateLocal.currentPage - 1) }> Prev {stateLocal.pages_slice.map((page) => page_change(page)} className={stateLocal.currentPage === page ? "pagination-active" : "pagination-item" } key={page}> {page} )} page_change(stateLocal.currentPage + 1)}> Next page_change(stateLocal.max_page)}> Last )} export default Posts;
Notará que tenemos una useEffect()
llamada bastante compleja para obtener nuestras publicaciones de nuestra base de datos. Esto se debe a que estamos guardando nuestras publicaciones de nuestra base de datos en el estado global, de modo que las publicaciones siguen ahí incluso si un usuario navega a otra página.
Hacer esto evita llamadas API innecesarias a nuestro servidor. Es por eso que usamos un condicional para verificar si las publicaciones ya están guardadas en el estado de contexto.
Si las publicaciones ya están guardadas en el estado global, simplemente establecemos las publicaciones en el estado global en nuestro estado local, lo que nos permite inicializar la paginación.
Paginación
Tenemos una implementación de paginación básica aquí en la page_change()
función. Básicamente, tenemos nuestros 5 bloques de paginación configurados como una matriz. Cuando la página cambia, la matriz se actualiza con los nuevos valores. Esto se ve en la primera if
declaración de la page_change()
función, las otras 4 if
declaraciones son solo para manejar los primeros 2 y los últimos 2 cambios de página.
También tenemos que hacer una window.scrollTo()
llamada para desplazarnos a la parte superior en cada cambio de página.
Ponte a prueba para ver si puedes construir una implementación de paginación más compleja, pero para nuestros propósitos, esta función única aquí para la paginación está bien.
necesitamos 4 valores de estado para nuestra paginación. Nosotros necesitamos:
num_posts
: número de publicacionesposts_slice
: una porción del total de publicacionescurrentPage
: la página actualposts_per_page
: El número de publicaciones en cada página.
También necesitamos pasar el currentPage
valor de estado al useEffect()
gancho, esto nos permite activar una función cada vez que cambia la página. Obtenemos el indexOfLastPost
multiplicando 3 por el currentPage
y obtenemos la indexOfFirstPost
publicación que queremos mostrar restando 3. Luego podemos establecer esta nueva matriz dividida como la nueva matriz en nuestro estado local.
Ahora para nuestro JSX. Estamos usando flexbox para estructurar y diseñar nuestros bloques de paginación en lugar de las listas horizontales habituales que se usan tradicionalmente.
Tenemos 4 botones que le permiten ir a la primera página o retroceder una página y viceversa. Luego usamos una declaración de mapa en nuestra pages_slice
matriz que nos da los valores para nuestros bloques de paginación. Un usuario también puede hacer clic en un bloque de paginación que pasará a la página como argumento de la page_change()
función.
También tenemos clases de CSS que nos permiten establecer estilos en nuestra paginación.
.pagination-active
: esta es una clase CSS normal en lugar de un pseudo selector que normalmente ve con listas horizontales como.item:active
. Estamos alternando la clase activa en React JSX comparandocurrentPage
con la página en lapages_slice
matriz..pagination-item
: estilo para todos los bloques de paginación.pagination-item:hover
: estilo para aplicar cuando el usuario se desplaza sobre un bloque de paginación
page_change(1) }> First page_change(stateLocal.currentPage - 1) }> Prev {stateLocal.pages_slice.map((page) => page_change(page)} className={stateLocal.currentPage === page ? "pagination-active" : "pagination-item" } key={page}> {page} )} page_change(stateLocal.currentPage + 1)}> Next page_change(stateLocal.max_page)}> Last
.pagination-active { background-color: blue; cursor: pointer; color: white; padding: 10px 15px; border: 1px solid #ddd; /* Gray */ } .pagination-item { cursor: pointer; border: 1px solid #ddd; /* Gray */ padding: 10px 15px; } .pagination-item:hover { background-color: #ddd }
RenderPosts
es el componente funcional que utilizamos para representar cada publicación individual. El título de las publicaciones es un
Link
que, al hacer clic en él, llevará al usuario a cada publicación individual con comentarios. También notará que pasamos en toda la publicación a la state
propiedad del Link
elemento. Esta state
propiedad es diferente de nuestro estado local, en realidad es una propiedad de react-router
y veremos esto con más detalle en el showpost.js
componente. También hacemos lo mismo con el autor de la publicación.
También notará algunas otras cosas relacionadas con la búsqueda de publicaciones que analizaré en las secciones posteriores.
También discutiré la funcionalidad "Me gusta" en el showpost.js
componente.
showpost.js
Ahora aquí tenemos, con mucho, el componente más complejo de esta aplicación. No se preocupe, lo desglosaré completamente paso a paso, no es tan intimidante como parece.
import React, { useContext, useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import axios from 'axios'; import history from '../utils/history'; import Context from '../utils/context'; import TextField from '@material-ui/core/TextField'; import Button from '@material-ui/core/Button'; const ShowPost = (props) => { const context = useContext(Context) const [stateLocal, setState] = useState({ comment: '', fetched: false, cid: 0, delete_comment_id: 0, edit_comment_id: 0, edit_comment: '', comments_arr: null, cur_user_id: null, like_post: true, likes: 0, like_user_ids: [], post_title: null, post_body: null, post_author: null, post_id: null }) useEffect(() => { if(props.location.state && !stateLocal.fetched) { setState({...stateLocal, fetched: true, likes: props.location.state.post.post.likes, like_user_ids: props.location.state.post.post.like_user_id, post_title: props.location.state.post.post.title, post_body: props.location.state.post.post.body, post_author: props.location.state.post.post.author, post_id: props.location.state.post.post.pid}) } }, [stateLocal, props.location]) useEffect( () => { if(!props.location.state && !stateLocal.fetched) { const post_id = props.location.pathname.substring(6) axios.get('/api/get/post', {params: {post_id: post_id}} ) .then(res => res.data.length !== 0 ? setState({...stateLocal, fetched: true, likes: res.data[0].likes, like_user_ids: res.data[0].like_user_id, post_title: res.data[0].title, post_body: res.data[0].body, post_author: res.data[0].author, post_id: res.data[0].pid }) : null ) .catch((err) => console.log(err) ) } }, [stateLocal, props.location]) useEffect(() => { if(!stateLocal.comments_arr) { if(props.location.state) { const post_id = props.location.pathname.substring(6) axios.get('/api/get/allpostcomments', {params: {post_id: post_id}} ) .then(res => res.data.length !== 0 ? setState({...stateLocal, comments_arr: [...res.data]}) : null ) .catch((err) => console.log(err)) } } }, [props.location, stateLocal]) const handleCommentSubmit = (submitted_comment) => { if(stateLocal.comments_arr) { setState({...stateLocal, comments_arr: [submitted_comment, ...stateLocal.comments_arr]}) } else { setState({...stateLocal, comments_arr: [submitted_comment]}) } }; const handleCommentUpdate = (comment) => { const commentIndex = stateLocal.comments_arr.findIndex(com => com.cid === comment.cid) var newArr = [...stateLocal.comments_arr ] newArr[commentIndex] = comment setTimeout(() => setState({...stateLocal, comments_arr: [...newArr], edit_comment_id: 0 }), 100) }; const handleCommentDelete = (cid) => { setState({...stateLocal, delete_comment_id: cid}) const newArr = stateLocal.comments_arr.filter(com => com.cid !== cid) setState({...stateLocal, comments_arr: newArr}) }; const handleEditFormClose = () => { setState({...stateLocal, edit_comment_id: 0}) } const RenderComments = (props) => { return(
{props.comment.comment}
{ props.comment.date_created === 'Just Now' ? {props.comment.isEdited ? Edited : Just Now } : props.comment.date_created }By: { props.comment.author}
{props.cur_user_id === props.comment.user_id ? !props.isEditing ? setState({...stateLocal, edit_comment_id: props.comment.cid, edit_comment: props.comment.comment }) }> Edit : handleUpdate(event, props.comment.cid) }>Agree Cancel handleDeleteComment(props.comment.cid)}> Delete : null } ); } const handleEditCommentChange = (event) => ( setState({...stateLocal, edit_comment: event.target.value}) ); const handleSubmit = (event) => { event.preventDefault() setState({...stateLocal, comment: ''}) const comment = event.target.comment.value const user_id = context.dbProfileState[0].uid const username = context.dbProfileState[0].username const post_id = stateLocal.post_id const current_time = "Just Now" const temp_cid = Math.floor(Math.random() * 1000); const submitted_comment = {cid: temp_cid, comment: comment, user_id: user_id, author: username, date_created: current_time } const data = {comment: event.target.comment.value, post_id: post_id, user_id: user_id, username: username} axios.post('/api/post/commenttodb', data) .then(res => console.log(res)) .catch((err) => console.log(err)) window.scroll({top: 0, left: 0, behavior: 'smooth'}) handleCommentSubmit(submitted_comment) } const handleUpdate = (event, cid) => { event.preventDefault() console.log(event) console.log(cid) const comment = event.target.editted_comment.value const comment_id = cid const post_id = stateLocal.post_id const user_id = context.dbProfileState[0].uid const username = context.dbProfileState[0].username const isEdited = true const current_time = "Just Now" const edited_comment = {cid: comment_id, comment: comment, user_id: user_id, author: username, date_created: current_time, isEdited: isEdited } const data = {cid: comment_id, comment: comment, post_id: post_id, user_id: user_id, username: username} axios.put('/api/put/commenttodb', data) .then(res => console.log(res)) .catch((err) => console.log(err)) handleCommentUpdate(edited_comment); } const handleDeleteComment = (cid) => { const comment_id = cid console.log(cid) axios.delete('/api/delete/comment', {data: {comment_id: comment_id}} ) .then(res => console.log(res)) .catch((err) => console.log(err)) handleCommentDelete(cid) } const handleLikes = () => { const user_id = context.dbProfileState[0].uid const post_id = stateLocal.post_id const data = { uid: user_id, post_id: post_id } console.log(data) axios.put('/api/put/likes', data) .then( !stateLocal.like_user_ids.includes(user_id) && stateLocal.like_post ? setState({...stateLocal, likes: stateLocal.likes + 1, like_post: false}) : null ) .catch(err => console.log(err)) }; return(
Post
{stateLocal.comments_arr || props.location.state ?{stateLocal.post_title}
{stateLocal.post_body}
{stateLocal.post_author}
: null } handleLikes() : () => history.replace('/signup')}>thumb_up {stateLocal.likes}Comments:
{stateLocal.comments_arr ? stateLocal.comments_arr.map((comment) => ) : null }
{context.authState ? Submit : Signup to Comment } )} export default ShowPost;
Primero notará una useState
llamada gigantesca . Explicaré cómo funciona cada propiedad a medida que exploramos nuestro componente aquí de una vez.
useEffect () y solicitudes de API
Lo primero que debemos tener en cuenta es que un usuario puede acceder a una publicación de 2 formas diferentes. Accediendo a él desde el foro o navegando a él usando la URL directa .
useEffect(() => { if(props.location.state && !stateLocal.fetched) { setState({...stateLocal, fetched: true, likes: props.location.state.post.post.likes, like_user_ids: props.location.state.post.post.like_user_id, post_title: props.location.state.post.post.title, post_body: props.location.state.post.post.body, post_author: props.location.state.post.post.author, post_id: props.location.state.post.post.pid}) } }, [stateLocal, props.location]) useEffect( () => { if(!props.location.state && !stateLocal.fetched) { const post_id = props.location.pathname.substring(6) axios.get('/api/get/post', {params: {post_id: post_id}} ) .then(res => res.data.length !== 0 ? setState({...stateLocal, fetched: true, likes: res.data[0].likes, like_user_ids: res.data[0].like_user_id, post_title: res.data[0].title, post_body: res.data[0].body, post_author: res.data[0].author, post_id: res.data[0].pid }) : null ) .catch((err) => console.log(err) ) } }, [stateLocal, props.location]) useEffect(() => { if(!stateLocal.comments_arr) { if(props.location.state) { const post_id = props.location.pathname.substring(6) axios.get('/api/get/allpostcomments', {params: {post_id: post_id}} ) .then(res => res.data.length !== 0 ? setState({...stateLocal, comments_arr: [...res.data]}) : null ) .catch((err) => console.log(err)) } } }, [props.location, stateLocal])
Si acceden a él desde el foro, lo comprobamos en nuestra useEffect()
llamada y luego configuramos nuestro estado local en la publicación. Dado que usamos lastate
propiedad react router en el elemento, tenemos acceso a todos los datos de publicación que ya están disponibles para nosotros a través de accesorios, lo que nos ahorra una llamada API innecesaria.
Si el usuario ingresa la URL directa de una publicación en el navegador, entonces no tenemos más remedio que realizar una solicitud de API para obtener la publicación, ya que un usuario tiene que hacer clic en una publicación del posts.js
foro para guardar los datos de la publicación en la reacción. state
propiedad del enrutador .
Primero extraemos el ID de publicación de la URL con la pathname
propiedad react-router , que luego usamos como parámetro en nuestra solicitud axios . Después de la solicitud de API, simplemente guardamos la respuesta en nuestro estado local.
Después de eso, también necesitamos obtener los comentarios con una solicitud de API . Podemos usar el mismo método de extracción de URL de ID de publicación para buscar comentarios asociados con una publicación.
RenderComments y animaciones
Aquí tenemos nuestro componente funcional que usamos para mostrar un comentario individual.
.... const RenderComments = (props) => { return( {props.comment.comment}
{ props.comment.date_created === 'Just Now' ? {props.comment.isEdited ? Edited : Just Now } : props.comment.date_created } By: { props.comment.author}
{props.cur_user_id === props.comment.user_id ? !props.isEditing ? setState({...stateLocal, edit_comment_id: props.comment.cid, edit_comment: props.comment.comment }) }> Edit : handleUpdate(event, props.comment.cid) }>Agree Cancel handleDeleteComment(props.comment.cid)}> Delete : null } ); } ....
Comments:
{stateLocal.comments_arr ? stateLocal.comments_arr.map((comment) => ) : null } ....
.CommentStyles { opacity: 1; } .FadeInComment { animation-name: fadeIn; animation-timing-function: ease; animation-duration: 2s } .FadeOutComment { animation-name: fadeOut; animation-timing-function: linear; animation-duration: 2s } @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fadeOut { 0% { opacity: 1; } 100% { opacity: 0; width: 0; height: 0; } }
Primero comenzamos usando una expresión ternaria dentro del className
accesorio de div para alternar las clases de estilo. Si delete_comment_id
en nuestro estado local coincide con el ID de comentario actual, se elimina y se aplica una animación de desvanecimiento al comentario.
Usamos @keyframe
para hacer las animaciones. Encuentro que las @keyframe
animaciones CSS son mucho más simples que los enfoques basados en javascript con bibliotecas como react-spring
y react-transition-group
.
A continuación, mostramos el comentario real.
Seguido de una expresión ternaria que establece la fecha de creación del comentario , "Editado" o "Recién ahora" según las acciones de los usuarios.
A continuación, tenemos una expresión ternaria anidada bastante compleja. Primero comparamos el cur_user_id
(que obtenemos de nuestro context.dbProfileState
estado y establecemos en nuestro JSX) con la identificación del usuario del comentario . Si hay una coincidencia mostramos un botón de edición .
Si el usuario hace clic en el botón editar , establecemos el comentario en el edit_comment
estado y establecemos el edit_comment_id
estado en la identificación del comentario . Y esto también hace que la propiedad isEditing sea verdadera, lo que abre el formulario y permite al usuario editar el comentario. Cuando el usuario presiona Aceptar, handleUpdate()
se llama a la función que veremos a continuación.
Comentarios Operaciones CRUD
Aquí tenemos nuestras funciones para manejar operaciones CRUD para comentarios. Verá que tenemos 2 conjuntos de funciones , uno para manejar CRUD del lado del cliente y otro para manejar solicitudes de API . Explicaré por qué a continuación.
.... //Handling CRUD operations client side const handleCommentSubmit = (submitted_comment) => { if(stateLocal.comments_arr) { setState({...stateLocal, comments_arr: [submitted_comment, ...stateLocal.comments_arr]}) } else { setState({...stateLocal, comments_arr: [submitted_comment]}) } }; const handleCommentUpdate = (comment) => { const commentIndex = stateLocal.comments_arr.findIndex(com => com.cid === comment.cid) var newArr = [...stateLocal.comments_arr ] newArr[commentIndex] = comment setTimeout(() => setState({...stateLocal, comments_arr: [...newArr], edit_comment_id: 0 }), 100) }; const handleCommentDelete = (cid) => { setState({...stateLocal, delete_comment_id: cid}) const newArr = stateLocal.comments_arr.filter(com => com.cid !== cid) setState({...stateLocal, comments_arr: newArr}) }; .... //API requests const handleSubmit = (event) => { event.preventDefault() setState({...stateLocal, comment: ''}) const comment = event.target.comment.value const user_id = context.dbProfileState[0].uid const username = context.dbProfileState[0].username const post_id = stateLocal.post_id const current_time = "Just Now" const temp_cid = Math.floor(Math.random() * 1000); const submitted_comment = {cid: temp_cid, comment: comment, user_id: user_id, author: username, date_created: current_time } const data = {comment: event.target.comment.value, post_id: post_id, user_id: user_id, username: username} axios.post('/api/post/commenttodb', data) .then(res => console.log(res)) .catch((err) => console.log(err)) window.scroll({top: 0, left: 0, behavior: 'smooth'}) handleCommentSubmit(submitted_comment) } const handleUpdate = (event, cid) => { event.preventDefault() console.log(event) console.log(cid) const comment = event.target.editted_comment.value const comment_id = cid const post_id = stateLocal.post_id const user_id = context.dbProfileState[0].uid const username = context.dbProfileState[0].username const isEdited = true const current_time = "Just Now" const edited_comment = {cid: comment_id, comment: comment, user_id: user_id, author: username, date_created: current_time, isEdited: isEdited } const data = {cid: comment_id, comment: comment, post_id: post_id, user_id: user_id, username: username} axios.put('/api/put/commenttodb', data) .then(res => console.log(res)) .catch((err) => console.log(err)) handleCommentUpdate(edited_comment); } const handleDeleteComment = (cid) => { const comment_id = cid console.log(cid) axios.delete('/api/delete/comment', {data: {comment_id: comment_id}} ) .then(res => console.log(res)) .catch((err) => console.log(err)) handleCommentDelete(cid) }
Se debe a que si un usuario envía, edita o elimina un comentario, la interfaz de usuario no se actualizará sin volver a cargar la página. Puede resolver esto haciendo otra solicitud de API o con una configuración de socket web que escuche los cambios en la base de datos, pero una solución mucho más simple es solo manejarlo del lado del cliente mediante programación.
Todas las funciones CRUD del lado del cliente se llaman dentro de sus respectivas llamadas API.
CRUD del lado del cliente:
handleCommentSubmit()
: actualicecomments_arr
simplemente agregando el comentario al comienzo de la matriz.handleCommentUpdate()
: Busque y reemplace el comentario en la matriz con el índice, luego actualice y establezca la nueva matriz en elcomments_arr
handleCommentDelete()
: Busque el comentario en la matriz con el ID del comentario, luego.filter()
sáquelo y guarde la nueva matriz encomments_arr
.
Solicitudes de API:
handleSubmit()
: obtenemos nuestros datos de nuestro formulario, luego combinamos las diferentes propiedades que necesitamos y enviamos esos datos a nuestro servidor. Las variablesdata
ysubmitted_comment
son diferentes porque nuestras operaciones CRUD del lado del cliente necesitan valores ligeramente diferentes a los de nuestra base de datos.handleUpdate()
: esta función es casi idéntica a nuestrahandleSubmit()
función. la principal diferencia es que estamos haciendo una solicitud de venta en lugar de una publicación .handleDeleteComment()
: solicitud de eliminación simple usando el ID de comentario.
manejando gustos
Now we can discuss how to handle when a user likes a post.
.... const handleLikes = () => { const user_id = context.dbProfileState[0].uid const post_id = stateLocal.post_id const data = { uid: user_id, post_id: post_id } console.log(data) if(!stateLocal.like_user_ids.includes(user_id)) { axios.put('/api/put/likes', data) .then( !stateLocal.like_user_ids.includes(user_id) && stateLocal.like_post ? setState({...stateLocal, likes: stateLocal.likes + 1, like_post: false}) : null ) .catch(err => console.log(err)) }; } .... handleLikes() : () => history.replace('/signup')}>thumb_up {stateLocal.likes} ....
.notification-num-showpost { position:relative; padding:5px 9px; background-color: red; color: #941e1e; bottom: 23px; right: 5px; z-index: -1; border-radius: 50%; }
in the handleLikes()
function we first set the post id and user id. Then we use a conditional to check if the current user id is not in the like_user_id
array which remember has all the user ids of the users who have already liked this post.
If not then we make a put request to our server and after we use another conditional and check if the user hasnt already liked this post client side with the like_post
state property then update the likes.
In the JSX we use an onClick
event in our div to either call the handleLikes()
function or redirect to the sign up page. Then we use a material icon to show the thumb up icon and then style it with some CSS.
That's it! not too bad right.
profile.js
Now we have our profile.js
component which will essentially be our user dashboard. It will contain the users profile data on one side and their posts on the other.
The profile data we display here is different than the dbProfile
which is used for database operations. We use the other profile here we are getting from auth0 (or other oauth logins) because it contains data we dont have in our dbProfile
. For example maybe their Facebook profile picture or nickname.
import React, { useContext, useState, useEffect } from 'react'; import Context from '../utils/context'; import { Link } from 'react-router-dom'; import history from '../utils/history'; import axios from 'axios'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; import Button from '@material-ui/core/Button'; const Profile = () => { const context = useContext(Context) const [stateLocal, setState] = useState({ open: false, post_id: null, posts: [] }) useEffect(() => { const user_id = context.dbProfileState[0].uid axios.get('/api/get/userposts', {params: { user_id: user_id}}) .then((res) => setState({...stateLocal, posts: [...res.data] })) .catch((err) => console.log(err)) }) const handleClickOpen = (pid) => { setState({open: true, post_id: pid }) } const handleClickClose = () => { setState({open: false, post_id: null }) } const DeletePost = () => { const post_id = stateLocal.post_id axios.delete('api/delete/postcomments', {data: { post_id: post_id }} ) .then(() => axios.delete('/api/delete/post', {data: { post_id: post_id }} ) .then(res => console.log(res) ) ) .catch(err => console.log(err)) .then(() => handleClickClose()) .then(() => setTimeout(() => history.replace('/'), 700 ) ) } const RenderProfile = (props) => { return(
{props.profile.profile.nickname}
{props.profile.profile.email}
{props.profile.profile.name}
Email Verified:
{props.profile.profile.email_verified ?Yes
:No
}) } const RenderPosts = post => (
Delete } />
{post.post.body} ); return( {stateLocal.posts ? stateLocal.posts.map(post => ) : null } Confirm Delete? Deleteing Post DeletePost() }> Agree handleClickClose()}> Cancel )} export default (Profile);
.FlexProfileDrawer { display: flex; flex-direction: row; margin-top: 20px; margin-left: -90px; margin-right: 25px; } .FlexColumnProfile > h1 { text-align: center; } FlexProfileDrawerRow { display: flex; flex-direction: row; margin: 10px; padding-left: 15px; padding-right: 15px; } .FlexColumn { display: flex; flex-direction: column; } .FlexRow { display: flex; flex-direction: row; }
The vast majority of this functionality in this component we have seen before. We begin by making an API request in our useEffect()
hook to get our posts from the database using the user id then save the posts to our local state.
Then we have our functional component. We get the profile data during the authentication and save it to global state so we can just access it here without making an API request.
Then we have which displays a post and allows a user to go to, edit or delete a post. They can go to the post page by clicking on the title. Clicking on the edit button will take them to the
editpost.js
component and clicking on the delete button will open the dialog box.
In the DeletePost()
function we first delete all the comments associated with that post using the post id. Because if we just deleted the post without deleting the comments we would just have a bunch of comments sitting in our database without a post. After that we just delete the post.
showuser.js
Now we have our component that displays another users posts and comments when a user clicks on their name in the forum.
import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import axios from 'axios'; import moment from 'moment'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import Button from '@material-ui/core/Button'; const ShowUser = (props) => { const [profile, setProfile ] = useState({}) const [userPosts, setPosts ] = useState([]) useEffect(() => { const username = props.location.state.post.post.author axios.get('/api/get/otheruserprofilefromdb', {params: {username: username}} ) .then(res => setProfile({...res.data} )) .catch(function (error) { console.log(error); }) axios.get('/api/get/otheruserposts', {params: {username: username}} ) .then(res => setPosts([...res.data])) .catch(function (error) { console.log(error); }) window.scrollTo({top: 0, left: 0}) }, [props.location.state.post.post.author] ) const RenderProfile = (props) => (
{props.profile.username}
Send Message ); const RenderPosts = (post) => ({ post.post.body } ); return ( {profile ? : null } Latest Activity:
{ userPosts ? userPosts.map(post =>
) : null } ) } export default (ShowUser);
We begin with 2 API requests in our useEffect()
hook since we will need both the other user's profile data and their posts, and then save it to the local state.
We get the user id with react-routers state
property that we saw in the showpost.js
component.
We have our usual and
functional components that display the Profile data and posts. And then we just display them in our JSX.
This is it for this component, there wasn't anything new or ambiguous here so I kept it brief.
Admin App
No full stack blog is complete without an admin app so this is what we will setup next.
Below is a diagram that will show essentially how an admin app will work. It is possible to just have your admin app on different routes within your regular app but having it completely separated in its own app makes both your apps much more compartmentalized and secure.

So the admin app will be its own app with its own authentication but connect to the same database as our regular app.
Admin App authentication
Authentication for the admin app will be a little bit different than our regular app. The main difference being that there will be no sign-up option on the admin app, admins will have to be added manually. Since we dont want random people signing up for our admin app.
Similar to the regular app, I will use Auth0 for authentication.
First we will start on the admin dashboard.

Next click on the create application button.

Next we will have to create a database connection. Go the connections section and click on create DB connection.

We will call our new connection “adminapp2db”.
**Important: Check the slider button that is labeled “Disable Sign Ups”. We do not want random people signing up for our admin app.

Click Create and go to the Applications tab. Click on the slider button for the adminapp2 that we created in the last step.

Next we want to manually add users to be able to log in to our admin app. Go to the users section and click Create User.

Fill out the email and password fields to your desired login info and set the connection to the adminapp2db connection we created in the last step. Then click save.

And that’s it. We can now test if our login is working. Go back to the connections section and click on the adminapp2db connection. Click on the try connection tab. Enter in your login details from the Create User step. You should also not see a tab for Sign Up.

If successful you should be seeing this:

Which means our authentication is setup and only admins we added manually can log in. Great!
Global Edit and Delete Privileges
One of the main functionalities of an admin app will be to have global edit delete privileges which will allow an admin or moderator to make edits to user's posts and comments or to delete spam. This is what we will build here.
The basic idea of how we will do this is to remove the authentication check to edit and delete posts and comments, but at the same time making sure the post and comment still belongs to its original author.
We dont have to start from scratch we can use the same app we have been building in the previous sections and add some admin specific code.
The very first thing we can do is get rid of the "sign up to add post/comments" buttons in our addpost.js
and showpost.js
component since an admin cant sign up for this app by themselves.
next in our editpost.js
component in the handleSubmit()
function we can access the user_id
and username
with the react-router props that we have seen before.
This will ensure that even though we edit the post as an admin, it still belongs to the original user.
const handleSubmit = (event) => { event.preventDefault() const user_id = props.location.state.post.post.user_id const username = props.location.state.post.post.author const pid = props.location.state.post.post.pid const title = event.target.title.value const body = event.target.body.value const data = {title: title, body: body, pid: pid, uid: user_id, username: username } axios.put("/api/put/post", data) .then(res => console.log(res)) .catch(err => console.log(err)) .then(setTimeout(() => history.replace('/'), 700 )) }
The addpost.js
component can be left as is, since an admin should be able to make posts as normal.
Back in our posts.js
component we can add edit and delete buttons to our function.
.... const RenderPosts = post => ( ... Edit deletePost(post.post.pid)}> Delete ) ....
This functionality was only available on the user dashboard in our regular app, but we can implement directly in the main forum for our admin app, which gives us global edit and delete privileges on all the posts.
The rest of the posts.js
component can be left as is.
Now in our showpost.js
component the first thing we can do is remove the comparison of the current user id to the comment user id that allows for edits.
.... // props.cur_user_id === props.comment.user_id const RenderComments = (props) => { return( {true ? !props.isEditing ? ....
Next in the handleUpdate()
function we can set the user name and user id to the original author of the comment.
.... const handleUpdate = (event, cid, commentprops) => { event.preventDefault() .... const user_id = commentprops.userid const username = commentprops.author ....
Our server and database can be left as is.
This is it! we have implemented global edit and delete functionality to our app.
Admin Dashboard
Another very common feature in admin apps is to have a calendar with appointments times and dates, which is what we will have to implement here.
We will start with the server and SQL.
CREATE TABLE appointments ( aid SERIAL PRIMARY KEY, title VARCHAR(10), start_time TIMESTAMP WITH TIME ZONE UNIQUE, end_time TIMESTAMP WITH TIME ZONE UNIQUE );
We have a simple setup here. We have the PRIMARY KEY
. Then the title of the appointment. After that we have start_time
and end_time
. TIMESTAMP WITH TIME ZONE
gives us the date and time, and we use the UNIQUE
keyword to ensure that there cant be duplicate appointments.
/* DATE APPOINTMENTS */ router.post('/api/post/appointment', (req, res, next) => { const values = [req.body.title, req.body.start_time, req.body.end_time] pool.query('INSERT INTO appointments(title, start_time, end_time) VALUES($1, $2, $3 )', values, (q_err, q_res) => { if (q_err) return next(q_err); console.log(q_res) res.json(q_res.rows); }); }); router.get('/api/get/allappointments', (req, res, next) => { pool.query("SELECT * FROM appointments", (q_err, q_res) => { res.json(q_res.rows) }); });
Here we have our routes and queries for the appointments. For the sake of brevity I have omitted the edit and delete routes since we have seen those queries many times before. Challenge yourself to see if you can create those queries. These are basic INSERT
and SELECT
statements nothing out of the ordinary here.
We can now go to our client side.
At the time of this writing I couldn't find a good Calendar library that would work inside of a React Hooks component so I decided to just implement a class component with the react-big-calendar
library.
It will still be easy to follow along, we wont be using Redux or any complex class functionality that isnt available to React hooks.
componentDidMount()
is equivalent to useEffect(() => {}, [] )
. The rest of the syntax is basically the same expect you add the this
keyword at the beginning when accessing property values.
I will replace the regular profile.js
component with the admin dashboard here, and we can set it up like so.
//profile.js import React, { Component } from 'react' import { Calendar, momentLocalizer, Views } from 'react-big-calendar'; import moment from 'moment'; import 'react-big-calendar/lib/css/react-big-calendar.css'; import history from '../utils/history'; import Button from '@material-ui/core/Button'; import Paper from '@material-ui/core/Paper'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; import axios from 'axios'; const localizer = momentLocalizer(moment) const bus_open_time = new Date('07/17/2018 9:00 am') const bus_close_time = new Date('07/17/2018 5:00 pm') let allViews = Object.keys(Views).map(k => Views[k]) class Profile extends Component { constructor(props) { super(props) this.state = { events: [], format_events: [], open: false, start_display: null, start_slot: null, end_slot: null } } componentDidMount() { axios.get('api/get/allappointments') .then((res) => this.setState({events: res.data})) .catch(err => console.log(err)) .then(() => this.dateStringtoObject()) } handleClickOpen = () => { this.setState({ open: true }); }; handleClose = () => { this.setState({ open: false }); }; dateStringtoObject = () => { this.state.events.map(appointment => { this.setState({ format_events: [...this.state.format_events, { id: appointment.aid, title: appointment.title, start: new Date(appointment.start_time), end: new Date(appointment.end_time) }]}) }) } handleAppointmentConfirm = () => { const time_start = this.state.start_slot const time_end = this.state.end_slot const data = {title: 'booked', start_time: time_start, end_time: time_end } axios.post('api/post/appointment', data) .then(response => console.log(response)) .catch(function (error) { console.log(error); }) .then(setTimeout( function() { history.replace('/') }, 700)) .then(alert('Booking Confirmed')) } showTodos = (props) => ( { props.appointment.start.toLocaleString() }
) BigCalendar = () => ( alert(event.start)} onSelectSlot={slotInfo => { this.setState({start_slot: slotInfo.start, end_slot: slotInfo.end, start_display: slotInfo.start.toLocaleString() }); this.handleClickOpen(); }} /> ) render() { return ( Admin Dashboard
Appointments:
{ this.state.format_events ? this.state.format_events.map(appointment => ) : null }{ this.state.format_events ? : null }
Confirm Appointment? Confirm Appointment: {this.state.start_display} this.handleAppointmentConfirm() }> Confirm this.handleClose() }> Cancel )} } export default (Profile);
We will start with our usual imports. Then we will initialize the calendar localizer with the moment.js
library.
Next we will set the business open and close time which I have set at from 9:00 am to 5:00 pm in the bus_open_time
and bus_close_time
variables.
Then we set the allViews
variable which will allow the calendar to have the months, weeks, and days views.
Next we have our local state
variable in the constructor which is equivalent to the useState
hook.
Its not necessary to understand constructors
and the super()
method for our purposes since those are fairly large topics.
Next we have our componentDidMount()
method which we use to make an axios
request to our server to get our appointments and save them to our events
property of local state.
handleClickOpen()
and handleClose()
are helper functions that open and close our dialog box when a user is confirming an appointment.
next we have dateStringToObject()
function which takes our raw data from our request and turns it into a usable format by our calendar. format_events
is the state property to hold the formatted events.
after that we have the handleAppointmentConfirm()
function. We will use this function to make our API request to our server. These values we will get from our component which we will see in a second.
our is how we display each appointment.
Next we have our actual calendar. Most of the props should be self explanatory, but 2 we can focus on are onSelectEvent
and onSelectSlot
.
onSelectEvent
is a function that is called every time a user clicks on an existing event on the calendar, and we just alert them of the event start time.
onSelectSlot
is a function that is called every time a user clicks an empty slot on the calendar, and this is how we get the time values from the calendar. When the user clicks on a slot we save the time values that are contained in the slotInfo parameter to our local state, then we open a dialog box to confirm the appointment.
Our render method is fairly standard. We display our events in a element and have the calendar below. We also have a standard dialog box that allows a user to confirm or cancel the request.
And thats it for the admin dashboard. You should have something that looks like this:

Deleting users along with their posts and comments
Now for the final part of this tutorial we can delete users and their associated comments and posts.
We will start off with our API requests. We have fairly simple DELETE
statements here, I will explain more with the front end code.
/* Users Section */ router.get('/api/get/allusers', (req, res, next) => { pool.query("SELECT * FROM users", (q_err, q_res) => { res.json(q_res.rows) }); }); /* Delete Users and all Accompanying Posts and Comments */ router.delete('/api/delete/usercomments', (req, res, next) => { uid = req.body.uid pool.query('DELETE FROM comments WHERE user_id = $1', [ uid ], (q_err, q_res) => { res.json(q_res); }); }); router.get('/api/get/user_postids', (req, res, next) => { const user_id = req.query.uid pool.query("SELECT pid FROM posts WHERE user_id = $1", [ user_id ], (q_err, q_res) => { res.json(q_res.rows) }); }); router.delete('/api/delete/userpostcomments', (req, res, next) => { post_id = req.body.post_id pool.query('DELETE FROM comments WHERE post_id = $1', [ post_id ], (q_err, q_res) => { res.json(q_res); }); }); router.delete('/api/delete/userposts', (req, res, next) => { uid = req.body.uid pool.query('DELETE FROM posts WHERE user_id = $1', [ uid ], (q_err, q_res) => { res.json(q_res); }); }); router.delete('/api/delete/user', (req, res, next) => { uid = req.body.uid console.log(uid) pool.query('DELETE FROM users WHERE uid = $1', [ uid ], (q_err, q_res) => { res.json(q_res); console.log(q_err) }); }); module.exports = router
And now for our component, you will notice we are using all our API requests in the handleDeleteUser()
function.
import React, { useState, useEffect } from 'react' import axios from 'axios'; import history from '../utils/history'; import Button from '@material-ui/core/Button'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; const Users = () => { const [state, setState] = useState({ users: [], open: false, uid: null }) useEffect(() => { axios.get('api/get/allusers') .then(res => setState({users: res.data})) .catch(err => console.log(err)) }, []) const handleClickOpen = (user_id) => { setState({ open: true, uid: user_id }); }; const handleClose = () => { setState({ open: false }); }; const handleDeleteUser = () => { const user_id = state.uid axios.delete('api/delete/usercomments', { data: { uid: user_id }}) .then(() => axios.get('api/get/user_postids', { params: { uid: user_id }}) .then(res => res.data.map(post => axios.delete('/api/delete/userpostcomments', { data: { post_id: post.pid }})) ) ) .then(() => axios.delete('api/delete/userposts', { data: { uid: user_id }}) .then(() => axios.delete('api/delete/user', { data: { uid: user_id }} ) )) .catch(err => console.log(err) ) .then(setTimeout(history.replace('/'), 700)) } const RenderUsers = (user) => ({ user.user.username }
{ user.user.email }
handleClickOpen(user.user.uid)}> Delete User ); return (
Users
User {state.users ? state.users.map(user => ) : null }
Delete User Deleteing User will delete all posts and comments made by user {handleDeleteUser(); handleClose()} }> Delete Cancel ) } export default (Users);
handleDeleteUser()
I will start off with the handleDeleteUser()
function. The first thing we do is define the user id of the user we want to delete which we get from local state. The user id is saved to local state when an admin clicks on a users name and the dialog box pops up.
The rational for this setup is because of PSQL's foreign key constraint, where we cant delete a row on a table that is being referenced by another table before we delete that other row first. See the PSQL foreign key constraint section for a refresher.
This is why we must work backwards and delete all the comments and posts associated with a user before we can delete the actual user.
The very first axios delete request is to delete all the comments where there is a matching user id which we just defined. We do this because we cant delete the comments associated with posts before deleting the posts themselves.
In our first.then()
statement we look up all the posts this user made and retrieve those post ids. You will notice that our second .then()
statement is actually inside our first .then()
statement. This is because we want the response of the axios.get('api/get/user_postids')
request as opposed to response of the first axios delete request.
In our second .then()
statement we are getting an array of the post ids of the posts associated with the user we want to delete and then calling .map()
on the array. We are then deleting all the comments associated with that post regardless by which user it was made. This would make axios.delete('/api/delete/userpostcomments')
a triple nested axios request!
Our 3rd .then()
statement is deleting the actual posts the user made.
Our 4th.then()
statement is finally deleting the user from the database. Our 5th .then()
is then redirecting the admin to the home page. Our 4th .then()
statement is inside our 3rd.then()
statement for the same reason as to why our 2nd.then()
statement is inside our 1st.
Everything else is functionality we have seen several times before, which will conclude our tutorial!
Thanks for Reading!