Full Stack React: Comment créer votre propre blog en utilisant Express, Hooks et Postgres.

Dans ce didacticiel, nous allons créer un blog React complet avec un back-end d'administrateur de blog.

Je vais vous expliquer toutes les étapes en détail.

À la fin de ce didacticiel, vous aurez suffisamment de connaissances pour créer des applications full stack assez complexes à l'aide d'outils modernes: React, Express et une base de données PostgreSQL.

Pour garder les choses concises, je vais faire le strict minimum de style / mise en page et laisser cela au lecteur.

Projet terminé:

//github.com/iqbal125/react-hooks-complete-fullstack

Application d'administration:

//github.com/iqbal125/react-hooks-admin-app-fullstack

Projet de démarrage:

//github.com/iqbal125/react-hooks-routing-auth-starter

Comment construire le projet de démarrage:

//www.freecodecamp.org/news/build-a-react-hooks-front-end-app-with-routing-and-authentication/

Comment ajouter un moteur de recherche Fullstack à ce projet:

//www.freecodecamp.org/news/react-express-fullstack-search-engine-with-psql/

Vous pouvez regarder une version vidéo de ce tutoriel ici

//www.youtube.com/playlist?list=PLMc67XEAt-yzxRboCFHza4SBOxNr7hDD5

Connectez-vous avec moi sur Twitter pour plus de mises à jour sur les futurs tutoriels: //twitter.com/iqbal125sf

Section 1: Configuration d'Express Server et de la base de données PSQL

  1. Structure du projet
  2. Configuration Express de base
  3. Connexion côté client

    axios vs react-router vs routeur express

    pourquoi ne pas utiliser un ORM comme Sequelize?

  4. Configuration de la base de données

    Clés étrangères PSQL

    Shell PSQL

  5. Configuration des routes express et des requêtes PSQL

Section 2: Configuration frontale de React

  1. Mettre en place un état global avec des réducteurs, des actions et un contexte.

    Enregistrement des données de profil utilisateur dans notre base de données

    Configuration des actions et des réducteurs

  2. Application React côté client

    addpost.js

    editpost.js

    posts.js

    showpost.js

    profile.js

    showuser.js

Section 3: Application d'administration

  1. Authentification de l'application d'administration
  2. Privilèges globaux de modification et de suppression
  3. Tableau de bord d'administration
  4. Suppression des utilisateurs avec leurs publications et commentaires

Structure du projet

Nous commencerons par discuter de la structure des répertoires. Nous aurons 2 répertoires, le répertoire Client et Server . Le répertoire client contiendra le contenu de notre application React que nous avons configurée dans le dernier didacticiel et le serveur contiendra le contenu de notre expressserveur et conservera la logique de nos appels API à notre base de données. Le répertoire du serveur contiendra également le schéma de notre base de données SQL .

La structure du répertoire final ressemblera à ceci.

Configuration express de base

Si vous ne l'avez pas déjà fait, vous pouvez installer le express-generatoravec la commande:

npm install -g express-generator

Il s'agit d'un outil simple qui générera un projet express de base avec une simple commande, similaire à create-react-app. Cela nous fera gagner un peu de temps et n'aura plus à tout configurer à partir de zéro.

Nous pouvons commencer par exécuter la expresscommande dans le répertoire Serveur . Cela nous donnera une application express par défaut, mais nous n'utiliserons pas la configuration par défaut, nous devrons la modifier.

Supprimons d'abord le dossier routes , le dossier views et le dossier public . Nous n'en aurons pas besoin. Vous ne devriez avoir plus que 3 fichiers. Le fichier www dans le répertoire bin , le app.jsfichier et le package.jsonfichier. Si vous avez accidentellement supprimé l'un de ces fichiers, générez simplement un autre projet express. Puisque nous avons supprimé ces dossiers, nous devrons également modifier un peu le code. Refactorisez votre app.jsfichier comme suit:

 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; 

Nous pouvons également placer app.jsdans un dossier appelé main .

Ensuite, nous devons changer le port par défaut dans le fichier www en quelque chose d'autre que le port 3000 car il s'agit du port par défaut sur lequel notre application frontale React fonctionnera.

/** * Get port from environment and store in Express. */ var port = normalizePort(process.env.PORT || '5000'); app.set('port', port); 

En plus des dépendances que nous avons obtenues en générant l'application express, nous ajouterons également 3 bibliothèques supplémentaires pour nous aider:

cors: c'est la bibliothèque que nous utiliserons pour faciliter la communication entre l'application React et le serveur Express. Nous le ferons via un proxy dans l'application React. Sans cela, nous recevrions une erreur Cross Origin Resource dans le navigateur.

helmet: Une bibliothèque de sécurité qui met à jour les en-têtes http. Cette bibliothèque rendra nos requêtes http plus sécurisées.

pg: C'est la bibliothèque principale que nous utiliserons pour communiquer avec notre base de données psql. Sans cette bibliothèque, la communication avec la base de données ne sera pas possible.

nous pouvons continuer et installer ces bibliothèques

npm install pg helmet cors

Nous avons terminé la configuration de notre serveur minimal et nous devrions avoir une structure de projet qui ressemble à ceci.

Nous pouvons maintenant tester pour voir si notre serveur fonctionne. Vous exécutez le serveur sans application côté client . Express est une application entièrement fonctionnelle et fonctionnera indépendamment d'une application côté client . Si cela est fait correctement, vous devriez le voir dans votre terminal.

Nous pouvons maintenir le serveur en marche car nous l'utiliserons sous peu.

Connexion au côté client

La connexion de notre application côté client à notre serveur est très simple et nous n'avons besoin que d'une seule ligne de code. Accédez à votre package.jsonfichier dans votre répertoire Client et entrez ce qui suit:

“proxy”: “//localhost:5000"

Et c'est tout! Notre client peut désormais communiquer avec notre serveur via un proxy.

** Remarque: rappelez-vous que si vous définissez un autre port en plus du port: 5000 dans le wwwfichier, utilisez plutôt ce port dans le proxy.

Voici un diagramme pour décomposer et expliquer ce qui se passe et comment cela fonctionne.

Notre localhost: 3000 fait essentiellement des demandes comme s'il s'agissait de localhost: 5000 via un intermédiaire proxy, ce qui permet à notre serveur de communiquer avec notre client .

Notre côté client est maintenant connecté à notre serveur et nous voulons maintenant tester notre application.

Nous devons maintenant retourner côté serveur et configurer le expressroutage. Dans votre dossier principal du répertoire Serveur , créez un nouveau fichier appelé routes.js. Ce fichier contiendra toutes les expressroutes. qui nous permettent d'envoyer des données à notre application côté client . Nous pouvons définir un itinéraire très simple pour l'instant:

var express = require('express') var router = express.Router() router.get('/api/hello', (req, res) => { res.json('hello world') }) module.exports = router

Essentiellement, si un appel d'API est effectué sur la /helloroute, notre serveur Express répondra avec une chaîne de «bonjour le monde» au format json.

Nous devons également refactoriser notre app.jsfichier pour utiliser les routes express.

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;

Maintenant, pour notre code côté client dans notre home.jscomposant:

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;

Nous faisons une axiosrequête get basique à notre expressserveur en cours d'exécution , si cela fonctionne, nous devrions voir "hello world" rendu à l'écran.

Et oui, cela fonctionne, nous avons configuré avec succès une application React Node Fullstack!

Avant de continuer , je voudrais répondre à quelques questions que vous pourriez avoir qui est ce que la différence entre axios, react routeret express routerpourquoi je ne suis pas en utilisant un ORM comme Sequelize .

Axios vs Express Router vs React Router

TLDR; Nous utilisons react routerpour naviguer dans notre application, nous utilisons axiospour communiquer avec notre expressserveur et nous utilisons notre expressserveur pour communiquer avec notre base de données.

Vous vous demandez peut-être à ce stade comment ces 3 bibliothèques fonctionnent ensemble. Nous utilisons axiospour communiquer avec notre expressserveur backend, nous signifierons un appel à notre expressserveur en incluant «/ api /» dans l'URI. axiospeut également être utilisé pour effectuer des requêtes http directes vers n'importe quel point de terminaison backend. Cependant, pour des raisons de sécurité, il n'est pas conseillé de faire des requêtes du client à la base de données.

express routerest principalement utilisé pour communiquer avec notre base de données, puisque nous pouvons passer des requêtes SQL dans le corps de la express routerfonction. expressavec Node est utilisé pour exécuter du code en dehors du navigateur, ce qui rend les requêtes SQL possibles. expressest également un moyen plus sûr de faire des requêtes http plutôt que axios.

Cependant, nous avons besoin axiosdu côté client React pour gérer les requêtes http asynchrones, nous ne pouvons évidemment pas utiliser express router du côté client React. axiosest basé sur Promise afin qu'il puisse également gérer automatiquement les actions asynchrones.

Nous utilisons react-routerpour naviguer dans notre application, puisque React est une application à page unique, le navigateur ne se recharge pas lors d'un changement de page. Notre application dispose d'une technologie en coulisse qui saura automatiquement si nous demandons un itinéraire via expressou react-router.

Pourquoi ne pas utiliser une bibliothèque ORM comme Sequelize?

TLDR; Préférence pour travailler directement avec SQL qui permet plus de contrôle que ORM. Plus de ressources d'apprentissage pour SQL qu'un ORM. Les compétences ORM ne sont pas transférables, les compétences SQL sont très transférables.

Il existe de nombreux didacticiels qui montrent comment implémenter une bibliothèque ORM utilisée avec une base de données SQL. Rien de mal à cela mais je préfère personnellement interagir directement avec le SQL. Travailler directement avec le SQL vous donne un contrôle plus fin sur le code et je pense que cela vaut la légère augmentation de la difficulté lorsque vous travaillez directement avec le SQL.

Il y a beaucoup plus de ressources sur SQL que sur n'importe quelle bibliothèque ORM donnée, donc si vous avez une question ou une erreur, il est beaucoup plus facile de trouver une solution.

En outre, vous ajoutez une autre dépendance et un autre niveau d'abstraction avec une bibliothèque ORM qui pourrait provoquer des erreurs sur la route. Si vous utilisez un ORM, vous devrez garder une trace des mises à jour et des modifications importantes lorsque la bibliothèque est modifiée. SQL, d'un autre côté, est extrêmement mature et existe depuis des décennies, ce qui signifie qu'il est peu probable qu'il y ait beaucoup de changements de rupture. SQL a également eu le temps d'être affiné et perfectionné, ce qui n'est généralement pas le cas pour les bibliothèques ORM.

Enfin, une bibliothèque ORM prend du temps à apprendre et les connaissances ne sont généralement pas transférables à autre chose. SQL est le langage de base de données le plus utilisé par une très large marge, (la dernière fois que j'ai vérifié environ 90% des bases de données commerciales utilisaient SQL). L'apprentissage d'un système SQL tel que PSQL vous permettra de transférer directement ces compétences et connaissances vers un autre système SQL tel que MySQL.

Ce sont mes raisons pour ne pas utiliser de bibliothèque ORM.

Configuration de la base de données

Commençons par configurer le schéma SQL en créant un fichier dans le dossier principal du répertoire Serveur appelé schema.sql.

Cela contiendra la forme et la structure de la base de données. Pour configurer réellement la base de données, vous devrez bien sûr saisir ces commandes dans le shell PSQL. Le simple fait d'avoir un fichier SQL ici dans notre projet ne fait rien , c'est simplement un moyen pour nous de référencer la structure de notre base de données et de permettre à d'autres ingénieurs d'avoir accès à nos commandes SQL s'ils souhaitent utiliser notre code.

Mais pour avoir une base de données fonctionnelle, nous entrerons ces mêmes commandes dans le 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 ); 

Nous avons donc 3 tableaux ici qui contiendront des données pour nos utilisateurs, nos publications et nos commentaires. Conformément à la convention SQL, tous les textes en minuscules sont des noms de colonnes ou de tables définis par l'utilisateur et tout le texte en majuscules est des commandes SQL.

PRIMARY KEY : Numéro unique généré par psql pour une colonne donnée

VARCHAR (255) : caractère variable, ou texte et nombres. 255 définit la longueur de la ligne.

BOOLEAN : Vrai ou faux

RÉFÉRENCES : comment définir la clé étrangère. La clé étrangère est une clé primaire dans une autre table. J'explique cela plus en détail ci-dessous.

UNIQUE : empêche les entrées en double dans une colonne.

DEFAULT : définir une valeur par défaut

INT [] DEFAULT ARRAY [] :: INT [] : c'est une commande assez complexe mais plutôt simple. Nous avons d'abord un tableau d'entiers, puis nous définissons ce tableau d'entiers sur une valeur par défaut d'un tableau vide de type tableau d'entiers.

Table des utilisateurs

Nous avons un tableau très basique pour les utilisateurs , la plupart de ces données proviendront de auth0, que nous verrons plus en détail dans la section authcheck .  

Tableau des messages

Ensuite, nous avons le tableau des messages. Nous obtiendrons notre titre et notre corps du front-end de React et nous associerons également chaque message avec un user_idet et username. Nous associons chaque publication à un utilisateur avec la clé étrangère de SQL.

Nous avons également notre tableau de like_user_id, qui contiendra tous les identifiants d'utilisateurs des personnes qui ont aimé un message, empêchant ainsi plusieurs likes du même utilisateur.

Tableau des commentaires

Enfin, nous avons notre tableau des commentaires. Nous obtiendrons notre commentaire du front-end de réaction et nous associerons également chaque utilisateur à un commentaire afin d'utiliser le champ user idet usernamede notre table users . Et nous avons également besoin post idde notre table de publication puisqu'un commentaire est fait sur un message, un commentaire n'existe pas de manière isolée. Ainsi, chaque commentaire doit être associé à la fois à un utilisateur et à une publication .

Clés étrangères PSQL

Une clé étrangère est essentiellement un champ ou une colonne dans une autre table qui est référencée par la table d'origine. Une clé étrangère fait généralement référence à une clé primaire dans une autre table, mais comme vous pouvez le voir dans notre table de publications, elle a également un lien de clé étrangère vers le usernamedont nous avons besoin pour des raisons évidentes. Pour garantir l'intégrité des données, vous pouvez utiliser la UNIQUEcontrainte sur le usernamechamp qui lui permet de fonctionner comme une clé étrangère.

L'utilisation d'une colonne dans une table qui fait référence à une colonne dans une table différente est ce qui nous permet d'avoir des relations entre les tables de notre base de données, d'où la raison pour laquelle les bases de données SQL sont appelées «bases de données relationnelles».

La syntaxe que nous utilisons est:

 column_name data_type REFERENCES other_table(column_name_in_other_table) 

Par conséquent, une seule ligne dans la user_idcolonne de notre tableau des messages devra correspondre à une seule ligne dans la uidcolonne de la table des utilisateurs . Cela nous permettra de faire des choses telles que rechercher tous les messages d'un certain utilisateur ou rechercher tous les commentaires associés à un message.

Contrainte de clé étrangère

Vous devrez également être conscient des contraintes de clé étrangère de PSQL. Quelles sont les restrictions qui vous empêchent de supprimer des lignes référencées par une autre table.

Un exemple simple est la suppression de messages sans supprimer les commentaires associés à ce message . L' identifiant de publication de la table de publication est une clé étrangère dans la table de commentaires et est utilisé pour établir une relation entre les tables .

Vous ne pouvez pas simplement supprimer le message sans supprimer d'abord les commentaires, car vous aurez alors un tas de commentaires assis dans votre base de données avec une clé étrangère d'ID de publication inexistante .

Voici un exemple montrant comment supprimer un utilisateur et ses publications et commentaires.

Shell PSQL

Ouvrons le shell PSQL et saisissons ces commandes que nous venons de créer ici dans notre schema.sqlfichier. Ce shell PSQL aurait dû être installé automatiquement lorsque vous avez installé PSQL . Sinon, rendez-vous simplement sur le site Web de PSQL pour le télécharger et l'installer à nouveau.

Si vous vous connectez pour la première fois au shell PSQL, vous serez invité à définir le serveur, le nom de la base de données, le port, le nom d'utilisateur et le mot de passe. Laissez le port par défaut 5432 et configurez le reste des informations d'identification sur ce que vous voulez.

Alors maintenant, vous devriez juste voir postgres#sur le terminal ou tout ce que vous définissez le nom de la base de données. Cela signifie que nous sommes prêts à commencer à entrer des commandes SQL . Au lieu d'utiliser la base de données par défaut, créons-en une nouvelle avec la commande CREATE DATABASE database1, puis connectez-vous avec \c database1. Si cela est fait correctement, vous devriez voir le fichier database#.

Si vous voulez une liste de toutes les commandes, vous pouvez taper   help  ou \? dans le shell PSQL . N'oubliez jamais de terminer vos requêtes SQL par un ;  qui est l'une des erreurs les plus courantes lorsque vous travaillez avec SQL.

D'entendre, nous pouvons simplement copier et coller nos commandes à partir du schema.sqlfichier.

Pour voir une liste de nos tables, nous utilisons la \dtcommande et vous devriez le voir dans le terminal.

Et nous avons mis en place avec succès la base de données!

Nous devons maintenant connecter cette base de données à notre serveur . Faire cela est extrêmement simple. Nous pouvons le faire en utilisant la pgbibliothèque. Installez la pgbibliothèque si vous ne l'avez pas déjà fait et assurez-vous que vous êtes dans le répertoire Serveur, nous ne voulons pas installer cette bibliothèque dans notre application React.

Créez un fichier séparé appelé db.jsdans le répertoire principal et configurez-le comme suit:

const { Pool } = require('pg') const pool = new Pool({ user: 'postgres', host: 'localhost', database: 'postgres', password: '', post: 5432 }) module.exports = pool 

Ce seront les mêmes informations d'identification que vous avez définies lors de la configuration du shell PSQL .

Et voilà, nous avons configuré notre base de données pour une utilisation avec notre serveur. Nous pouvons maintenant commencer à lui faire des requêtes à partir de notre serveur express.

Configuration des routes express et des requêtes PSQL

Voici la configuration des itinéraires et des requêtes. Nous avons besoin de nos opérations CRUD de base pour les messages et les commentaires. Toutes ces valeurs proviendront de notre frontend React que nous configurerons ensuite.

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

Commandes SQL

SELECT * FROM table: Comment nous obtenons les données de la base de données. renvoie toutes les lignes d'une table.

INSERT INTO table(column1, column2): Comment nous sauvegardons les données et ajoutons des lignes à la base de données.  

UPDATE table SET column1 =$1, column2 = $2: comment mettre à jour ou modifier des lignes existantes dans une base de données. La WHEREclause spécifie les lignes à mettre à jour.

DELETE FROM table: supprime les lignes en fonction des conditions de la WHEREclause. ATTENTION : ne pas inclure de WHEREclause supprime toute la table.

WHEREclause: instruction conditionnelle facultative à ajouter aux requêtes. Cela fonctionne de la même manière qu'une ifinstruction en javascript.

WHERE (array @> value): Si la valeur est contenue dans le tableau.

Routes express

Pour configurer les routes express, nous utilisons d'abord l' routerobjet que nous avons défini en haut avec express.Router(). Ensuite, la méthode http que nous voulons qui peut être les méthodes standard telles que GET, POST, PUT, etc.

Ensuite, entre parenthèses, nous passons d'abord la chaîne de la route que nous voulons et le deuxième argument est une fonction à exécuter lorsque la route est appelée depuis le client , Express écoute automatiquement ces appels de route du client . Lorsque les routes correspondent, la fonction dans le corps est appelée, ce qui dans notre cas se trouve être des requêtes PSQL .

Nous pouvons également passer des paramètres dans notre appel de fonction. Nous utilisons req, res et next .

req: est l'abréviation de request et contient les données de demande de notre client. C'est essentiellement ainsi que nous obtenons les données de notre front-end vers notre serveur. Les données de notre frontend React sont contenues dans cet objet req et nous les utilisons ici dans nos routes largement pour accéder aux valeurs. Les données seront fournies à axios en tant que paramètre en tant qu'objet javascript.

Pour les requêtes GET avec un paramètre facultatif, les données seront disponibles avec req.query . Pour les requêtes PUT, POST et DELETE , les données seront disponibles directement dans le corps de la requête avec req.body . Les données seront un objet javascript et chaque propriété est accessible avec une notation par points régulière.

res: est l'abréviation de response et contient la réponse express du serveur . Nous voulons envoyer la réponse que nous obtenons de notre base de données au client afin de transmettre la réponse de la base de données à cette fonction res qui l'envoie ensuite à notre client.

next: est un middleware qui vous permet de passer des rappels à la fonction suivante.

Remarquez à l'intérieur de notre itinéraire express que nous faisons pool.queryet cet poolobjet est le même que celui qui contient les informations de connexion de notre base de données que nous avons configurées précédemment et importées en haut. La fonction de requête nous permet d'effectuer des requêtes SQL dans notre base de données au format chaîne. Notez également que j'utilise `` pas de citations, ce qui me permet d'avoir ma requête sur plusieurs lignes.

Ensuite, nous avons une virgule après notre requête SQL et le paramètre suivant qui est une fonction de flèche à exécuter après l'exécution de la requête . nous passons d'abord 2 paramètres à notre fonction flèche, q_erret q_ressignifiant l' erreur de requête et la réponse de requête . Pour envoyer des données au frontend, nous passons q_res.rowsà la res.jsonfonction. q_res.rowsest la réponse de la base de données car il s'agit de SQL et la base de données nous rendra les lignes correspondantes en fonction de notre requête. Nous convertissons ensuite ces lignes au format json et les envoyons à notre frontend avec le resparamètre.

Nous pouvons également transmettre des valeurs optionnelles à nos requêtes SQL en passant un tableau après la requête séparé par une virgule. Ensuite, nous pouvons accéder aux éléments individuels de ce tableau dans la requête SQL avec la syntaxe $1$1est le premier élément du tableau. $2accède au deuxième élément du tableau et ainsi de suite. Notez que ce n'est pas un système basé sur 0 comme en javascript, il n'y a pas$0

Décomposons chacun de ces itinéraires et donnons une brève description de chacun.

Itinéraires des messages

  • / api / get / allposts: récupère tous nos articles de la base de données.  ORDER BY date_created DESCnous permet d'afficher les messages les plus récents en premier.
  • / api / post / posttodb: enregistre une publication utilisateur dans la base de données. Nous sauvegardons les 4 valeurs dont nous avons besoin: titre, corps, identifiant utilisateur, nom d'utilisateur dans un tableau de valeurs.
  • / api / put / post: modifie une publication existante dans la base de données. Nous utilisons la UPDATE   commande SQL et transmettons à nouveau toutes les valeurs du message. Nous recherchons la publication avec l'identifiant de publication que nous obtenons de notre front-end.
  • / api / delete / postcomments: supprime tous les commentaires associés à une publication. En raison de la contrainte de clé étrangère de PSQL , nous devons supprimer tous les commentaires associés à la publication avant de pouvoir supprimer la publication réelle.
  • / api / delete / post: supprime un post avec l'identifiant du post.
  • / api / put / likes : Nous faisons une requête put pour ajouter l'ID utilisateur de l'utilisateur qui a aimé la publication dans le like_user_idtableau puis nous augmentons le likesnombre de 1.

Commentaires Routes

  • / api / post / commenttodb: enregistre un commentaire dans la base de données
  • / api / put / commenttodb: modifie un commentaire existant dans la base de données
  • / api / delete / comment: supprime un seul commentaire, c'est différent de la suppression de tous les commentaires associés à un post.
  • / api / get / allpostcomments: récupère tous les commentaires associés à un seul post

Itinéraires utilisateur

  • / api / posts / userprofiletodb: enregistre les données d'un profil utilisateur de auth0 dans notre propre base de données. Si l'utilisateur existe déjà, PostgreSQL ne fait rien.
  • / api / get / userprofilefromdb: récupère un utilisateur en recherchant son email
  • / api / get / userposts: récupère les publications faites par un utilisateur en recherchant toutes les publications qui correspondent à son identifiant d'utilisateur.
  • / api / get / otheruserprofilefromdb: récupère les données de profil d'un autre utilisateur à partir de la base de données et affiche sa page de profil.
  • / api / get / otheruserposts: Obtenez les messages d'un autre utilisateur lorsque vous affichez leur page de profil

Configuration de l'état global avec des réducteurs, des actions et un contexte.

Enregistrement des données de profil utilisateur dans notre base de données

Avant de pouvoir commencer à configurer l'état global, nous avons besoin d'un moyen de sauvegarder les données de notre profil utilisateur dans notre propre base de données, actuellement nous ne faisons que récupérer nos données à partir de auth0. Nous ferons cela dans notre authcheck.jscomposant.

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;

Nous avons configuré la plupart de ce composant dans le dernier didacticiel, je recommande donc de voir ce didacticiel pour une explication détaillée, mais ici, nous faisons une demande de publication axios suivie immédiatement d'une autre demande axios get pour obtenir immédiatement les données de profil utilisateur que nous venons d'enregistrer dans la base de données.

Nous faisons cela parce que nous avons besoin de l'ID de clé primaire unique qui est généré par notre base de données et cela nous permet d'associer cet utilisateur à ses commentaires et publications . Et nous utilisons l'e-mail des utilisateurs pour les rechercher car nous ne savons pas quel est leur identifiant unique lors de leur première inscription. Enfin, nous enregistrons les données de profil utilisateur de cette base de données dans notre état global.

* Notez que cela s'applique également aux connexions OAuth telles que les connexions Google et Facebook.

Actions et réducteurs

Nous pouvons maintenant commencer à configurer les actions et les réducteurs avec le contexte pour configurer l'état global de cette application.

Pour configurer le contexte à partir de zéro, consultez mon tutoriel précédent. Ici, nous n'aurons besoin que de l'état du profil de la base de données et de tous les messages.

Premièrement nos types d'actions

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"

Maintenant nos actions

 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 } } 

Enfin notre réducteur de post et réducteur d'authentification

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 } }

Nous devons maintenant les ajouter à la  

 ... /* 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(), ... }}> ...

Ça y est, nous sommes maintenant prêts à utiliser cet état global dans nos composants.

Application React côté client

Ensuite, nous allons configurer le blog de réaction côté client. Tous les appels d'API de cette section ont été configurés dans la section précédente des routes express.

Il sera configuré en 6 composants comme suit.

addpost.js : un composant avec un formulaire pour soumettre des articles.

editpost.js : un composant pour éditer les articles avec un formulaire dont les champs sont déjà remplis.

posts.js : un composant pour rendre tous les messages, comme dans un forum typique.

showpost.js : un composant pour rendre un article individuel après qu'un utilisateur a cliqué sur un article.

profile.js : un composant qui rend les publications associées à un utilisateur. Le tableau de bord utilisateur.

showuser.js : un composant qui affiche les données de profil et les publications d'un autre utilisateur.

Pourquoi ne pas utiliser Redux Form?

TDLR; Redux Form est excessif pour la plupart des cas d'utilisation.

Redux Form est une bibliothèque populaire couramment utilisée dans les applications React. Alors pourquoi ne pas l'utiliser ici? J'ai essayé Redux Form, mais je n'ai tout simplement pas pu trouver de cas d'utilisation ici. Nous devons toujours garder à l'esprit l'utilisation finale, et je n'ai pas pu proposer de scénario pour cette application où nous aurions besoin de sauvegarder les données du formulaire dans l'état redux global.

Dans cette application, nous prenons simplement les données d'une forme régulière et les transmettons à Axios qui les transmet ensuite au serveur express qui les enregistre finalement dans la base de données. L'autre cas d'utilisation possible concerne un composant editpost, que je gère en transmettant les données de publication à une propriété de l'élément Link.

Essayez Redux Form et voyez si vous pouvez en proposer une utilisation intelligente, mais nous n'en aurons pas besoin dans cette application. De plus, toute fonctionnalité offerte par Redux Form peut être accomplie relativement plus facilement sans elle.

La forme Redux est tout simplement exagérée pour la plupart des cas d'utilisation.

Comme avec un ORM, il n'y a aucune raison d'ajouter une autre couche de complexité inutile à notre application.

Il est tout simplement plus facile de configurer des formulaires avec React standard.

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;

Dans le composant addpost, nous avons un simple formulaire à 2 champs où un utilisateur peut entrer un titre et un corps. Le formulaire est soumis à l'aide de la handlesubmit()fonction que nous avons créée. la handleSubmit()fonction prend un mot-clé de paramètre d'événement qui contient les données de formulaire soumises par l'utilisateur.

Nous utiliserons event.preventDefault()pour empêcher le rechargement de la page car React est une application à une seule page et ce serait inutile.

La méthode axios post prend un paramètre de «data» qui sera utilisé pour contenir les données qui seront stockées dans la base de données. Nous obtenons le nom d'utilisateur et user_id de l'état global dont nous avons parlé dans la dernière section.

En fait, la publication des données dans la base de données est gérée dans la fonction de routes express avec des requêtes SQL que nous avons vues auparavant. Notre appel d'API axios transmet ensuite les données à notre serveur express qui enregistrera les informations dans la base de données.

editpost.js

Ensuite, nous avons notre editpost.jscomposant. Ce sera un composant de base pour éditer les messages des utilisateurs. Il ne sera accessible que via la page de profil des utilisateurs.

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: est une fonctionnalité offerte par react-router . Lorsqu'un utilisateur clique sur une publication à partir de sa page de profil, les données de publication sur lesquelles il a cliqué sont enregistrées dans une propriété d'état dans l'élément de lien et que cela est différent de l'état du composant local dans React du useStatehook.

Cette approche nous offre un moyen plus simple de sauvegarder les données par rapport au contexte et nous évite également une demande d'API. Nous verrons comment cela fonctionne dans le profile.jscomposant.

Après cela, nous avons un formulaire de composant contrôlé de base et nous sauvegardons les données à chaque frappe dans l'état React.

Dans notre handleSubmit()fonction, nous combinons toutes nos données avant de les envoyer à notre serveur dans une requête axios put.  

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;

Vous remarquerez que nous avons un useEffect()appel assez complexe pour obtenir nos messages de notre base de données. C'est parce que nous enregistrons nos publications de notre base de données dans l'état global, de sorte que les publications sont toujours là même si un utilisateur accède à une autre page.

Cela évite les appels d'API inutiles à notre serveur. C'est pourquoi nous utilisons un conditionnel pour vérifier si les publications sont déjà enregistrées dans l'état de contexte.

Si les publications sont déjà enregistrées dans l'état global, nous définissons simplement les publications dans l'état global sur notre état local, ce qui nous permet d'initialiser la pagination.  

Pagination

Nous avons une implémentation de pagination de base ici dans la page_change()fonction. Nous avons fondamentalement nos 5 blocs de pagination configurés sous forme de tableau. Lorsque la page change, le tableau est mis à jour avec les nouvelles valeurs. Ceci est vu dans la première ifinstruction de la page_change()fonction, les 4 autres ifinstructions sont juste pour gérer les 2 premières et 2 dernières modifications de page.

Nous devons également passer un window.scrollTo()appel pour faire défiler vers le haut à chaque changement de page.

Mettez-vous au défi de voir si vous pouvez créer une implémentation de pagination plus complexe, mais pour nos besoins, cette fonction unique ici pour la pagination est bien.

nous avons besoin de 4 valeurs d'état pour notre pagination. Nous avons besoin:

  • num_posts: nombre de postes
  • posts_slice: une tranche du total des messages
  • currentPage: la page courante
  • posts_per_page: Le nombre de messages sur chaque page.

Nous devons également passer la currentPagevaleur de l' état au useEffect()hook, ce qui nous permet de déclencher une fonction à chaque fois que la page change. Nous obtenons le indexOfLastPost en multipliant 3 fois le currentPageet obtenons le indexOfFirstPostmessage que nous voulons afficher en soustrayant 3. Nous pouvons ensuite définir ce nouveau tableau en tranches comme le nouveau tableau dans notre état local.

Maintenant pour notre JSX. Nous utilisons flexbox pour structurer et mettre en page nos blocs de pagination au lieu des listes horizontales habituelles qui sont traditionnellement utilisées.

Nous avons 4 boutons qui vous permettent d'aller à la toute première page ou de reculer d'une page et vice-versa. Ensuite, nous utilisons une instruction map sur notre pages_slicetableau qui nous donne les valeurs de nos blocs de pagination. Un utilisateur peut également cliquer sur un bloc de pagination qui passera dans la page en argument de la page_change()fonction.

Nous avons également des classes CSS qui nous permettent également de définir le style de notre pagination.  

  • .pagination-active: il s'agit d'une classe CSS ordinaire au lieu d'un pseudo-sélecteur que vous voyez habituellement avec des listes horizontales telles que .item:active. Nous basculons la classe active dans le React JSX en comparant le currentPageavec la page dans le pages_slicetableau.
  • .pagination-item: style pour tous les blocs de pagination
  • .pagination-item:hover: style à appliquer lorsque l'utilisateur survole un bloc de pagination
 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

est le composant fonctionnel que nous utilisons pour rendre chaque article individuel. Le titre des articles est un Linkqui, une fois cliqué, amènera un utilisateur à chaque article individuel avec des commentaires. Vous remarquerez également que nous passons dans tout le message à la statepropriété de l' Linkélément. Cette statepropriété est différente de notre état local, il s'agit en fait d'une propriété de react-routeret nous verrons cela plus en détail dans le showpost.jscomposant. Nous faisons de même avec l'auteur du message.

Vous remarquerez également quelques autres choses liées à la recherche de messages dont je parlerai dans les sections suivantes.  

Je discuterai également de la fonctionnalité "aime" dans le showpost.jscomposant.

showpost.js

Maintenant, nous avons de loin le composant le plus complexe de cette application. Ne vous inquiétez pas, je vais le décomposer complètement étape par étape, ce n'est pas aussi intimidant qu'il en a l'air.  

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;

Vous remarquerez d'abord un useStateappel gigantesque . J'expliquerai comment chaque propriété fonctionne alors que nous explorons notre composant ici en même temps.

useEffect () et requêtes API

La première chose dont nous devons être conscients est qu'un utilisateur peut accéder à un message de 2 manières différentes. Y accéder depuis le forum ou y accéder en utilisant l'URL directe .  

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])

S'ils y accèdent à partir du forum, nous vérifions cela dans notre useEffect()appel, puis définissons notre état local sur le message. Puisque nous utilisons lastate propriété de React Router dans l' élément, nous avons accès à toutes les données de publication déjà disponibles via des accessoires, ce qui nous évite un appel API inutile.

Si l'utilisateur entre l'URL directe d'un message dans le navigateur, nous n'avons pas d'autre choix que de faire une demande API pour obtenir le message, car un utilisateur doit cliquer sur un message du posts.jsforum pour enregistrer les données du message dans la réaction. statepropriété du routeur .

Nous extrayons d'abord l' ID de publication de l'URL avec la pathnamepropriété react-router , que nous utilisons ensuite comme paramètre dans notre requête axios . Après la demande d'API, nous enregistrons simplement la réponse dans notre état local.

Après cela, nous devons également obtenir les commentaires avec une demande d'API . Nous pouvons utiliser la même méthode d'extraction d'URL de post id pour rechercher des commentaires associés à un post.

RenderComments et animations

Ici, nous avons notre composant fonctionnel que nous utilisons pour afficher un commentaire individuel.

.... 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; } }

Nous commençons par utiliser une expression ternaire à l'intérieur de l' classNameaccessoire de div pour basculer les classes de style. Si le delete_comment_iddans notre état local correspond à l'ID de commentaire actuel, il est supprimé et une animation de fondu est appliquée au commentaire.

Nous utilisons @keyframepour faire les animations. Je trouve que les @keyframeanimations css sont beaucoup plus simples que les approches basées sur javascript avec des bibliothèques telles que react-springet react-transition-group.

Ensuite, nous avons affiché le commentaire réel

Suivi par une expression ternaire qui définit la date de création du commentaire , "Modifié" ou "Juste maintenant" en fonction des actions des utilisateurs.  

Ensuite, nous avons une expression ternaire imbriquée assez complexe. Nous comparons d'abord le cur_user_id(que nous obtenons de notre context.dbProfileStateétat et que nous définissons dans notre JSX) avec l' ID utilisateur du commentaire . S'il y a une correspondance, nous affichons un bouton d'édition .

Si l'utilisateur clique sur le bouton d'édition, nous définissons le commentaire sur l' edit_commentétat et définissons l' edit_comment_idétat sur l' identifiant du commentaire . Et cela rend également la prop isEditing à true, ce qui fait apparaître le formulaire et permet à l'utilisateur de modifier le commentaire. Lorsque l'utilisateur clique sur Accepter, la handleUpdate()fonction est appelée que nous verrons ensuite.

Commentaires Opérations CRUD

Ici, nous avons nos fonctions pour gérer les opérations CRUD pour les commentaires. Vous verrez que nous avons 2 ensembles de fonctions , un ensemble pour gérer le CRUD côté client et un autre pour gérer les requêtes API . J'expliquerai pourquoi ci-dessous.

.... //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) }

C'est parce que si un utilisateur soumet, modifie ou supprime un commentaire, l' interface utilisateur ne sera pas mise à jour sans recharger la page. Vous pouvez résoudre ce problème en effectuant une autre demande d'API ou en ayant une configuration de socket Web qui écoute les modifications apportées à la base de données, mais une solution beaucoup plus simple consiste simplement à la gérer côté client par programme.  

Toutes les fonctions CRUD côté client sont appelées dans leurs appels API respectifs.

CRUD côté client:

  • handleCommentSubmit(): mettez à jour le comments_arren ajoutant simplement le commentaire au début du tableau.  
  • handleCommentUpdate(): Recherchez et remplacez le commentaire dans le tableau par l'index, puis mettez à jour et définissez le nouveau tableau sur le comments_arr
  • handleCommentDelete(): Trouvez le commentaire dans le tableau avec l' ID de commentaire, puis .filter()sortez-le et enregistrez le nouveau tableau dans comments_arr.

Demandes d'API:

  • handleSubmit(): nous récupérons nos données à partir de notre formulaire, puis combinons les différentes propriétés dont nous avons besoin et envoyons ces données à notre serveur. Les variables dataet submitted_commentsont différentes car nos opérations CRUD côté client nécessitent des valeurs légèrement différentes de celles de notre base de données.
  • handleUpdate(): cette fonction est presque identique à notre handleSubmit()fonction. la principale différence étant que nous faisons une demande de vente au lieu d'un message .
  • handleDeleteComment(): demande de suppression simple en utilisant l' identifiant du commentaire.  

manutention aime

Nous pouvons maintenant discuter de la façon de gérer lorsqu'un utilisateur aime un message.

 .... 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%; }

dans la handleLikes()fonction, nous définissons d'abord l' identifiant de la publication et l'identifiant de l'utilisateur . Ensuite, nous utilisons un conditionnel pour vérifier si l' ID utilisateur actuel n'est pas dans le like_user_idtableau qui se souvient de tous les ID utilisateur des utilisateurs qui ont déjà aimé ce message.

Sinon, nous faisons une demande de mise à notre serveur et après avoir utilisé un autre conditionnel et vérifié si l'utilisateur n'a pas déjà aimé ce côté client de poste avec la like_postpropriété d'état, mettez à jour les 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!