Full Stack React: So erstellen Sie Ihr eigenes Blog mit Express, Hooks und Postgres.

In diesem Tutorial erstellen wir ein Full-Stack-React-Blog zusammen mit einem Blog-Administrator-Backend.

Ich werde Sie im Detail durch alle Schritte führen.

Am Ende dieses Tutorials verfügen Sie über genügend Kenntnisse, um mit modernen Tools ziemlich komplexe Full-Stack-Apps zu erstellen: React, Express und eine PostgreSQL-Datenbank.

Um die Dinge kurz zu halten, werde ich das absolute Minimum an Styling / Layout tun und dies dem Leser überlassen.

Abgeschlossenes Projekt:

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

Admin App:

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

Starterprojekt:

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

So erstellen Sie das Starterprojekt:

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

So fügen Sie diesem Projekt eine Fullstack-Suchmaschine hinzu:

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

Sie können sich hier eine Videoversion dieses Tutorials ansehen

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

Verbinde dich mit mir auf Twitter, um weitere Updates für zukünftige Tutorials zu erhalten: //twitter.com/iqbal125sf

Abschnitt 1: Einrichtung der Express Server- und PSQL-Datenbank

  1. Projektstruktur
  2. Grundlegende Express-Einrichtung
  3. Verbindung zur Client-Seite herstellen

    Axios vs React-Router vs Express-Router

    Warum nicht ein ORM wie Sequelize verwenden?

  4. Einrichten der Datenbank

    PSQL-Fremdschlüssel

    PSQL-Shell

  5. Einrichten von Express-Routen und PSQL-Abfragen

Abschnitt 2: Reagieren Sie auf das Front-End-Setup

  1. Aufbau eines globalen Staates mit Reduzierungen, Aktionen und Kontext.

    Speichern von Benutzerprofildaten in unserer Datenbank

    Einrichtung von Aktionen und Reduzierern

  2. Clientseitige App reagieren

    addpost.js

    editpost.js

    posts.js

    showpost.js

    profile.js

    showuser.js

Abschnitt 3: Admin App

  1. Admin-App-Authentifizierung
  2. Globale Berechtigungen zum Bearbeiten und Löschen
  3. Admin-Dashboard
  4. Löschen von Benutzern zusammen mit ihren Posts und Kommentaren

Projektstruktur

Wir werden zunächst die Verzeichnisstruktur diskutieren. Wir werden 2 Verzeichnisse haben, das Client- und das Server- Verzeichnis. Das Client- Verzeichnis enthält den Inhalt unserer React-App, die wir im letzten Lernprogramm eingerichtet haben, und der Server enthält den Inhalt unseres expressServers und die Logik für unsere API-Aufrufe an unsere Datenbank. Das Serververzeichnis enthält auch das Schema für unsere SQL- Datenbank.

Die Struktur des endgültigen Verzeichnisses sieht folgendermaßen aus.

Grundlegendes Express-Setup

Wenn Sie dies noch nicht getan haben, können Sie das express-generatormit dem folgenden Befehl installieren :

npm install -g express-generator

Dies ist ein einfaches Tool, das ein einfaches Express-Projekt mit einem einfachen Befehl generiert, ähnlich wie create-react-app. Dies erspart uns ein wenig Zeit, da wir nicht alles von Grund auf neu einrichten müssen.

Wir können beginnen, indem wir den expressBefehl im Serververzeichnis ausführen. Dadurch erhalten wir eine Standard-Express-App, aber wir verwenden nicht die Standardkonfiguration, die wir ändern müssen.

Lassen Sie uns zuerst löschen die Routen - Ordner, die Ansichten Ordner und die öffentlichen Ordner. Wir werden sie nicht brauchen. Sie sollten nur noch 3 Dateien haben. Die WWW- Datei im bin- Verzeichnis, die app.jsDatei und die package.jsonDatei. Wenn Sie versehentlich eine dieser Dateien gelöscht haben, generieren Sie einfach ein anderes Express-Projekt. Da wir diese Ordner gelöscht haben, müssen wir auch den Code ein wenig ändern. Refactor Ihre app.jsDatei wie folgt:

 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; 

Wir können auch app.jsin einen Ordner namens main legen .

Als nächstes müssen wir den Standardport in der WWW- Datei in einen anderen als Port 3000 ändern, da dies der Standardport ist, auf dem unsere React-Front-End-App ausgeführt wird.

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

Zusätzlich zu den Abhängigkeiten, die wir durch das Generieren der Express-App erhalten haben, werden wir drei weitere Bibliotheken hinzufügen, um uns zu helfen:

cors: Dies ist die Bibliothek, mit der wir die Kommunikation zwischen der React App und dem Express-Server unterstützen. Wir werden dies über einen Proxy in der React-App tun. Ohne dies würden wir einen Cross Origin Resource-Fehler im Browser erhalten.

helmet: Eine Sicherheitsbibliothek, die http-Header aktualisiert. Diese Bibliothek macht unsere http-Anfragen sicherer.

pg: Dies ist die Hauptbibliothek, mit der wir mit unserer psql-Datenbank kommunizieren. Ohne diese Bibliothek ist keine Kommunikation mit der Datenbank möglich.

Wir können fortfahren und diese Bibliotheken installieren

npm install pg helmet cors

Wir sind mit dem Einrichten unseres Minimalservers fertig und sollten eine Projektstruktur haben, die so aussieht.

Jetzt können wir testen, ob unser Server funktioniert. Sie führen den Server ohne clientseitige App aus . Express ist eine voll funktionsfähige App und wird unabhängig von einer clientseitigen App ausgeführt . Wenn Sie dies richtig gemacht haben, sollten Sie dies in Ihrem Terminal sehen.

Wir können den Server am Laufen halten, da wir ihn in Kürze verwenden werden.

Verbindung zur Client-Seite herstellen

Das Verbinden unserer clientseitigen App mit unserem Server ist sehr einfach und wir benötigen nur eine Codezeile. Gehen Sie zu Ihrer package.jsonDatei in Ihrem Client-Verzeichnis und geben Sie Folgendes ein:

“proxy”: “//localhost:5000"

Und das ist es! Unser Client kann jetzt über einen Proxy mit unserem Server kommunizieren.

** Hinweis: Denken Sie daran, dass Sie wwwstattdessen diesen Port im Proxy verwenden , wenn Sie neben Port: 5000 in der Datei einen anderen Port festlegen .

Hier ist ein Diagramm, um zu erklären, was passiert und wie es funktioniert.

Unser localhost: 3000 stellt im Wesentlichen Anfragen, als wäre es localhost: 5000, über einen Proxy-Mittelsmann, wodurch unser Server mit unserem Client kommunizieren kann .

Unsere Client-Seite ist jetzt mit unserem Server verbunden und wir möchten jetzt unsere App testen.

Wir müssen jetzt zur Serverseite zurückkehren und das expressRouting einrichten . In Ihrem Hauptordner im Server - Verzeichnis eine neue Datei namens erstellen routes.js. Diese Datei enthält alle expressRouten. Damit können wir Daten an unsere clientseitige App senden . Wir können vorerst eine sehr einfache Route festlegen:

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

Wenn ein API-Aufruf an die /helloRoute erfolgt, antwortet unser Express-Server im Wesentlichen mit einer Zeichenfolge von "Hallo Welt" im JSON-Format.

Wir müssen auch unsere app.jsDatei umgestalten , um die Expressrouten zu verwenden.

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;

Nun zu unserem clientseitigen Code in unserer home.jsKomponente:

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;

Wir stellen eine grundlegende axiosGet-Anfrage an unseren laufenden expressServer. Wenn dies funktioniert, sollte "Hallo Welt" auf dem Bildschirm angezeigt werden.

Und ja, es funktioniert, wir haben erfolgreich eine React Node Fullstack-App eingerichtet!

Bevor wir fortfahren möchte ich ein paar Fragen zu beantworten haben Sie vielleicht das ist , was der Unterschied ist zwischen axios, react routerund , express routerund warum Im nicht unter Verwendung eines ORM wie Sequelize .

Axios vs Express Router vs React Router

TLDR; Wir verwenden react routerdie Navigation innerhalb unserer App, die axiosKommunikation mit unserem expressServer und expressdie Kommunikation mit unserer Datenbank.

Möglicherweise fragen Sie sich an dieser Stelle, wie diese drei Bibliotheken zusammenarbeiten. Wir verwenden axios, um mit unserem expressServer-Backend zu kommunizieren. Wir kennzeichnen einen Anruf an unseren expressServer, indem wir "/ api /" in die URI aufnehmen. axioskann auch verwendet werden, um direkte http-Anforderungen an einen beliebigen Backend-Endpunkt zu senden. Aus Sicherheitsgründen wird jedoch nicht empfohlen, Anforderungen vom Client an die Datenbank zu stellen.

express routerwird hauptsächlich für die Kommunikation mit unserer Datenbank verwendet, da wir SQL-Abfragen im Hauptteil der express routerFunktion übergeben können. expresszusammen mit Node wird Code außerhalb des Browsers ausgeführt, was SQL-Abfragen ermöglicht. expressist auch eine sicherere Möglichkeit, http-Anfragen anstelle von Axios zu stellen.

Wir müssen jedoch axiosauf der React-Client-Seite die asynchronen http-Anforderungen verarbeiten, die wir express router auf unserer React-Client-Seite offensichtlich nicht verwenden können . axiosist Promise- basiert, sodass auch asynchrone Aktionen automatisch verarbeitet werden können.

Wir verwenden react-routerdie Navigation innerhalb unserer App, da React eine Einzelseiten-App ist, die der Browser bei einem Seitenwechsel nicht neu lädt. Unsere App verfügt über eine Technologie hinter den Kulissen, die automatisch erkennt, ob wir eine Route durch expressoder anfordern react-router.

Warum nicht eine ORM-Bibliothek wie Sequelize verwenden?

TLDR; Präferenz für die direkte Arbeit mit SQL, die mehr Kontrolle als ORM ermöglicht. Mehr Lernressourcen für SQL als ein ORM. ORM-Kenntnisse sind nicht übertragbar, SQL-Kenntnisse sind sehr übertragbar.

Es gibt viele Tutorials, die zeigen, wie eine ORM-Bibliothek in Verwendung mit einer SQL-Datenbank implementiert wird. Daran ist nichts auszusetzen, aber ich persönlich bevorzuge es, direkt mit SQL zu interagieren. Wenn Sie direkt mit SQL arbeiten, haben Sie eine genauere Kontrolle über den Code, und ich glaube, dies ist den leichten Anstieg der Schwierigkeit wert, wenn Sie direkt mit SQL arbeiten.

In SQL gibt es viel mehr Ressourcen als in einer bestimmten ORM-Bibliothek. Wenn Sie also eine Frage oder einen Fehler haben, ist es viel einfacher, eine Lösung zu finden.

Außerdem fügen Sie einer ORM-Bibliothek eine weitere Abhängigkeit und Abstraktionsebene hinzu, die später zu Fehlern führen kann. Wenn Sie ein ORM verwenden, müssen Sie Aktualisierungen und Änderungen nachverfolgen, wenn die Bibliothek geändert wird. SQL hingegen ist extrem ausgereift und gibt es schon seit Jahrzehnten, was bedeutet, dass es wahrscheinlich nicht sehr viele wichtige Änderungen gibt. SQL hatte auch Zeit, verfeinert und perfektioniert zu werden, was bei ORM-Bibliotheken normalerweise nicht der Fall ist.

Schließlich braucht eine ORM-Bibliothek Zeit zum Lernen und das Wissen ist normalerweise nicht auf etwas anderes übertragbar. SQL ist mit großem Abstand die am häufigsten verwendete Datenbanksprache (zuletzt habe ich etwa 90% der kommerziellen Datenbanken überprüft, in denen SQL verwendet wurde). Durch das Erlernen eines SQL-Systems wie PSQL können Sie diese Fähigkeiten und Kenntnisse direkt auf ein anderes SQL-System wie MySQL übertragen.

Das sind meine Gründe, warum ich keine ORM-Bibliothek benutze.

Einrichten der Datenbank

Beginnen wir mit dem Einrichten des SQL-Schemas, indem wir eine Datei im Hauptordner des Serververzeichnisses namens erstellen schema.sql.

Dies behält die Form und Struktur der Datenbank bei. Um die Datenbank tatsächlich einzurichten, müssen Sie diese Befehle natürlich in die PSQL-Shell eingeben. Nur eine SQL-Datei hier in unserem Projekt zu haben, bringt nichts. Wir können lediglich darauf verweisen, wie unsere Datenbankstruktur aussieht, und anderen Ingenieuren den Zugriff auf unsere SQL-Befehle ermöglichen, wenn sie unseren Code verwenden möchten.

Aber um tatsächlich eine funktionierende Datenbank zu haben, werden wir dieselben Befehle in das PSQL-Terminal eingeben.

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

Wir haben hier also 3 Tabellen, die Daten für unsere Benutzer, Beiträge und Kommentare enthalten. In Übereinstimmung mit der SQL-Konvention sind alle Kleinbuchstaben benutzerdefinierte Spalten- oder Tabellennamen und alle Großbuchstaben SQL-Befehle.

PRIMARY KEY : Eindeutige Zahl, die von psql für eine bestimmte Spalte generiert wird

VARCHAR (255) : variables Zeichen oder Text und Zahlen. 255 legt die Länge der Reihe fest.

BOOLEAN : Richtig oder falsch

REFERENZEN : Einstellen des Fremdschlüssels. Der Fremdschlüssel ist ein Primärschlüssel in einer anderen Tabelle. Ich erkläre dies weiter unten ausführlicher.

EINZIGARTIG : Verhindert doppelte Einträge in einer Spalte.

STANDARD : Legen Sie einen Standardwert fest

INT [] DEFAULT ARRAY [] :: INT [] : Dies ist ein ziemlich komplex aussehender Befehl, aber ziemlich einfach. Wir haben zuerst ein Array von Ganzzahlen, dann setzen wir dieses Ganzzahlarray auf einen Standardwert eines leeren Arrays vom Typ Array von Ganzzahlen.

Benutzertabelle

Wir haben eine sehr einfache Tabelle für Benutzer . Die meisten dieser Daten stammen von auth0, von denen wir im Abschnitt authcheck mehr sehen werden .  

Beitragstabelle

Als nächstes haben wir die Beitragstabelle. Wir erhalten unseren Titel und Text vom React-Frontend und verknüpfen jeden Beitrag mit einem user_idund und username. Wir verknüpfen jeden Beitrag mit einem Benutzer mit dem Fremdschlüssel von SQL.

Wir haben auch eine Reihe von like_user_id, die alle Benutzer-IDs von Personen enthalten, denen ein Beitrag gefallen hat, wodurch verhindert wird, dass mehrere Likes von demselben Benutzer stammen.

Kommentartabelle

Endlich haben wir unsere Kommentartabelle. Wir erhalten unseren Kommentar vom React-Frontend und ordnen jedem Benutzer einen Kommentar zu, sodass wir das Feld user idund usernameaus unserer Benutzertabelle verwenden . Und wir brauchen auch die post idaus unserer Post-Tabelle, da ein Kommentar zu einem Post gemacht wird, existiert ein Kommentar nicht isoliert. Jeder Kommentar muss also sowohl einem Benutzer als auch einem Beitrag zugeordnet sein .

PSQL-Fremdschlüssel

Ein Fremdschlüssel ist im Wesentlichen ein Feld oder eine Spalte in einer anderen Tabelle, auf die von der ursprünglichen Tabelle verwiesen wird. Ein Fremdschlüssel verweist normalerweise auf einen Primärschlüssel in einer anderen Tabelle. Wie Sie jedoch in unserer Beitragstabelle sehen können, enthält er auch einen Fremdschlüssel- Link zu dem, usernameden wir aus offensichtlichen Gründen benötigen. Um die Datenintegrität sicherzustellen, können Sie die UNIQUEEinschränkung für das usernameFeld verwenden, die es ermöglicht, als Fremdschlüssel zu fungieren.

Die Verwendung einer Spalte in einer Tabelle, die auf eine Spalte in einer anderen Tabelle verweist, ermöglicht es uns, Beziehungen zwischen Tabellen in unserer Datenbank herzustellen, weshalb SQL-Datenbanken als "relationale Datenbanken" bezeichnet werden.

Die von uns verwendete Syntax lautet:

 column_name data_type REFERENCES other_table(column_name_in_other_table) 

Daher muss eine einzelne Zeile in der user_idSpalte in unserer Beitragstabelle mit einer einzelnen Zeile in der uidSpalte der Benutzertabelle übereinstimmen . Auf diese Weise können wir beispielsweise alle Beiträge eines bestimmten Benutzers oder alle mit einem Beitrag verknüpften Kommentare nachschlagen.

Fremdschlüsseleinschränkung

Außerdem müssen Sie die Fremdschlüsseleinschränkungen von PSQL berücksichtigen. Dies sind Einschränkungen, die Sie daran hindern, Zeilen zu löschen, auf die von einer anderen Tabelle verwiesen wird.

Ein einfaches Beispiel ist das Löschen von Posts, ohne die mit diesem Post verknüpften Kommentare zu löschen . Die Post-ID aus der Post-Tabelle ist ein Fremdschlüssel in der Kommentartabelle und wird verwendet, um eine Beziehung zwischen den Tabellen herzustellen .

Sie können den Beitrag nicht einfach löschen, ohne zuerst die Kommentare zu löschen, da dann eine Reihe von Kommentaren mit einer nicht vorhandenen Post-ID-Fremdschlüssel in Ihrer Datenbank gespeichert sind .

Hier ist ein Beispiel, das zeigt, wie ein Benutzer und seine Beiträge und Kommentare gelöscht werden.

PSQL-Shell

Öffnen wir die PSQL-Shell und geben Sie diese Befehle ein, die wir gerade hier in unserer schema.sqlDatei erstellt haben. Diese PSQL-Shell sollte bei der Installation von PSQL automatisch installiert worden sein . Wenn nicht, gehen Sie einfach auf die PSQL- Website, um sie herunterzuladen und erneut zu installieren.

Wenn Sie sich zum ersten Mal bei der PSQL-Shell anmelden, werden Sie aufgefordert, den Server, den Datenbanknamen, den Port, den Benutzernamen und das Kennwort festzulegen . Überlassen Sie den Port dem Standardwert 5432 und richten Sie den Rest der Anmeldeinformationen nach Belieben ein.

Jetzt sollten Sie nur noch postgres#auf dem Terminal sehen oder wie auch immer Sie den Datenbanknamen festlegen. Dies bedeutet, dass wir bereit sind, SQL- Befehle einzugeben. Anstatt die Standarddatenbank zu verwenden, erstellen wir mit dem Befehl eine neue CREATE DATABASE database1und stellen dann eine Verbindung mit her \c database1. Wenn Sie es richtig gemacht haben, sollten Sie das sehen database#.

Wenn Sie eine Liste aller Befehle wünschen, die Sie eingeben können,   help  oder \? in die PSQL-Shell . Denken Sie immer daran, Ihre SQL-Abfragen mit einem zu beenden, ;  der einer der häufigsten Fehler bei der Arbeit mit SQL ist.

Von hören können wir einfach unsere Befehle aus der schema.sqlDatei kopieren und einfügen .

Um eine Liste unserer Tabellen anzuzeigen, verwenden wir den \dtBefehl, und Sie sollten dies im Terminal sehen.

Und wir haben die Datenbank erfolgreich eingerichtet!

Jetzt müssen wir diese Datenbank tatsächlich mit unserem Server verbinden . Dies zu tun ist extrem einfach. Wir können dies tun, indem wir die pgBibliothek nutzen. Installieren Sie die pgBibliothek, falls Sie dies noch nicht getan haben, und stellen Sie sicher, dass Sie sich im Serververzeichnis befinden. Wir möchten diese Bibliothek nicht in unserer React-App installieren.

Erstellen Sie eine separate Datei, die db.jsim Hauptverzeichnis aufgerufen wird, und richten Sie sie wie folgt ein:

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

Dies sind die gleichen Anmeldeinformationen, die Sie beim Einrichten der PSQL- Shell festgelegt haben .

Und das ist es, was wir unsere Datenbank mit Verwendung mit unserem Server eingerichtet haben. Wir können jetzt von unserem Express-Server aus Fragen an ihn stellen.

Einrichten von Express-Routen und PSQL-Abfragen

Hier ist das Setup für die Routen und Abfragen. Wir benötigen unsere grundlegenden CRUD-Operationen für die Beiträge und Kommentare. Alle diese Werte stammen aus unserem React-Frontend, das wir als Nächstes einrichten werden.

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

SQL-Befehle

SELECT * FROM table: Wie wir Daten aus der DB erhalten. Alle Zeilen einer Tabelle zurückgeben.

INSERT INTO table(column1, column2): Wie wir Daten speichern und der Datenbank Zeilen hinzufügen.  

UPDATE table SET column1 =$1, column2 = $2: So aktualisieren oder ändern Sie vorhandene Zeilen in einer Datenbank. Die WHEREKlausel gibt an, welche Zeilen aktualisiert werden sollen.

DELETE FROM table: löscht Zeilen basierend auf den Bedingungen der WHEREKlausel. VORSICHT : Wenn keine WHEREKlausel enthalten ist, wird die gesamte Tabelle gelöscht.

WHEREKlausel: Eine optionale bedingte Anweisung zum Hinzufügen zu Abfragen. Dies funktioniert ähnlich wie eine ifAnweisung in Javascript.

WHERE (array @> value): Wenn der Wert im Array enthalten ist.

Express-Routen

Um Express-Routen einzurichten , verwenden wir zuerst das routerObjekt, mit dem wir oben definiert haben express.Router(). Dann die gewünschte http-Methode , die die Standardmethoden wie GET, POST, PUT usw. sein können.

Dann übergeben wir in Klammern zuerst die Zeichenfolge der gewünschten Route , und das zweite Argument ist eine Funktion, die ausgeführt wird, wenn die Route vom Client aufgerufen wird. Express wartet automatisch auf diese Routenaufrufe vom Client . Wenn die Routen übereinstimmen, wird die Funktion im Body aufgerufen, in unserem Fall PSQL-Abfragen .

Wir können auch Parameter innerhalb unseres Funktionsaufrufs übergeben. Wir verwenden req, res und next .

req: steht für request und enthält die Anforderungsdaten unseres Kunden. Auf diese Weise erhalten wir im Wesentlichen Daten von unserem Front-End zu unserem Server. Die Daten aus unserem React-Frontend sind in diesem Req-Objekt enthalten und werden hier in unseren Routen ausgiebig verwendet, um auf die Werte zuzugreifen. Die Daten werden axios als Parameter als Javascript-Objekt zur Verfügung gestellt.

Für GET- Anforderungen mit einem optionalen Parameter stehen die Daten mit req.query zur Verfügung . Für PUT-, POST- und DELETE- Anforderungen stehen die Daten mit req.body direkt im Hauptteil der Anforderung zur Verfügung . Die Daten sind ein Javascript-Objekt und auf jede Eigenschaft kann mit regulärer Punktnotation zugegriffen werden.

res: steht für Antwort und enthält die Express-Server- Antwort. Wir möchten die Antwort, die wir von unserer Datenbank erhalten, an den Client senden, damit wir die Datenbankantwort an diese res-Funktion übergeben, die sie dann an unseren Client sendet.

next: ist eine Middleware, mit der Sie Rückrufe an die nächste Funktion weiterleiten können.

Beachten Sie, dass wir innerhalb unserer Express-Route, die wir ausführen, pool.querydasselbe poolObjekt sind, das unsere Datenbank- Anmeldeinformationen enthält , die wir zuvor eingerichtet und oben importiert haben. Mit der Abfragefunktion können wir SQL-Abfragen im Zeichenfolgenformat an unsere Datenbank senden. Beachten Sie auch, dass ich `` keine Anführungszeichen verwende, wodurch ich meine Abfrage in mehreren Zeilen haben kann.

Dann haben wir nach unserer SQL-Abfrage ein Komma und den nächsten Parameter, eine Pfeilfunktion, die nach dem Ausführen der Abfrage ausgeführt werden soll . wir zunächst in zwei Parameter in unseren Pfeil Funktion übergeben, q_errund das q_resheißt die Abfrage Fehler und die Abfrageantwort . Um Daten an das Frontend zu senden, übergeben wir diese q_res.rowsan die res.jsonFunktion. q_res.rowsist die Datenbankantwort, da dies SQL ist und die Datenbank uns basierend auf unserer Abfrage übereinstimmende Zeilen zurückgibt. Wir konvertieren diese Zeilen dann in das JSON-Format und senden sie mit dem Parameter an unser Frontendres .

Wir können unseren SQL-Abfragen auch optionale Werte übergeben, indem wir ein Array nach der durch Komma getrennten Abfrage übergeben . Dann können wir auf die einzelnen Elemente in diesem Array in der SQL-Abfrage mit der Syntax zugreifen, $1wobei $1das erste Element im Array ist. $2würde auf das zweite Element im Array zugreifen und so weiter. Beachten Sie, dass es sich nicht um ein 0-basiertes System wie in Javascript handelt$0

Lassen Sie uns jede dieser Routen aufschlüsseln und eine kurze Beschreibung jeder Route geben.

Beiträge Routen

  • / api / get / allposts: Ruft alle unsere Beiträge aus der Datenbank ab.  ORDER BY date_created DESCermöglicht es uns, die neuesten Beiträge zuerst anzuzeigen.
  • / api / post / posttodb: Speichert einen Benutzerbeitrag in der Datenbank. Wir speichern die 4 benötigten Werte: Titel, Text, Benutzer-ID, Benutzername in einem Array von Werten.
  • / api / put / post: Bearbeitet einen vorhandenen Beitrag in der Datenbank. Wir verwenden den SQL- UPDATE   Befehl und übergeben alle Werte des Beitrags erneut. Wir suchen den Beitrag mit der Beitrags-ID, die wir von unserem Frontend erhalten.
  • / api / delete / postcomments: Löscht alle mit einem Beitrag verknüpften Kommentare. Aufgrund der Fremdschlüsseleinschränkung von PSQL müssen wir alle mit dem Beitrag verknüpften Kommentare löschen, bevor wir den eigentlichen Beitrag löschen können.
  • / api / delete / post: Löscht einen Beitrag mit der Beitrags-ID.
  • / api / put / liks : Wir stellen eine Put-Anfrage, um die Benutzer-ID des Benutzers, dem der Beitrag gefallen hat, zum like_user_idArray hinzuzufügen, und erhöhen dann die likesAnzahl um 1.

Kommentare Routen

  • / api / post / commenttodb: Speichert einen Kommentar in der Datenbank
  • / api / put / commenttodb: Bearbeitet einen vorhandenen Kommentar in der Datenbank
  • / api / delete / comment: Löscht einen einzelnen Kommentar. Dies unterscheidet sich vom Löschen aller mit einem Beitrag verknüpften Kommentare.
  • / api / get / allpostcomments: Ruft alle Kommentare ab, die einem einzelnen Beitrag zugeordnet sind

Benutzerrouten

  • / api / posts / userprofiletodb: Speichert Benutzerprofildaten von auth0 in unserer eigenen Datenbank. Wenn der Benutzer bereits existiert, tut PostgreSQL nichts.
  • / api / get / userprofilefromdb: Ruft einen Benutzer ab, indem er seine E-Mail- Adresse nachschlägt
  • / api / get / userposts: Ruft Beiträge eines Benutzers ab, indem alle Beiträge nachgeschlagen werden , die seiner Benutzer-ID entsprechen.
  • / api / get / otheruserprofilefromdb: Profildaten anderer Benutzer aus der Datenbank abrufen und auf ihrer Profilseite anzeigen.
  • / api / get / otheruserposts: Ruft Beiträge anderer Benutzer ab, wenn Sie deren Profilseite anzeigen

Aufbau eines globalen Zustands mit Reduzierern, Aktionen und Kontext.

Speichern von Benutzerprofildaten in unserer Datenbank

Bevor wir mit dem Einrichten des globalen Status beginnen können, müssen wir unsere Benutzerprofildaten in unserer eigenen Datenbank speichern. Derzeit beziehen wir unsere Daten nur von auth0. Wir werden dies in unserer authcheck.jsKomponente tun .

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;

Wir haben den größten Teil dieser Komponente im letzten Tutorial eingerichtet. Ich empfehle daher, dieses Tutorial für eine detaillierte Erklärung zu lesen. Hier führen wir jedoch eine Axios-Post-Anfrage durch, gefolgt von einer weiteren Axios-Get-Anfrage , um die Benutzerprofildaten, die wir gerade in der Datenbank gespeichert haben, sofort abzurufen .

Wir tun dies, weil wir die eindeutige Primärschlüssel-ID benötigen, die von unserer Datenbank generiert wird, und dies ermöglicht es uns, diesen Benutzer mit seinen Kommentaren und Beiträgen zu verknüpfen . Und wir verwenden die E-Mail-Adresse des Benutzers, um sie nachzuschlagen, da wir nicht wissen, wie ihre eindeutige ID lautet, wenn sie sich zum ersten Mal anmelden. Schließlich speichern wir diese Benutzerprofildaten der Datenbank in unserem globalen Status.

* Beachten Sie, dass dies auch für OAuth-Anmeldungen wie Google- und Facebook-Anmeldungen gilt.

Aktionen und Reduzierungen

Wir können jetzt mit dem Einrichten der Aktionen und Reduzierungen zusammen mit dem Kontext beginnen, um den globalen Status für diese App einzurichten.

Informationen zum Einrichten des Kontexts von Grund auf finden Sie in meinem vorherigen Tutorial. Hier benötigen wir nur den Status für das Datenbankprofil und alle Beiträge.

Zuerst unsere Aktionstypen

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"

Nun unsere Handlungen

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

Endlich unser Post Reducer und Auth Reducer

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

Jetzt müssen wir diese dem hinzufügen  

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

Das ist es, wir sind jetzt bereit, diesen globalen Zustand in unseren Komponenten zu verwenden.

Client Side React App

Als nächstes richten wir den clientseitigen Reaktionsblog ein. Alle API-Aufrufe in diesem Abschnitt wurden im vorherigen Abschnitt für Expressrouten eingerichtet.

Es wird wie folgt in 6 Komponenten eingerichtet.

addpost.js : Eine Komponente mit einem Formular zum Senden von Posts.

editpost.js : Eine Komponente zum Bearbeiten von Posts mit einem Formular, in dem bereits Felder ausgefüllt sind .

posts.js : Eine Komponente zum Rendern aller Beiträge, wie in einem typischen Forum.

showpost.js : Eine Komponente zum Rendern eines einzelnen Beitrags, nachdem ein Benutzer auf einen Beitrag geklickt hat.

profile.js : Eine Komponente, die Beiträge rendert, die einem Benutzer zugeordnet sind. Das Benutzer-Dashboard.

showuser.js : Eine Komponente, die Profildaten und Beiträge eines anderen Benutzers anzeigt.

Warum nicht Redux Form verwenden?

TDLR; Redux Form ist für die meisten Anwendungsfälle übertrieben.

Redux Form ist eine beliebte Bibliothek, die häufig in React-Apps verwendet wird. Warum also nicht hier verwenden? Ich habe Redux Form ausprobiert, aber ich konnte hier einfach keinen Anwendungsfall dafür finden. Wir müssen immer die endgültige Verwendung berücksichtigen, und ich konnte mir kein Szenario für diese App ausdenken, in dem wir die Formulardaten im globalen Redux-Status speichern müssten.

In dieser App nehmen wir die Daten einfach aus einem regulären Formular und übergeben sie an Axios, der sie dann an den Express-Server weiterleitet, der sie schließlich in der Datenbank speichert. Der andere mögliche Anwendungsfall betrifft eine Editpost-Komponente, die ich durch Übergabe der Post-Daten an eine Eigenschaft des Link-Elements verarbeite.

Probieren Sie Redux Form aus und sehen Sie, ob Sie eine clevere Verwendung dafür finden können, aber wir werden es in dieser App nicht benötigen. Auch jede von Redux Form angebotene Funktionalität kann ohne sie relativ einfach ausgeführt werden.

Die Redux-Form ist für die meisten Anwendungsfälle einfach übertrieben.

Wie bei einem ORM gibt es keinen Grund, unserer App eine weitere unnötige Komplexitätsebene hinzuzufügen.

Es ist einfach einfacher, Formulare mit regulärem React einzurichten.

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;

In der Addpost-Komponente haben wir ein einfaches 2-Feld-Formular, in das ein Benutzer einen Titel und einen Text eingeben kann. Das Formular wird mit der von handlesubmit()uns erstellten Funktion gesendet. Die handleSubmit()Funktion verwendet ein Ereignisparameter-Schlüsselwort, das die vom Benutzer übermittelten Formulardaten enthält.

Wir werden verwenden event.preventDefault(), um das Neuladen der Seite zu verhindern, da React eine Einzelseiten-App ist und dies nicht erforderlich wäre.

Die Axios-Post- Methode verwendet einen Parameter von "Daten", der zum Speichern der Daten verwendet wird, die in der Datenbank gespeichert werden. Wir erhalten den Benutzernamen und die Benutzer- ID aus dem globalen Status, den wir im letzten Abschnitt besprochen haben.

Das tatsächliche Posten der Daten in der Datenbank wird in der Expressroutenfunktion mit SQL-Abfragen behandelt, die wir zuvor gesehen haben. Unser Axios-API-Aufruf leitet die Daten dann an unseren Express-Server weiter, der die Informationen in der Datenbank speichert.

editpost.js

Als nächstes haben wir unsere editpost.jsKomponente. Dies ist eine grundlegende Komponente zum Bearbeiten von Benutzerbeiträgen. Der Zugriff ist nur über die Benutzerprofilseite möglich.

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: ist eine Funktionalität, die vom React-Router angeboten wird . Wenn ein Benutzer auf seiner Profilseite auf einen Beitrag klickt, werden die von ihm angeklickten Beitragsdaten in einer Statuseigenschaft im Linkelement gespeichert und unterscheiden sich vom lokalen Komponentenstatus in Reagieren aus dem useStateHook.

Dieser Ansatz bietet uns eine einfachere Möglichkeit, die Daten im Vergleich zum Kontext zu speichern, und speichert uns auch eine API-Anfrage. Wir werden sehen, wie dies in der profile.jsKomponente funktioniert .

Danach haben wir ein grundlegendes gesteuertes Komponentenformular und speichern die Daten bei jedem Tastendruck im Status "Reagieren".

In unserer handleSubmit()Funktion kombinieren wir alle unsere Daten, bevor wir sie in einer Axios-Put-Anfrage an unseren Server senden.  

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;

Sie werden feststellen, dass wir einen ziemlich komplexen useEffect()Anruf haben, um unsere Beiträge aus unserer Datenbank zu erhalten. Dies liegt daran, dass wir unsere Beiträge aus unserer Datenbank im globalen Status speichern, sodass die Beiträge auch dann noch vorhanden sind, wenn ein Benutzer zu einer anderen Seite navigiert.

Auf diese Weise werden unnötige API-Aufrufe an unseren Server vermieden. Aus diesem Grund verwenden wir eine Bedingung, um zu überprüfen, ob die Beiträge bereits im Kontextstatus gespeichert sind.

Wenn die Posts bereits im globalen Status gespeichert sind, setzen wir die Posts im globalen Status einfach auf unseren lokalen Status, sodass wir die Paginierung initialisieren können.  

Seitennummerierung

Wir haben hier in der page_change()Funktion eine grundlegende Paginierungsimplementierung . Grundsätzlich haben wir unsere 5 Paginierungsblöcke als Array eingerichtet. Wenn sich die Seite ändert, wird das Array mit den neuen Werten aktualisiert. Dies ist in der ersten ifAnweisung in der page_change()Funktion zu sehen. Die anderen 4 ifAnweisungen dienen nur dazu, die ersten 2 und letzten 2 Seitenänderungen zu behandeln.

Wir müssen auch einen window.scrollTo()Anruf tätigen, um bei jedem Seitenwechsel nach oben zu scrollen.

Fordern Sie sich heraus, ob Sie eine komplexere Paginierungsimplementierung erstellen können, aber für unsere Zwecke ist diese einzelne Funktion hier für die Paginierung in Ordnung.

Wir brauchen 4 Zustandswerte für unsere Paginierung. Wir brauchen:

  • num_posts: Anzahl der Beiträge
  • posts_slice: Ein Teil der gesamten Beiträge
  • currentPage: die aktuelle Seite
  • posts_per_page: Die Anzahl der Beiträge auf jeder Seite.

Wir müssen auch den currentPageStatuswert an den useEffect()Hook übergeben, damit wir bei jedem Seitenwechsel eine Funktion auslösen können. Wir erhalten das, indexOfLastPost indem wir das 3-fache multiplizieren currentPageund den indexOfFirstPostBeitrag, den wir anzeigen möchten, durch Subtrahieren von 3 erhalten. Wir können dann dieses neue geschnittene Array als neues Array in unserem lokalen Zustand festlegen.

Nun zu unserem JSX. Wir verwenden Flexbox zum Strukturieren und Layouten unserer Paginierungsblöcke anstelle der üblichen horizontalen Listen, die traditionell verwendet werden.

Wir haben 4 Schaltflächen, mit denen Sie zur ersten Seite gehen oder eine Seite zurückblättern können und umgekehrt. Dann verwenden wir eine map-Anweisung in unserem pages_sliceArray, die uns die Werte für unsere Paginierungsblöcke gibt. Ein Benutzer kann auch auf einen Paginierungsblock klicken, der auf der Seite als Argument an die page_change()Funktion übergeben wird.

Wir haben auch CSS- Klassen, mit denen wir auch das Styling unserer Paginierung festlegen können.  

  • .pagination-active: Dies ist eine reguläre CSS-Klasse anstelle eines Pseudo-Selektors, den Sie normalerweise bei horizontalen Listen wie z .item:active. Wir schalten die aktive Klasse in React JSX um, indem wir sie currentPagemit der Seite im pages_sliceArray vergleichen.
  • .pagination-item: Styling für alle Paginierungsblöcke
  • .pagination-item:hover: Styling, das angewendet werden soll, wenn der Benutzer mit der Maus über einen Paginierungsblock fährt
 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

ist die funktionale Komponente, mit der wir jeden einzelnen Beitrag rendern. Der Titel der Beiträge ist ein Titel, Linkder beim Klicken einen Benutzer zu jedem einzelnen Beitrag mit Kommentaren führt. Sie werden auch feststellen, dass wir den gesamten Beitrag an die stateEigenschaft des LinkElements übergeben. Diese stateEigenschaft unterscheidet sich von unserem lokalen Bundesstaat. Dies ist tatsächlich eine Eigenschaft von react-routerund wir werden dies in der showpost.jsKomponente genauer sehen . Dasselbe machen wir auch mit dem Autor des Beitrags.

Sie werden auch einige andere Dinge im Zusammenhang mit der Suche nach Beiträgen bemerken, die ich in den späteren Abschnitten besprechen werde.  

Ich werde auch die "Likes" -Funktionalität in der showpost.jsKomponente diskutieren .

showpost.js

Jetzt haben wir hier die mit Abstand komplexeste Komponente in dieser App. Keine Sorge, ich werde es Schritt für Schritt komplett auflösen, es ist nicht so einschüchternd, wie es aussieht.  

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;

Sie werden zuerst einen gigantischen useStateAnruf bemerken . Ich werde erklären, wie jede Eigenschaft funktioniert, wenn wir stattdessen hier auf einmal unsere Komponente untersuchen.

useEffect () - und API-Anforderungen

Als erstes müssen wir uns bewusst sein, dass ein Benutzer auf zwei verschiedene Arten auf einen Beitrag zugreifen kann. Zugriff über das Forum oder Navigation über die direkte URL .  

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

Wenn sie über das Forum darauf zugreifen, überprüfen wir dies in unserem useEffect()Anruf und setzen dann unseren lokalen Status auf den Beitrag. Da wir diestate Eigenschaft des Reaktionsrouters im Element verwenden, haben wir über Requisiten Zugriff auf die gesamten Postdaten, die uns bereits zur Verfügung stehen, was uns einen unnötigen API-Aufruf erspart.

Wenn der Benutzer die direkte URL für einen Beitrag im Browser eingibt, haben wir keine andere Wahl, als eine API-Anfrage zu stellen, um den Beitrag zu erhalten, da ein Benutzer auf einen Beitrag aus dem posts.jsForum klicken muss , um die Beitragsdaten für die Reaktion zu speichern. Router- stateEigenschaft.

Wir extrahieren zuerst die Post-ID aus der URL mit der pathnameEigenschaft des React -Routers , die wir dann als Parameter in unserer Axios-Anfrage verwenden . Nach der API-Anfrage speichern wir einfach die Antwort in unserem lokalen Status.

Danach müssen wir die Kommentare auch mit einer API-Anfrage erhalten . Wir können dieselbe URL-Extraktionsmethode für Post-IDs verwenden, um Kommentare zu suchen, die einem Post zugeordnet sind.

RenderComments und Animationen

Hier haben wir unsere Funktionskomponente, mit der wir einen individuellen Kommentar anzeigen.

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

Wir beginnen mit der Verwendung eines ternären Ausdrucks in der classNameRequisite von div, um Stilklassen umzuschalten. Wenn die delete_comment_idin unserem lokalen Status mit der aktuellen Kommentar-ID übereinstimmt, wird sie gelöscht und eine Ausblendanimation wird auf den Kommentar angewendet.

Wir @keyframemachen die Animationen. Ich finde CSS- @keyframeAnimationen viel einfacher als Javascript-basierte Ansätze mit Bibliotheken wie react-springund react-transition-group.

Als nächstes haben wir den aktuellen Kommentar angezeigt

Gefolgt von einem ternären Ausdruck, der entweder das Erstellungsdatum des Kommentars "Bearbeitet" oder "Gerade jetzt" basierend auf den Aktionen des Benutzers festlegt.  

Als nächstes haben wir einen ziemlich komplexen verschachtelten ternären Ausdruck. Wir vergleichen zuerst die cur_user_id(die wir von unserem context.dbProfileStateStatus erhalten und in unserer JSX festlegen) mit der Kommentar-Benutzer-ID . Wenn es eine Übereinstimmung gibt, wird eine Schaltfläche zum Bearbeiten angezeigt .

Wenn der Benutzer auf die Schaltfläche Bearbeiten klickt , setzen wir den Kommentar auf den edit_commentStatus und den edit_comment_idStatus auf die Kommentar-ID . Dadurch wird auch die isEditing- Requisite auf true gesetzt, wodurch das Formular aufgerufen wird und der Benutzer den Kommentar bearbeiten kann. Wenn der Benutzer auf Zustimmen klickt, wird die handleUpdate()Funktion aufgerufen, die wir als nächstes sehen werden.

Kommentare CRUD-Operationen

Hier haben wir unsere Funktionen zur Behandlung von CRUD-Operationen für Kommentare. Sie werden sehen, dass wir zwei Funktionssätze haben , einen für die Verarbeitung von clientseitigem CRUD und einen für API-Anforderungen . Ich werde unten erklären, warum.

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

Dies liegt daran, dass die Benutzeroberfläche nicht aktualisiert wird, ohne dass die Seite neu geladen wird, wenn ein Benutzer einen Kommentar einreicht, bearbeitet oder löscht . Sie können dieses Problem lösen, indem Sie eine weitere API-Anfrage stellen oder ein Web-Socket-Setup durchführen, das auf Änderungen an der Datenbank wartet. Eine weitaus einfachere Lösung besteht jedoch darin, sie clientseitig programmgesteuert zu behandeln.  

Alle clientseitigen CRUD-Funktionen werden in ihren jeweiligen API-Aufrufen aufgerufen.

Client Side CRUD:

  • handleCommentSubmit(): Aktualisieren Sie das, comments_arrindem Sie einfach den Kommentar am Anfang des Arrays hinzufügen.  
  • handleCommentUpdate(): Find and replace the comment in the array with the index then update and set the new array to the comments_arr
  • handleCommentDelete(): Find the comment in the array with the comment id then .filter() it out and save the new array to comments_arr.

API requests:

  • handleSubmit(): we are getting our data from our form, then combining the different properties we need, and sending that data to our server. The data and submitted_comment variables are different because our client side CRUD operations need slightly different values than our database.
  • handleUpdate(): this function is nearly identical to our handleSubmit() function. the main difference being that we are doing a put request instead of a post.
  • handleDeleteComment(): simple delete request using the comment id.  

handling Likes

Now we can discuss how to handle when a user likes a post.

 .... const handleLikes = () => { const user_id = context.dbProfileState[0].uid const post_id = stateLocal.post_id const data = { uid: user_id, post_id: post_id } console.log(data) if(!stateLocal.like_user_ids.includes(user_id)) { axios.put('/api/put/likes', data) .then( !stateLocal.like_user_ids.includes(user_id) && stateLocal.like_post ? setState({...stateLocal, likes: stateLocal.likes + 1, like_post: false}) : null ) .catch(err => console.log(err)) }; } .... handleLikes() : () => history.replace('/signup')}>thumb_up  {stateLocal.likes}  ....
.notification-num-showpost { position:relative; padding:5px 9px; background-color: red; color: #941e1e; bottom: 23px; right: 5px; z-index: -1; border-radius: 50%; }

in the handleLikes() function we first set the post id and user id. Then we use a conditional to check if the current user id is not in the like_user_id array which remember has all the user ids of the users who have already liked this post.

If not then we make a put request to our server and after we use another conditional and check if the user hasnt already liked this post client side with the like_post state property then update the likes.  

In the JSX we use an onClick event in our div to either call the handleLikes() function or redirect to the sign up page. Then we use a material icon to show the thumb up icon and then style it with some CSS.

That's it! not too bad right.

profile.js

Now we have our profile.js component which will essentially be our user dashboard. It will contain the users profile data on one side and their posts on the other.

The profile data we display here is different than the dbProfile which is used for database operations. We use the other profile here we are getting from auth0 (or other oauth logins) because it contains data we dont have in our dbProfile. For example maybe their Facebook profile picture or nickname.

import React, { useContext, useState, useEffect } from 'react'; import Context from '../utils/context'; import { Link } from 'react-router-dom'; import history from '../utils/history'; import axios from 'axios'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; import Button from '@material-ui/core/Button'; const Profile = () => { const context = useContext(Context) const [stateLocal, setState] = useState({ open: false, post_id: null, posts: [] }) useEffect(() => { const user_id = context.dbProfileState[0].uid axios.get('/api/get/userposts', {params: { user_id: user_id}}) .then((res) => setState({...stateLocal, posts: [...res.data] })) .catch((err) => console.log(err)) }) const handleClickOpen = (pid) => { setState({open: true, post_id: pid }) } const handleClickClose = () => { setState({open: false, post_id: null }) } const DeletePost = () => { const post_id = stateLocal.post_id axios.delete('api/delete/postcomments', {data: { post_id: post_id }} ) .then(() => axios.delete('/api/delete/post', {data: { post_id: post_id }} ) .then(res => console.log(res) ) ) .catch(err => console.log(err)) .then(() => handleClickClose()) .then(() => setTimeout(() => history.replace('/'), 700 ) ) } const RenderProfile = (props) => { return( 

{props.profile.profile.nickname}

{props.profile.profile.email}

{props.profile.profile.name}

Email Verified:
{props.profile.profile.email_verified ?

Yes

:

No

}

) } const RenderPosts = post => ( Delete } />

{post.post.body} ); return( {stateLocal.posts ? stateLocal.posts.map(post => ) : null } Confirm Delete? Deleteing Post DeletePost() }> Agree handleClickClose()}> Cancel )} export default (Profile);

 .FlexProfileDrawer { display: flex; flex-direction: row; margin-top: 20px; margin-left: -90px; margin-right: 25px; } .FlexColumnProfile > h1 { text-align: center; } FlexProfileDrawerRow { display: flex; flex-direction: row; margin: 10px; padding-left: 15px; padding-right: 15px; } .FlexColumn { display: flex; flex-direction: column; } .FlexRow { display: flex; flex-direction: row; }

The vast majority of this functionality in this component we have seen before. We begin by making an API request in our useEffect() hook to get our posts from the database using the user id then save the posts to our local state.

Then we have our functional component. We get the profile data during the authentication and save it to global state so we can just access it here without making an API request.  

Then we have which displays a post and allows a user to go to, edit or delete a post. They can go to the post page by clicking on the title. Clicking on the edit button will take them to the editpost.js component and clicking on the delete button will open the dialog box.

In the DeletePost() function we first delete all the comments associated with that post using the post id. Because if we just deleted the post without deleting the comments we would just have a bunch of comments sitting in our database without a post. After that we just delete the post.

showuser.js

Now we have our component that displays another users posts and comments when a user clicks on their name in the forum.

import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import axios from 'axios'; import moment from 'moment'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import Button from '@material-ui/core/Button'; const ShowUser = (props) => { const [profile, setProfile ] = useState({}) const [userPosts, setPosts ] = useState([]) useEffect(() => { const username = props.location.state.post.post.author axios.get('/api/get/otheruserprofilefromdb', {params: {username: username}} ) .then(res => setProfile({...res.data} )) .catch(function (error) { console.log(error); }) axios.get('/api/get/otheruserposts', {params: {username: username}} ) .then(res => setPosts([...res.data])) .catch(function (error) { console.log(error); }) window.scrollTo({top: 0, left: 0}) }, [props.location.state.post.post.author] ) const RenderProfile = (props) => ( 

{props.profile.username}

Send Message ); const RenderPosts = (post) => ( { post.post.body } ); return ( {profile ? : null }


Latest Activity:

{ userPosts ? userPosts.map(post =>

) : null } ) } export default (ShowUser);

We begin with 2 API requests in our useEffect() hook since we will need both the other user's profile data and their posts, and then save it to the local state.

We get the user id with react-routers state property that we saw in the showpost.js component.

We have our usual and functional components that display the Profile data and posts. And then we just display them in our JSX.

This is it for this component, there wasn't anything new or ambiguous here so I kept it brief.

Admin App

No full stack blog is complete without an admin app so this is what we will setup next.

Below is a diagram that will show essentially how an admin app will work. It is possible to just have your admin app on different routes within your regular app but having it completely separated in its own app makes both your apps much more compartmentalized and secure.

So the admin app will be its own app with its own authentication but connect to the same database as our regular app.

Admin App authentication

Authentication for the admin app will be a little bit different than our regular app. The main difference being that there will be no sign-up option on the admin app, admins will have to be added manually. Since we dont want random people signing up for our admin app.

Similar to the regular app, I will use Auth0 for authentication.

First we will start on the admin dashboard.

Next click on the create application button.

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

We will call our new connection “adminapp2db”.

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

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

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

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

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

If successful you should be seeing this:

Which means our authentication is setup and only admins we added manually can log in. Great!

Global Edit and Delete Privileges

One of the main functionalities of an admin app will be to have global edit delete privileges which will allow an admin or moderator to make edits to user's posts and comments or to delete spam. This is what we will build here.

The basic idea of how we will do this is to remove the authentication check to edit and delete posts and comments, but at the same time making sure the post and comment still belongs to its original author.

We dont have to start from scratch we can use the same app we have been building in the previous sections and add some admin specific code.

The very first thing we can do is get rid of the "sign up to add post/comments" buttons in our addpost.js and showpost.js component since an admin cant sign up for this app by themselves.  

next in our editpost.js component in the handleSubmit() function we can access the user_id and username with the react-router props that we have seen before.

This will ensure that even though we edit the post as an admin, it still belongs to the original user.

const handleSubmit = (event) => { event.preventDefault() const user_id = props.location.state.post.post.user_id const username = props.location.state.post.post.author const pid = props.location.state.post.post.pid const title = event.target.title.value const body = event.target.body.value const data = {title: title, body: body, pid: pid, uid: user_id, username: username } axios.put("/api/put/post", data) .then(res => console.log(res)) .catch(err => console.log(err)) .then(setTimeout(() => history.replace('/'), 700 )) }

The addpost.js component can be left as is, since an admin should be able to make posts as normal.

Back in our posts.js component we can add edit and delete buttons to our function.

.... const RenderPosts = post => ( ...   Edit    deletePost(post.post.pid)}> Delete ) ....

This functionality was only available on the user dashboard in our regular app, but we can implement directly in the main forum for our admin app, which gives us global edit and delete privileges on all the posts.

The rest of the posts.js component can be left as is.

Now in our showpost.js component the first thing we can do is remove the comparison of the current user id to the comment user id that allows for edits.

.... // props.cur_user_id === props.comment.user_id const RenderComments = (props) => { return( {true ? !props.isEditing ? ....

Next in the handleUpdate() function we can set the user name and user id to the original author of the comment.  

.... const handleUpdate = (event, cid, commentprops) => { event.preventDefault() .... const user_id = commentprops.userid const username = commentprops.author ....

Our server and database can be left as is.

This is it! we have implemented global edit and delete functionality to our app.

Admin Dashboard

Another very common feature in admin apps is to have a calendar with appointments times and dates, which is what we will have to implement here.

We will start with the server and SQL.

 CREATE TABLE appointments ( aid SERIAL PRIMARY KEY, title VARCHAR(10), start_time TIMESTAMP WITH TIME ZONE UNIQUE, end_time TIMESTAMP WITH TIME ZONE UNIQUE );

We have a simple setup here. We have the PRIMARY KEY. Then the title of the appointment. After that we have start_time and end_time. TIMESTAMP WITH TIME ZONE gives us the date and time, and we use the UNIQUE keyword to ensure that there cant be duplicate appointments.

/* DATE APPOINTMENTS */ router.post('/api/post/appointment', (req, res, next) => { const values = [req.body.title, req.body.start_time, req.body.end_time] pool.query('INSERT INTO appointments(title, start_time, end_time) VALUES($1, $2, $3 )', values, (q_err, q_res) => { if (q_err) return next(q_err); console.log(q_res) res.json(q_res.rows); }); }); router.get('/api/get/allappointments', (req, res, next) => { pool.query("SELECT * FROM appointments", (q_err, q_res) => { res.json(q_res.rows) }); });

Here we have our routes and queries for the appointments. For the sake of brevity I have omitted the edit and delete routes since we have seen those queries many times before. Challenge yourself to see if you can create those queries. These are basic INSERT and SELECT statements nothing out of the ordinary here.

We can now go to our client side.

At the time of this writing I couldn't find a good Calendar library that would work inside of a React Hooks component so I decided to just implement a class component with the react-big-calendar library.

It will still be easy to follow along, we wont be using Redux or any complex class functionality that isnt available to React hooks.

componentDidMount() is equivalent to useEffect(() => {}, [] ) . The rest of the syntax is basically the same expect you add the this keyword at the beginning when accessing property values.

I will replace the regular profile.js component with the admin dashboard here, and we can set it up like so.

//profile.js import React, { Component } from 'react' import { Calendar, momentLocalizer, Views } from 'react-big-calendar'; import moment from 'moment'; import 'react-big-calendar/lib/css/react-big-calendar.css'; import history from '../utils/history'; import Button from '@material-ui/core/Button'; import Paper from '@material-ui/core/Paper'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; import axios from 'axios'; const localizer = momentLocalizer(moment) const bus_open_time = new Date('07/17/2018 9:00 am') const bus_close_time = new Date('07/17/2018 5:00 pm') let allViews = Object.keys(Views).map(k => Views[k]) class Profile extends Component { constructor(props) { super(props) this.state = { events: [], format_events: [], open: false, start_display: null, start_slot: null, end_slot: null } } componentDidMount() { axios.get('api/get/allappointments') .then((res) => this.setState({events: res.data})) .catch(err => console.log(err)) .then(() => this.dateStringtoObject()) } handleClickOpen = () => { this.setState({ open: true }); }; handleClose = () => { this.setState({ open: false }); }; dateStringtoObject = () => { this.state.events.map(appointment => { this.setState({ format_events: [...this.state.format_events, { id: appointment.aid, title: appointment.title, start: new Date(appointment.start_time), end: new Date(appointment.end_time) }]}) }) } handleAppointmentConfirm = () => { const time_start = this.state.start_slot const time_end = this.state.end_slot const data = {title: 'booked', start_time: time_start, end_time: time_end } axios.post('api/post/appointment', data) .then(response => console.log(response)) .catch(function (error) { console.log(error); }) .then(setTimeout( function() { history.replace('/') }, 700)) .then(alert('Booking Confirmed')) } showTodos = (props) => ( 

{ props.appointment.start.toLocaleString() }

) BigCalendar = () => ( alert(event.start)} onSelectSlot={slotInfo => { this.setState({start_slot: slotInfo.start, end_slot: slotInfo.end, start_display: slotInfo.start.toLocaleString() }); this.handleClickOpen(); }} /> ) render() { return (

Admin Dashboard

Appointments:

{ this.state.format_events ? this.state.format_events.map(appointment => ) : null }

{ this.state.format_events ? : null }


Confirm Appointment? Confirm Appointment: {this.state.start_display} this.handleAppointmentConfirm() }> Confirm this.handleClose() }> Cancel )} } export default (Profile);

We will start with our usual imports. Then we will initialize the calendar localizer with the moment.js library.

Next we will set the business open and close time which I have set at from 9:00 am to 5:00 pm in the bus_open_time and bus_close_time variables.

Then we set the allViews variable which will allow the calendar to have the months, weeks, and days views.

Next we have our local state variable in the constructor which is equivalent to the useState hook.

Its not necessary to understand constructors and the super() method for our purposes since those are fairly large topics.

Next we have our componentDidMount() method which we use to make an axios request to our server to get our appointments and save them to our events property of local state.  

handleClickOpen() and handleClose() are helper functions that open and close our dialog box when a user is confirming an appointment.

next we have dateStringToObject()  function which takes our raw data from our request and turns it into a usable format by our calendar.  format_events is the state property to hold the formatted events.

after that we have the handleAppointmentConfirm() function. We will use this function to make our API request to our server. These values we will get from our component which we will see in a second.

our is how we display each appointment.

Next we have our actual calendar. Most of the props should be self explanatory, but 2 we can focus on are onSelectEvent and onSelectSlot.

onSelectEvent is a function that is called every time a user clicks on an existing event on the calendar, and we just alert them of the event start time.

onSelectSlot is a function that is called every time a user clicks an empty slot on the calendar, and this is how we get the time values from the calendar. When the user clicks on a slot we save the time values that are contained in the slotInfo parameter to our local state, then we open a dialog box to confirm the appointment.

Our render method is fairly standard. We display our events in a element and have the calendar below. We also have a standard dialog box that allows a user to confirm or cancel the request.

And thats it for the admin dashboard. You should have something that looks like this:

Deleting users along with their posts and comments

Now for the final part of this tutorial we can delete users and their associated comments and posts.

We will start off with our API requests. We have fairly simple DELETE statements here, I will explain more with the front end code.

 /* Users Section */ router.get('/api/get/allusers', (req, res, next) => { pool.query("SELECT * FROM users", (q_err, q_res) => { res.json(q_res.rows) }); }); /* Delete Users and all Accompanying Posts and Comments */ router.delete('/api/delete/usercomments', (req, res, next) => { uid = req.body.uid pool.query('DELETE FROM comments WHERE user_id = $1', [ uid ], (q_err, q_res) => { res.json(q_res); }); }); router.get('/api/get/user_postids', (req, res, next) => { const user_id = req.query.uid pool.query("SELECT pid FROM posts WHERE user_id = $1", [ user_id ], (q_err, q_res) => { res.json(q_res.rows) }); }); router.delete('/api/delete/userpostcomments', (req, res, next) => { post_id = req.body.post_id pool.query('DELETE FROM comments WHERE post_id = $1', [ post_id ], (q_err, q_res) => { res.json(q_res); }); }); router.delete('/api/delete/userposts', (req, res, next) => { uid = req.body.uid pool.query('DELETE FROM posts WHERE user_id = $1', [ uid ], (q_err, q_res) => { res.json(q_res); }); }); router.delete('/api/delete/user', (req, res, next) => { uid = req.body.uid console.log(uid) pool.query('DELETE FROM users WHERE uid = $1', [ uid ], (q_err, q_res) => { res.json(q_res); console.log(q_err) }); }); module.exports = router

And now for our component, you will notice we are using all our API requests in the handleDeleteUser() function.

import React, { useState, useEffect } from 'react' import axios from 'axios'; import history from '../utils/history'; import Button from '@material-ui/core/Button'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; const Users = () => { const [state, setState] = useState({ users: [], open: false, uid: null }) useEffect(() => { axios.get('api/get/allusers') .then(res => setState({users: res.data})) .catch(err => console.log(err)) }, []) const handleClickOpen = (user_id) => { setState({ open: true, uid: user_id }); }; const handleClose = () => { setState({ open: false }); }; const handleDeleteUser = () => { const user_id = state.uid axios.delete('api/delete/usercomments', { data: { uid: user_id }}) .then(() => axios.get('api/get/user_postids', { params: { uid: user_id }}) .then(res => res.data.map(post => axios.delete('/api/delete/userpostcomments', { data: { post_id: post.pid }})) ) ) .then(() => axios.delete('api/delete/userposts', { data: { uid: user_id }}) .then(() => axios.delete('api/delete/user', { data: { uid: user_id }} ) )) .catch(err => console.log(err) ) .then(setTimeout(history.replace('/'), 700)) } const RenderUsers = (user) => (

{ user.user.username }

{ user.user.email }

handleClickOpen(user.user.uid)}> Delete User ); return (

Users

User {state.users ? state.users.map(user => ) : null }
Delete User Deleteing User will delete all posts and comments made by user {handleDeleteUser(); handleClose()} }> Delete Cancel ) } export default (Users);

handleDeleteUser()

I will start off with the handleDeleteUser() function.  The first thing we do is define the user id of the user we want to delete which we get from local state. The user id is saved to local state when an admin clicks on a users name and the dialog box pops up.

The rational for this setup is because of PSQL's foreign key constraint, where we cant delete a row on a table that is being referenced by another table before we delete that other row first. See the PSQL foreign key constraint section for a refresher.

This is why we must work backwards and delete all the comments and posts associated with a user before we can delete the actual user.    

The very first axios delete request is to delete all the comments where there is a matching user id which we just defined. We do this because we cant delete the comments associated with posts before deleting the posts themselves.

In our first.then()statement we look up all the posts this user made and retrieve those post ids. You will notice that our second .then() statement is actually inside our first .then() statement. This is because we want the response of the axios.get('api/get/user_postids') request as opposed to response of the first axios delete request.

In our second .then()statement we are getting an array of the post ids of the posts associated with the user we want to delete and then calling .map() on the array. We are then deleting all the comments associated with that post regardless by which user it was made. This would make axios.delete('/api/delete/userpostcomments')  a triple nested axios request!

Our 3rd .then()statement is deleting the actual posts the user made.

Our 4th.then()statement is finally deleting the user from the database. Our 5th .then() is then redirecting the admin to the home page. Our 4th .then() statement is inside our 3rd.then()statement for the same reason as to why our 2nd.then()statement is inside our 1st.

Everything else is functionality we have seen several times before, which will conclude our tutorial!

Thanks for Reading!