Teljes veremreakció: Hogyan készítsünk saját blogot az Express, Hooks és Postgres használatával.

Ebben az oktatóanyagban egy teljes verem React blogot fogunk kiépíteni, valamint a blog adminisztrátorának hátterét.

Részletesen végigvezetem az összes lépést.

A bemutató végére elegendő tudással rendelkezik ahhoz, hogy meglehetősen összetett, teljes veremű alkalmazásokat hozzon létre modern eszközökkel: React, Express és PostgreSQL adatbázis.

Annak érdekében, hogy a dolgok tömörek legyenek, megcsinálom a minimális stílust / elrendezést, és ezt az olvasóra bízom.

Befejezett projekt:

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

Adminisztrációs alkalmazás:

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

Kezdő projekt:

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

Az indító projekt felépítése:

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

Hogyan adhatunk hozzá egy Fullstack keresőmotort a projekthez:

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

A bemutató videó verzióját itt tekintheti meg

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

Lépjen kapcsolatba velem a Twitteren, ha további frissítéseket szeretne kapni a jövőbeni oktatóanyagokról: //twitter.com/iqbal125sf

1. szakasz: Express Server és PSQL adatbázis beállítása

  1. Projekt felépítése
  2. Express Express beállítás
  3. Csatlakozás kliens oldalhoz

    axios vs reagál-router vs express router

    miért nem használ olyan ORM-et, mint a Sequelize?

  4. Az adatbázis beállítása

    PSQL idegen kulcsok

    PSQL shell

  5. Express Routes és PSQL lekérdezések beállítása

2. szakasz: Reagáljon a kezelőfelület beállításaira

  1. Globális állapot beállítása szűkítőkkel, műveletekkel és kontextussal.

    Felhasználói profiladatok mentése az adatbázisunkba

    Műveletek és reduktorok beállítása

  2. Ügyféloldali React alkalmazás

    addpost.js

    editpost.js

    posts.js

    showpost.js

    profile.js

    showuser.js

3. szakasz: Adminisztrátori alkalmazás

  1. Rendszergazda alkalmazás-hitelesítés
  2. Globális jogosultságok szerkesztése és törlése
  3. Rendszergazda irányítópultja
  4. Felhasználók törlése a bejegyzéseikkel és megjegyzéseikkel együtt

Projekt felépítése

Először a könyvtárstruktúra megvitatásával kezdjük. 2 könyvtárunk lesz, az Ügyfél és a Szerver könyvtár. Az Ügyfélkönyvtár tárolja az utolsó oktatóanyagban beállított React alkalmazásunk tartalmát, a Szerver pedig a expressszerverünk tartalmát és az adatbázisunkba kerülő API-hívások logikáját. A Kiszolgáló könyvtár a sémát az SQL adatbázisunkhoz is megtartja .

A Final Directory felépítése így fog kinézni.

Express Express beállítás

Ha még nem tette meg, telepítheti express-generatora parancsot:

npm install -g express-generator

Ez egy egyszerű eszköz, amely egy egyszerű expressz projektet generál egy egyszerű paranccsal, hasonló a create-react-app. Ez egy kis időt megspórol nekünk abban, hogy mindent a nulláról kelljen beállítanunk.

Kezdhetjük a expressparancs futtatásával a Szerver könyvtárban. Ez kap egy alapértelmezett expressz alkalmazást, de nem használjuk az alapértelmezett konfigurációt, amelyet módosítanunk kell.

Először töröljük az útvonal mappát, a nézetek mappát és a nyilvános mappát. Nincs szükségünk rájuk. Már csak 3 fájl maradhat. A kuka könyvtárában található www fájl , a fájl és a fájl. Ha véletlenül törölte ezeket a fájlokat, egyszerűen hozzon létre egy másik expressz projektet. Mivel töröltük ezeket a mappákat, egy kicsit módosítanunk kell a kódot is. Refaktorálja a fájlt az alábbiak szerint:app.jspackage.jsonapp.js

 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; 

A mainapp.js nevű mappába is helyezhetünk .

Ezután meg kell változtatnunk a www fájl alapértelmezett portját másra, mint a 3000-es portra, mivel ez az alapértelmezett port, amelyen a React front end alkalmazásunk futni fog.

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

Az expressz alkalmazás létrehozásával kapott függőségeken kívül további 3 könyvtárat is felveszünk a segítségünkre:

cors: ezt a könyvtárat fogjuk használni a React App és az Express kiszolgáló közötti kommunikáció elősegítésére. Ezt a React alkalmazás proxyján keresztül fogjuk megtenni. Enélkül Cross Origin Resource hibát kapunk a böngészőben.

helmet: Biztonsági könyvtár, amely frissíti a http fejléceket. Ez a könyvtár biztonságosabbá teszi a http-kéréseinket.

pg: Ez a fő könyvtár, amellyel kommunikálni fogunk a psql adatbázisunkkal. E könyvtár nélkül az adatbázissal nem lehet kommunikálni.

folytathatjuk és telepíthetjük ezeket a könyvtárakat

npm install pg helmet cors

Befejeztük a minimális szerver beállítását, és ehhez hasonló projektstruktúrával kell rendelkeznünk.

Most tesztelhetjük, hogy működik-e a szerverünk. A kiszolgálót kliensoldali alkalmazás nélkül futtatja . Az Express egy teljesen működő alkalmazás, amely az ügyféloldali alkalmazástól függetlenül fog futni . Ha helyesen tette, ezt látnia kell a terminálján.

A szervert folyamatosan működtethetjük, mert hamarosan használni fogjuk.

Csatlakozás az ügyfél oldalhoz

Ügyféloldali alkalmazásunk összekapcsolása a szerverünkkel nagyon egyszerű, és csak egy kódsorra van szükségünk. Nyissa meg a package.jsonfájlt az Ügyfél könyvtárban, és írja be a következőket:

“proxy”: “//localhost:5000"

És ez az! Ügyfelünk most proxy-n keresztül kommunikálhat szerverünkkel.

** Megjegyzés: Ne feledje, hogy ha a wwwfájlban a port: 5000 mellett másik portot állít be, akkor inkább a proxyban használja azt a portot.

Itt van egy diagram, amely lebontja és elmagyarázza, hogy mi történik és hogyan működik.

A localhost: 3000 lényegében kérjen mintha localhost: 5000 egy proxy közvetítő ami pedig lehetővé teszi a szerver kommunikál a kliens .

Ügyféloldalunk most csatlakozik a szerverünkhöz, és most szeretnénk tesztelni az alkalmazásunkat.

Most vissza kell térnünk a szerver oldalára, és be kell állítanunk az expressútválasztást. Az Ön mappát a szerver könyvtárban hozzon létre egy új fájlt úgynevezett routes.js. Ez a fájl tárolja az összes expressútvonalat. amelyek lehetővé teszik számunkra az adatok küldését a kliens oldali alkalmazásunkhoz . Most egy nagyon egyszerű útvonalat állíthatunk be:

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

Lényegében, ha API-hívást kezdeményeznek az /helloútvonalon, az Express kiszolgálónk egy json formátumú „hello world” húrral válaszol.

Az app.jsexpressz útvonalak használatához át kell alakítanunk a fájlunkat .

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;

Most a home.jskomponensünkben található ügyféloldali kódról :

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;

Alapvető axioslekérési kérelmet küldünk futó expressszerverünknek, ha működik, akkor a "hello world" -t kellene látnunk a képernyőn.

És igen, működik, sikeresen beállítottunk egy React Node Fullstack alkalmazást!

Mielőtt folytatnánk szeretnék foglalkozni egy pár kérdése lehet, hogy ami mi a különbség a axios, react routerés express routermiért nem vagyok használ ORM mint Sequelize .

Axios vs Express Router vs React Router

TLDR; Az általunk használt react routernavigálni a mi app, az általunk használt axioskommunikálni a expressszerver és mi használja a expressszerver kommunikálni az adatbázisunkban.

Ezen a ponton felmerülhet a kérdés, hogy ez a 3 könyvtár hogyan működik együtt. Szerver háttérképünkkel axiosvaló kommunikációhoz használjuk , és expressfelhívjuk a expressszerverünket azáltal, hogy az „/ api /” szót feltesszük az URI-be. axiosarra is használható, hogy közvetlen http-kéréseket küldjön bármely háttérprogram végpontjához. Biztonsági okokból azonban nem ajánlott az ügyféltől kéréseket benyújtani az adatbázisba.

express routerelsősorban az adatbázisunkkal való kommunikációra használják, mivel SQL lekérdezéseket adhatunk át a express routerfüggvény törzsében . expressA Node-szal együtt a böngészőn kívül futtatják a kódot, ami lehetővé teszi az SQL-lekérdezéseket. expressemellett biztonságosabb módszer a http-kérelmek elkészítésére az axiós helyett.

Az axiosaszinkron http-kérések kezeléséhez azonban a React kliens oldalon van szükségünk , nyilvánvalóan nem tudjuk használni express router a React kliens oldalán. axiosa Promise alapú, így automatikusan kezelni az aszinkron műveleteket is.

Az általunk használt react-routernavigálni a mi app, mivel reagál az egyetlen oldalra app a böngésző nem tölti újra fel egy oldalt változás. Alkalmazásunk a kulisszák mögötti technikával rendelkezik, amely automatikusan tudja, ha útvonalat kérünk a expressvagy keresztül react-router.

Miért ne használna olyan ORM könyvtárat, mint a Sequelize?

TLDR; Előnyben részesíti az SQL-vel való közvetlen együttműködést, amely nagyobb ellenőrzést tesz lehetővé, mint az ORM. Több tananyag az SQL számára, mint egy ORM. Az ORM készségek nem átvihetők, az SQL készségek nagyon átruházhatók.

Számos oktatóanyag mutatja be, hogyan lehet egy ORM könyvtárat SQL-adatbázissal használni. Nincs ezzel semmi baj, de én személy szerint inkább közvetlenül lépek kapcsolatba az SQL-szel. Közvetlenül az SQL-szel dolgozva finomabb részletekbe menő kontrollt biztosít a kód felett, és úgy gondolom, hogy ez megéri a nehézségek enyhe növekedését, amikor közvetlenül az SQL-szel dolgozunk.

Sokkal több erőforrás található az SQL-en, mint bármelyik ORM könyvtárban, így ha kérdésed vagy hibád van, sokkal könnyebb megoldást találni.

Ezenkívül egy másik függőséget és absztrakciós szintet ad hozzá egy ORM könyvtárhoz, amely hibákat okozhat az úton. ORM használatakor nyomon kell követnie a frissítéseket és a változásokat, amikor a könyvtár megváltozik. Az SQL viszont rendkívül kiforrott és évtizedek óta létezik, ami azt jelenti, hogy valószínűleg nem lesz nagyon sok megtörő változása. Az SQL-nek is volt ideje finomítani és tökéletesíteni, ami általában nem az ORM könyvtárak esetében van.

Végül, egy ORM könyvtár tanulásához idő kell, és az ismeretek általában nem ruházhatók át másra. Az SQL a leggyakrabban használt adatbázis-nyelv nagyon széles különbséggel (utoljára a kereskedelmi adatbázisok körülbelül 90% -át ellenőriztem az SQL használatával). Egy SQL rendszer, például a PSQL elsajátítása lehetővé teszi, hogy ezeket a készségeket és tudást közvetlenül át tudja vinni egy másik SQL rendszerbe, például a MySQL-be.

Ez az oka annak, hogy nem használok ORM könyvtárat.

Az adatbázis beállítása

Kezdjük azzal, hogy az SQL sémát úgy állítjuk be, hogy létrehozunk egy fájlt a kiszolgáló könyvtár fő mappájában schema.sql.

Ez megtartja az adatbázis alakját és felépítését. Az adatbázis tényleges beállításához természetesen meg kell adnia ezeket a parancsokat a PSQL shellben. Egyszerűen az, hogy itt van a projektünkben egy SQL fájl, nem tesz semmit , egyszerűen csak hivatkozhatunk az adatbázisunk szerkezetére, és lehetővé tesszük más mérnökök számára, hogy hozzáférjenek az SQL parancsokhoz, ha használni akarják a kódunkat.

De ahhoz, hogy valóban működőképes adatbázisunk legyen, ugyanezekben a parancsokban adjuk meg a PSQL terminált.

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

Tehát itt van 3 táblázat, amely a felhasználók, az üzenetek és a megjegyzések adatait tárolja. Az SQL konvenciónak megfelelően az összes kisbetű a felhasználó által definiált oszlop- vagy táblanév, a nagybetűs pedig SQL-parancs.

ELSŐDLEGES KULCS : A psql által generált egyedi szám egy adott oszlophoz

VARCHAR (255) : változó karakter, vagy szöveg és számok. 255 a sor hosszát állítja be.

BOOLEAN : Igaz vagy hamis

HIVATKOZÁSOK : az idegen kulcs beállítása. Az idegen kulcs egy másik táblázat elsődleges kulcsa. Az alábbiakban ezt részletesebben elmagyarázom.

EGYEDI : Megakadályozza az oszlopok ismétlődő bejegyzéseit.

DEFAULT : alapértelmezett érték beállítása

INT [] DEFAULT ARRAY [] :: INT [] : ez meglehetősen bonyolultnak tűnő parancs, de meglehetősen egyszerű. Először rendelkezünk egész tömbökkel, majd ezt az egész tömböt alapértelmezett értékre állítjuk be egy üres tömb típusú egész tömb alapértelmezett értékére.

Felhasználói táblázat

Van egy nagyon alapszintű táblázatunk a felhasználók számára , ezeknek az adatoknak a nagy része az auth0-ból származik, amelyről többet láthatunk az authcheck szakaszban.  

Bejegyzések táblázat

Ezután megkapjuk a hozzászólások táblázatot. A címünket és a törzsünket a React kezelőfelületétől kapjuk meg, és minden bejegyzést társítunk egy user_idés username. Minden bejegyzést társítunk egy felhasználóval az SQL idegen kulcsához.

Megvannak a tömbjeink is like_user_id, ez tartalmazza azoknak az összes felhasználói azonosítót, akiknek tetszett egy bejegyzés, megakadályozva ezzel több felhasználó lájkolását.

Megjegyzések táblázat

Végül megvan a megjegyzések táblázata. Megjegyzésünket a reagálási kezelőfelülettől kapjuk, és minden felhasználót társítunk egy megjegyzéshez, így használjuk a felhasználó táblánkuser id és usernamemezőjét . És mi is kell a hajóra utáni táblázatban , mivel egy megjegyzést készül egy bejegyzést, egy megjegyzés nem elszigetelten létezik. Tehát minden megjegyzéshez társítani kell a felhasználót és a bejegyzést is .post id

PSQL Idegen kulcsok

Az idegen kulcs lényegében egy másik táblázat mezője vagy oszlopa, amelyre az eredeti táblázat hivatkozik. Az idegen kulcs általában egy elsődleges kulcsra hivatkozik egy másik táblázatban, de amint láthatja a bejegyzéseket tartalmazó táblázatunkat, rendelkezik egy idegen kulcs linkkel is, usernameamelyre nyilvánvaló okokból szükségünk van. Az adatok integritásának biztosítása érdekében használhatja a mezőben lévő UNIQUEkorlátozást, usernameamely lehetővé teszi, hogy idegen kulcsként működjön.

Egy oszlop használata egy táblázatban, amely egy másik táblázat oszlopára hivatkozik, lehetővé teszi számunkra, hogy kapcsolatokat alakítsunk ki az adatbázisunk táblái között, ezért az SQL adatbázisokat „relációs adatbázisoknak” nevezik.

Az általunk használt szintaxis:

 column_name data_type REFERENCES other_table(column_name_in_other_table) 

Ezért user_ida bejegyzések táblázat oszlopának egyetlen sorának meg kell egyeznie uida felhasználói táblázat oszlopának egyetlen sorával . Ez lehetővé teszi számunkra, hogy olyan dolgokat végezzünk, mint például az összes felhasználó által bejegyzett üzenet megkeresése vagy a bejegyzéshez kapcsolódó összes megjegyzés megkeresése.

Külföldi kulcs korlátozása

Figyelnie kell a PSQL idegen kulcsfontosságú korlátozásaival is. Melyek azok a korlátozások, amelyek megakadályozzák, hogy törölje azokat a sorokat, amelyekre egy másik táblázat hivatkozik.

Egyszerű példa a bejegyzések törlése a bejegyzéshez társított megjegyzések törlése nélkül . Az üzenet id a poszt asztal egy idegen kulcs a megjegyzéseket asztal és felhasználják az összefüggés az asztalok .

Nem törölheti csak a bejegyzést a megjegyzések törlése nélkül, mert akkor egy csomó megjegyzés ül az adatbázisban egy nem létező külföldi azonosítóval .

Íme egy példa arra, hogy miként lehet törölni a felhasználót, valamint bejegyzéseit és megjegyzéseit.

PSQL Shell

Nyissuk meg a PSQL héjat, és írjuk be ezeket a parancsokat, amelyeket itt hoztunk létre a schema.sqlfájlunkban. Ezt a PSQL héjat automatikusan telepíteni kellett volna a PSQL telepítésekor . Ha nem egyszerűen látogasson el a PSQL webhelyére, hogy újra letöltse és telepítse.

Ha először jelentkezik be a PSQL héjba , a rendszer kéri a kiszolgáló, az adatbázis nevének, a portnak, a felhasználónévnek és a jelszónak a beállítását. Hagyja a portot az alapértelmezett 5432-re, és állítsa be a többi hitelesítő adatot bármire.

Tehát most csak látnia postgres#kell a terminálon, vagy bármi másként állíthatja be az adatbázis nevét. Ez azt jelenti, hogy készen állunk az SQL parancsok megadására . Az alapértelmezett adatbázis használata helyett hozzunk létre egy újat a paranccsal CREATE DATABASE database1, majd csatlakozzunk hozzá \c database1. Ha helyesen tette, látnia kell a database#.

Ha listát szeretne kapni az összes parancsról, amelyet   beírhat,help  vagy \? a PSQL héjba . Mindig ne felejtse el befejezni az SQL lekérdezéseit, ;  amelyek az egyik leggyakoribb hiba az SQL használatakor.

Hallásból csak átmásolhatjuk és beilleszthetjük a parancsokat a schema.sqlfájlból.

A táblázataink listájának megtekintéséhez használjuk a \dtparancsot, és ezt látnia kell a terminálban.

És sikeresen beállítottuk az adatbázist!

Most ténylegesen össze kell kapcsolnunk ezt az adatbázist a szerverünkkel . Ennek elvégzése rendkívül egyszerű. Ezt megtehetjük a pgkönyvtár használatával. Telepítse a pgkönyvtárat, ha még nem tette meg, és győződjön meg arról, hogy a Kiszolgáló könyvtárban van, és nem akarjuk telepíteni ezt a könyvtárat a React alkalmazásunkba.

Hozzon létre egy külön fájlt db.jsa főkönyvtárban, és állítsa be az alábbiak szerint:

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

Ezek ugyanazok a hitelesítő adatok lesznek, amelyeket a PSQL héj beállításakor állított be .

És ez az, hogy beállítottuk adatbázisunkat a szerverünkkel való használatra. Most megkezdhetjük a lekérdezéseket az expressz szerverünkről.

Express Routes és PSQL lekérdezések beállítása

Itt található az útvonalak és a lekérdezések beállítása. Szükségünk van az alapvető CRUD műveletekre a bejegyzésekhez és a hozzászólásokhoz. Ezek az értékek a React kezelőfelületünkből származnak, amelyet legközelebb beállítunk.

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 parancsok

SELECT * FROM table: Hogyan kapunk adatokat a DB-ből. adja vissza az asztal összes sorát.

INSERT INTO table(column1, column2): Hogyan mentünk adatokat és adunk sorokat a DB-hez.  

UPDATE table SET column1 =$1, column2 = $2: a db meglévő sorainak frissítése vagy módosítása. A WHEREzáradék meghatározza, hogy mely sorokat kell frissíteni.

DELETE FROM table: törli a sorokat a WHEREzáradék feltételei alapján . VIGYÁZAT : a WHEREzáradék hiányában az egész tábla törlődik.

WHEREzáradék: Választható feltételes utasítás a lekérdezésekhez való hozzáadáshoz. Ez hasonlóan működik, mint ifa javascript egyik állítása.

WHERE (array @> value): Ha az érték a tömbben található.

Express útvonalak

Az expressz útvonalak beállításához először azt az routerobjektumot használjuk , amelyet a tetején definiáltunk express.Router(). Ezután a kívánt http metódust , amely lehet a szokásos módszer, például GET, POST, PUT stb.

Ezután a zárójelben, először át a húr a útvonalon akarunk, és a második argumentum egy funkció futtatására útvonalat hívják a kliens , Express figyeli ezeket a hívásokat a kliens automatikusan. Amikor az útvonalak egyeznek, akkor a testben lévő függvényt hívják meg, amely esetünkben PSQL lekérdezések .

A függvényhívásunkon belül megadhatunk paramétereket is. Használjuk a req, res és next elemeket .

req: rövid a kéréshez, és tartalmazza az ügyfelünk kérési adatait. Lényegében így juttatjuk el az adatokat a kezelőfelületünkről a szerverünkre. A React kezelőfelületünk adatait ez a req objektum tartalmazza, és az útvonalakon itt széles körben felhasználjuk az értékek eléréséhez. Az adatokat paraméterként javascript objektumként továbbítjuk az axiózásokhoz.

Mert GET kérések egy opcionális paraméter, az adatok kapható lesz req.query . A PUT, POST és DELETE kérések esetében az adatok közvetlenül a kérelem törzsében lesznek elérhetők a req.body segítségével . Az adatok javascript objektumok lesznek, és minden tulajdonsághoz rendszeres pontjelöléssel lehet hozzáférni.

A res: rövid a válaszra, és tartalmazza az expressz szerver választ. Azt akarjuk, hogy küldje el a választ megkapjuk a mi adatbázisunkban a kliens úgy átadjuk az adatbázisban válasz erre res funkció, amely majd továbbítja azt ügyfelünk.

következő: olyan köztes szoftver, amely lehetővé teszi a visszahívások átadását a következő funkcióhoz.

Figyelem az expressz útvonalunkon belül, amit csinálunk, pool.queryés ez az poolobjektum ugyanaz, amely tartalmazza az adatbázis- bejelentkezési adatokat, amelyeket korábban beállítottunk és a tetején importáltunk. A lekérdezés funkció lehetővé teszi számunkra, hogy SQL lekérdezéseket tegyünk az adatbázisunkba string formátumban. Figyelje meg azt is, hogy "nem idézőjeleket használok, ami lehetővé teszi, hogy a lekérdezésem több sorban legyen.

Ezután vesszőnk van az SQL lekérdezésünk után, és a következő paraméter, amely egy nyílfüggvény, amelyet a lekérdezés futtatása után kell végrehajtani . először át a 2 paraméter a mi nyíl funkció, q_errés q_resjelenti, hogy a lekérdezés hiba , és a lekérdezés válasz . Adatok küldéséhez a kezelőfelülethez átadjuk q_res.rowsa res.jsonfüggvényt. q_res.rowsaz adatbázis válasza, mivel ez SQL, és az adatbázis visszaküldi a lekérdezésünknek megfelelő sorokat. Ezután konvertálja azokat sorban a JSON formátumban és küldje el a frontend a resparamétert.

Azt is át a választható értékeket a SQL lekérdezések átadásával egy tömbben után keresett vesszővel elválasztva. Akkor hozzáférhet az egyes elemeket, hogy tömb az SQL lekérdezés a szintaxis $1, ahol $1az első elem a tömbben. $2volna elérni a második elem a tömbben, és így tovább. Ne feledje, hogy ez nem 0 alapú rendszer, mint a javascriptben, nincs$0

Bontjuk le ezeket az útvonalakat, és adjunk rövid leírást mindegyikről.

Hozzászólások útvonalak

  • / api / get / allposts: az összes bejegyzésünket lekéri az adatbázisból.  ORDER BY date_created DESClehetővé teszi számunkra, hogy a legújabb bejegyzéseket jelenítsük meg először.
  • / api / post / posttodb: Felhasználói bejegyzést ment az adatbázisba. A szükséges 4 értéket elmentjük: cím, törzs, felhasználói azonosító, felhasználónév egy értéktömbbe.
  • / api / put / post: Meglévő bejegyzés szerkesztése az adatbázisban. Használjuk az SQL UPDATE   parancsot, és újra beírjuk a bejegyzés összes értékét. Megkeressük a bejegyzést azzal az azonosítóval, amelyet a kezelőfelületünktől kapunk.
  • / api / delete / postcomments: Törli az összes hozzászóláshoz tartozó megjegyzést. A PSQL külföldi kulcskényszer miatt törölni kell az összes hozzászólást, mielőtt a tényleges üzenetet törölnénk.
  • / api / delete / post: Törli a bejegyzés azonosítójú bejegyzést.
  • / api / put / likes : Feltételi kérelmet adunk annak a felhasználónak a felhasználói azonosítójával, akinek tetszett a bejegyzés a like_user_idtömbbe, majd likes1-gyel növeljük a számot.

Megjegyzések Útvonalak

  • / api / post / commenttodb: Megjegyzést ment az adatbázisba
  • / api / put / commenttodb: meglévő megjegyzés szerkesztése az adatbázisban
  • / api / delete / comment: Egyetlen megjegyzés törlése, ez különbözik a bejegyzéshez kapcsolódó összes megjegyzés törlésétől.
  • / api / get / allpostcomments: Az egyetlen hozzászóláshoz tartozó összes megjegyzés lekérése

Felhasználói útvonalak

  • / api / posts / userprofiletodb: Felhasználói profiladatokat menti az auth0-ból a saját adatbázisunkba. Ha a felhasználó már létezik, a PostgreSQL nem tesz semmit.
  • / api / get / userprofilefromdb: Felhasználó lekérése az e-mail megkeresésével
  • / api / get / userposts: a felhasználó által készített bejegyzéseket úgy keresi le, hogy megkeresi az összes felhasználó azonosítójának megfelelő bejegyzést.
  • / api / get / otheruserprofilefromdb: újabb felhasználói profiladatok beolvasása az adatbázisból és megtekintés a profiloldalukon.
  • / api / get / otheruserposts: Szerezzen be egy másik felhasználót, ha megtekinti a profiloldalukat

Globális állapot beállítása reduktorokkal, műveletek és kontextus.

Felhasználói profiladatok mentése az adatbázisunkba

Mielőtt elkezdenénk beállítani a globális állapotot, szükségünk van arra, hogy a felhasználói profil adatait a saját adatbázisunkba mentsük, jelenleg csak az auth0-tól kapjuk az adatainkat. Ezt a authcheck.jskomponensünkben fogjuk megtenni .

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;

Ennek a komponensnek a nagy részét az utolsó oktatóanyagban állítottuk be, ezért azt javasoljuk, hogy nézze meg az oktatóanyagot a részletes magyarázatért, de itt axios utáni kérést hajtunk végre, amelyet azonnal követ egy újabb axios get kérés, hogy azonnal megkapja a felhasználói profil adatait, amelyeket éppen elmentettünk a db-be.

Ezt azért tesszük, mert szükségünk van az adatbázisunk által létrehozott egyedi elsődleges kulcs azonosítóra, és ez lehetővé teszi számunkra, hogy ezt a felhasználót társítsuk megjegyzéseihez és bejegyzéseihez . És a felhasználók e-mailjét használjuk felkutatásra, mivel az első regisztrációkor nem tudjuk, mi az egyedi azonosítójuk. Végül elmentjük az adatbázis felhasználói profil adatait a globális állapotunkba.

* Vegye figyelembe, hogy ez vonatkozik az OAuth bejelentkezésekre is, például a Google és a Facebook bejelentkezésekre.

Műveletek és reduktorok

Most megkezdhetjük a műveletek és a reduktorok beállítását a kontextussal együtt az alkalmazás globális állapotának beállításához.

A kontextus nulláról történő beállításához lásd az előző bemutatómat. Itt csak az adatbázisprofilhoz és az összes hozzászóláshoz lesz szükségünk állapotra.

Először a cselekvési típusaink

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"

Most a tetteink

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

Végül a post-reduktor és a hitelesítés-csökkentő

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

Most ezeket hozzá kell adnunk a  

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

Ez az, most már készen állunk arra, hogy ezt a globális állapotot komponenseinkben felhasználjuk.

Client Side React App

Ezután beállítjuk az ügyféloldali reakció blogot. Az ebben a szakaszban található összes API-hívást az előző expressz útvonalak szakaszában állították be.

6 összetevőből állítja be az alábbiak szerint.

addpost.js : A hozzászólások beküldéséhez szükséges űrlappal rendelkező összetevő.

editpost.js : A bejegyzések szerkesztésére szolgáló összetevő egy olyan űrlappal, amely már kitöltött mezőket tartalmaz.

posts.js : Az összes bejegyzés renderelésére szolgáló elem, mint egy tipikus fórumban.

showpost.js : Komponens egyedi bejegyzés megjelenítéséhez, miután a felhasználó rákattint egy bejegyzésre.

profile.js : A felhasználóhoz társított bejegyzéseket megjelenítő összetevő. A felhasználói irányítópult.

showuser.js : Olyan összetevő, amely egy másik felhasználói profil adatait és bejegyzéseit mutatja.

Miért ne használná a Redux Formot?

TDLR; A Redux Form túlterhelt a legtöbb felhasználási esetben.

A Redux Form egy népszerű könyvtár, amelyet általában a React alkalmazásokban használnak. Akkor miért ne használná itt? Kipróbáltam a Redux Form-ot, de itt egyszerűen nem találtam használati esetet. Mindig szem előtt kell tartanunk a végső felhasználást, és nem tudtam kidolgozni egy forgatókönyvet ehhez az alkalmazáshoz, ahol az űrlapadatokat globális redux állapotban kell mentenünk.

Ebben az alkalmazásban egyszerűen átvesszük az adatokat egy szokásos formából, és továbbítjuk az Axiosnak, amely továbbítja azokat az expressz szervernek, amely végül elmenti az adatbázisba. A másik lehetséges eset egy editpost komponensre vonatkozik, amelyet úgy kezelek, hogy a post adatokat átadom a Link elem tulajdonságainak.

Próbálja ki a Redux Form-ot, és nézze meg, talál-e rá okos felhasználást, de nem lesz szükségünk erre az alkalmazásra. A Redux Form által kínált bármely funkció viszonylag könnyebben megvalósítható anélkül.

A Redux forma egyszerűen túlteljes a legtöbb használati esetben.

Ugyanúgy, mint egy ORM esetében, nincs ok újabb felesleges bonyolultsági réteg hozzáadásához alkalmazásunkhoz.

Az űrlapok beállítása egyszerűbb a szokásos React segítségével.

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;

Az addpost összetevőben van egy egyszerű 2 mező űrlap, ahol a felhasználó megadhat címet és törzset. Az űrlapot az handlesubmit()általunk létrehozott függvény segítségével küldjük be. a handleSubmit()függvény egy eseményparaméter kulcsszót vesz fel, amely a felhasználó által beküldött űrlap adatait tartalmazza.

Fogjuk használni event.preventDefault(), hogy állítsa le az oldalt újratölteni, mivel reagál az egyetlen oldalt app, és hogy szükségtelen lenne.

Az axios post módszer az „adatok” paramétert veszi fel, amelyet az adatbázisban tárolt adatok tárolására használnak. Kapunk a felhasználónév és user_id a globális állam megbeszéltük az előző részben.

Az adatok adatbázisba való feltöltését az expressz útvonalak függvényben kezeljük SQL-lekérdezésekkel, amelyeket korábban láttunk. A Axios API hívás , majd Az adatok átadása a kifejezett szerver, amely menteni az adatokat az adatbázisba.

editpost.js

Ezután megvan a editpost.jskomponensünk. Ez a felhasználói bejegyzések szerkesztésének alapkomponense lesz. Csak a felhasználói profil oldalon érhető el.

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: a reakció-útválasztó által kínált funkcionalitás . Amikor a felhasználó egy profilra kattint egy bejegyzésre a profiloldaláról, akkor az általa rákattintott bejegyzési adatok elmentésre kerülnek a hivatkozás elem egyik állami tulajdonságában, és hogy ez különbözik a React helyi kampányának állapotátóluseState .

Ez a megközelítés az adatok mentésének egyszerűbb módját kínálja a kontextushoz képest, és egy API kérést is ment. Meglátjuk, hogy működik ez a profile.jskomponensben.

Ezután rendelkezünk egy alap vezérelt komponens űrlappal, és minden egyes lenyomáskor elmentjük az adatokat a React állapotba.

A mi handleSubmit()funkció ötvözzük minden adatot, mielőtt elküldi azt a szervert egy Axiosz fel kérésre.  

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;

Észre fogja venni, hogy meglehetősen összetett useEffect()hívásunk van, hogy bejegyzéseinket adatbázisunkból szerezzük be. Ez azért van, mert az adatbázisunkból mentjük a bejegyzéseinket a globális állapotba, így a bejegyzések akkor is ott vannak, ha a felhasználó egy másik oldalra navigál.

Ezzel elkerülheti a szerverünkhöz szükséges felesleges API-hívásokat. Ezért használunk feltételes feltételeket annak ellenőrzésére, hogy a bejegyzések már el vannak-e mentve a kontextus állapotába.

Ha a bejegyzések már el vannak mentve a globális állapotba, akkor a globális állapotban lévő bejegyzéseket csak a helyi államunkra állítjuk, ami lehetővé teszi számunkra, hogy inicializáljuk a lapozást.  

Lapszámozás

Van egy alapvető oldalszámozási megvalósításunk itt a page_change()függvényben. Alapvetően az 5 oldalszámozási blokk tömbként van beállítva. Amikor az oldal megváltozik, a tömb frissül az új értékekkel. Ez látható ifa page_change()függvény első utasításában, a többi 4 ifutasítás csak az első 2 és az utolsó 2 oldal változásainak kezelésére szolgál.

Mi is van egy window.scrollTo()hívás, hogy lépjünk a tetejére minden oldalon változás.

Kihívja magát, hátha fel tud építeni egy bonyolultabb oldalszámozási megvalósítást, de céljaink szempontjából ez az egyetlen funkció a lapozáshoz kiváló.

lapozásunkhoz 4 állapotértékre van szükségünk. Szükségünk van:

  • num_posts: hozzászólások száma
  • posts_slice: az összes hozzászólás egy szelete
  • currentPage: az aktuális oldal
  • posts_per_page: Az egyes oldalak bejegyzéseinek száma.

Azt is át kell adnunk az currentPageállapotértéket a useEffect()kampónak, amely lehetővé teszi, hogy minden funkciót elindítsunk minden alkalommal, amikor az oldal megváltozik. A indexOfLastPost háromszoros szorzást currentPagekapjuk, és indexOfFirstPosta megjeleníteni kívánt bejegyzést a 3 kivonásával kapjuk meg. Ezután ezt az új szeletelt tömböt állíthatjuk be új tömbként helyi államunkban.

Most a JSX-nkről. Mi használ flexbox a felépítése és elrendezése a lapszámozás blokk helyett a szokásos vízszintes listák hagyományosan használják.

4 gombunk van, amelyek lehetővé teszik, hogy a legelső oldalra lépjen, vagy visszalépjen egy oldalra, és fordítva. Ezután egy térképes utasítást használunk a pages_slicetömbön, amely megadja nekünk a lapozás blokkjaink értékeit. A felhasználó kattinthat egy oldalszámozási blokkra is, amely az oldalon argumentumként átkerül a page_change()függvényhez.

Vannak CSS osztályaink is, amelyek lehetővé teszik számunkra, hogy a lapozásunkon is beállítsuk a stílust.  

  • .pagination-active: ez egy szokásos CSS osztály, egy álválasztó helyett, amelyet általában vízszintes listákkal, például .item:active. Az aktív osztályt a React JSX-ben kapcsoljuk össze, összehasonlítva currentPagea pages_slicetömbben található oldalt .
  • .pagination-item: stílus minden oldalszámozási blokkhoz
  • .pagination-item:hover: stílus, amelyet akkor kell alkalmazni, amikor a felhasználó egy paginációs blokk fölé viszi az egeret
 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

az a funkcionális komponens, amelyet az egyes bejegyzések megjelenítéséhez használunk. A bejegyzések címe, Linkamelyre kattintva a felhasználó minden egyes hozzászólással ellátott hozzászóláshoz eljut. Azt is észreveszi, hogy a teljes bejegyzést átadjuk stateaz Linkelem tulajdonságának . Ez a statetulajdonság különbözik a helyi államtól, ez tulajdonképpen a tulajdona, react-routerés ezt részletesebben meglátjuk az showpost.jsösszetevőben. Ugyanezt tesszük a bejegyzés írójával is.

Néhány egyéb dolgot is észrevesz a bejegyzések keresésével kapcsolatban, amelyeket a későbbi szakaszokban tárgyalok.  

Beszélek a showpost.jskomponens "tetszik" funkciójáról is .

showpost.js

Most itt van messze a legösszetettebb alkotóelem ebben az alkalmazásban. Ne aggódjon, lépésről lépésre teljesen lebontom, nem annyira megfélemlítő, mint amilyennek látszik.  

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;

Először észrevesz egy óriási useStatehívást. Elmagyarázom az egyes tulajdonságok működését, miközben összetevőinket vizsgáljuk itt egyszerre.

useEffect () és API kéréseket

Első dolog, amivel tisztában kell lennünk, hogy a felhasználó 2 különböző módon férhet hozzá a bejegyzésekhez. Hozzáférés a fórumtól vagy navigálás a közvetlen URL használatával .  

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

Ha a fórumról férnek hozzá , ezt useEffect()felhívásunkban ellenőrizzük, majd a helyi államot állítjuk a posztra. Mivel az elemben a router routerstate tulajdonságát használjuk , hozzáférhetünk a kellékek által a rendelkezésünkre álló teljes hozzászólási adatokhoz, ami felesleges API hívást spórol meg nekünk.

Ha a felhasználó a böngészőben megadja a bejegyzés közvetlen URL-jét , akkor nincs más választásunk, mint API-kérést benyújtani a bejegyzés megszerzéséhez, mivel a felhasználónak a posts.jsfórum egyik bejegyzésére kell kattintania , hogy a hozzászólás adatait a reakcióba mentse. router statetulajdonságai.

Először kivonjuk a hozzászólási azonosítót az URL-ből pathnamea react -router tulajdonsággal, amelyet aztán paraméternek használunk axióskérésünkben . Az API kérés után csak elmentjük a választ a helyi állapotunkba.

Ezt követően meg kell kapnunk a megjegyzéseket egy API kéréssel is. Ugyanazon bejegyzés azonosító URL-kinyerési módszert használhatjuk a bejegyzéshez kapcsolódó megjegyzések megkeresésére.

RenderComments and Animations

Itt van funkcionális komponensünk, amelyet egyedi megjegyzés megjelenítésére használunk.

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

Először egy classNamediv kifejezés használatával kezdjük el a háromosztályos kifejezést a stílusosztályok váltásához. Ha a delete_comment_idhelyi állapotunkban megegyezik az aktuális megjegyzés-azonosítóval, akkor az törlődik, és a megjegyzésre egy elhalványult animációt alkalmaznak.

Az @keyframeanimációkat szoktuk csinálni. A css @keyframeanimációkat sokkal egyszerűbbnek találom, mint a javascript alapú megközelítések olyan könyvtárakkal, mint az react-springés react-transition-group.

Ezután az aktuális megjegyzést jelenítettük meg

Háromszintű kifejezés követi, amely vagy a megjegyzés létrehozásának dátumát , a "Szerkesztett" vagy a "Csak most" beállítást adja a felhasználói műveletek alapján.  

Ezután egy meglehetősen összetett beágyazott ternáris kifejezést kapunk. Először összehasonlítjuk a cur_user_id(amelyet context.dbProfileStateállamunktól kapunk és beállítunk a JSX-ben) és a megjegyzés felhasználói azonosítóját . Ha van egyezés, megjelenítünk egy szerkesztés gombot .

Ha a felhasználó a szerkesztés gombra kattint, akkor a megjegyzés edit_commentállapotát állítjuk, az edit_comment_idállapot pedig a megjegyzés azonosítóját . És ez az isEditing prop-t is igazivá teszi, amely előhozza az űrlapot, és lehetővé teszi a felhasználó számára a megjegyzés szerkesztését. Amikor a felhasználó megnyomja az Agree gombot, handleUpdate()akkor meghívásra kerül a függvény, amelyet a következõben látunk.

Megjegyzések CRUD műveletek

Itt vannak a CRUD műveletek megjegyzéshez való kezelésének feladataink. Látni fogja, hogy 2 függvénykészletünk van , az egyik a kliens oldali CRUD kezelésére, a másik pedig az API kérések kezelésére van beállítva . Az alábbiakban elmagyarázom, miért.

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

Azért van szükség, mert ha a felhasználó beküldi, szerkeszti vagy töröl egy megjegyzést, a felhasználói felület nem frissül az oldal újratöltése nélkül. Megoldhatja ezt egy újabb API kéréssel, vagy olyan webaljzat-beállítással, amely meghallgatja az adatbázis változását, de sokkal egyszerűbb megoldás csak az ügyféloldali programszerű kezelése.  

Az összes ügyféloldali CRUD függvény meghívásra kerül a megfelelő API hívásokon belül.

Ügyféloldali CRUD:

  • handleCommentSubmit(): frissítse a comments_arrszimbólumot csak a megjegyzés hozzáadásával a tömb elején.  
  • handleCommentUpdate(): Keresse meg és cserélje le a tömb kommentjét az indexre, majd frissítse és állítsa az új tömböt a comments_arr
  • handleCommentDelete(): Keresse meg a tömbben a megjegyzést a megjegyzés azonosítójával, majd írja .filter()ki, és mentse az új tömböt a mappába comments_arr.

API kérések:

  • handleSubmit(): az űrlapunkról kapjuk az adatainkat, majd egyesítjük a szükséges tulajdonságokat és elküldjük ezeket az adatokat a szerverünkre. A dataés a submitted_commentváltozók azért különböznek egymástól, mert ügyféloldali CRUD műveleteinknek kissé más értékekre van szükségük, mint az adatbázisunknak.
  • handleUpdate(): ez a funkció közel azonos a mi handleSubmit()funkcióval. a fő különbség az, hogy küldési kérelmet csinálunk poszt helyett .
  • handleDeleteComment(): egyszerű törlési kérelem a megjegyzés azonosítóval.  

lájkok kezelése

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!