Full Stack React: Sådan opbygges din egen blog ved hjælp af Express, Hooks & Postgres.

I denne vejledning skal vi opbygge en fuld stack React-blog sammen med en blogadministrator-back-end.

Jeg vil lede dig gennem alle trinene i detaljer.

Ved afslutningen af ​​denne tutorial får du tilstrækkelig viden til at opbygge ret komplekse full stack apps ved hjælp af moderne værktøjer: React, Express og en PostgreSQL-database.

For at holde tingene kortfattede vil jeg gøre det minimale styling / layout og overlade det til læseren.

Afsluttet projekt:

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

Admin-app:

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

Startprojekt:

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

Sådan bygger du startprojektet:

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

Sådan tilføjes en Fullstack-søgemaskine til dette projekt:

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

Du kan se en videoversion af denne vejledning her

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

Opret forbindelse til mig på Twitter for flere opdateringer om fremtidige tutorials: //twitter.com/iqbal125sf

Afsnit 1: Opsætning af Express Server og PSQL-database

  1. Projektstruktur
  2. Grundlæggende Express-opsætning
  3. Opretter forbindelse til klientsiden

    Axios vs React-Router vs Express Router

    hvorfor ikke bruge en ORM som Sequelize?

  4. Opsætning af databasen

    PSQL fremmednøgler

    PSQL-skal

  5. Opsætning af ekspresruter og PSQL-forespørgsler

Afsnit 2: Reager front-end-opsætning

  1. Oprettelse af en global stat med reduktionsmidler, handlinger og kontekst.

    Gemme brugerprofildata i vores database

    Handling og reduktionsopsætning

  2. Client reagerer app

    addpost.js

    editpost.js

    posts.js

    showpost.js

    profile.js

    showuser.js

Afsnit 3: Admin-app

  1. Administration af app-godkendelse
  2. Global redigering og sletning af privilegier
  3. Admin Dashboard
  4. Sletning af brugere sammen med deres indlæg og kommentarer

Projektstruktur

Vi begynder med at diskutere katalogstrukturen. Vi har 2 mapper, klient- og serverbiblioteket . Den Client Register vil holde indholdet af vores React app vi setup i den sidste tutorial og serveren vil holde indholdet af vores expressserver og hold logik for vores API-opkald til vores database. Den server mappe vil også holde vores skemaet til vores SQL -database.

Final Directory-strukturen vil se sådan ud.

Grundlæggende ekspresopsætning

Hvis du ikke allerede har gjort det, kan du installere det express-generatormed kommandoen:

npm install -g express-generator

Dette er et simpelt værktøj, der genererer et grundlæggende ekspres-projekt med en simpel kommando, der ligner create-react-app. Det sparer os lidt tid på at skulle sætte alt op fra bunden.

Vi kan begynde med at køre expresskommandoen i serverkataloget . Dette giver os en standardekspress-app, men vi bruger ikke standardkonfigurationen, vi bliver nødt til at ændre den.

Først lad os slette ruter mappe, de synspunkter mappen og den offentlige mappe. Vi har ikke brug for dem. Du skal kun have 3 filer tilbage. Den www -filen i bin mappen, den app.jsfil og package.jsonfilen. Hvis du ved et uheld slettede nogen af ​​disse filer, skal du blot oprette et andet ekspres-projekt. Da vi slettede disse mapper, bliver vi også nødt til at ændre koden. Omdefiner din app.jsfil som følger:

 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; 

Vi kan også placere app.jsi en mappe kaldet main .

Dernæst skal vi ændre standardporten i www- filen til noget andet end port 3000, da dette er standardporten, som vores React-frontend-app kører på.

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

Ud over de afhængigheder, vi fik ved at generere ekspressappen, tilføjer vi også 3 flere biblioteker for at hjælpe os:

cors: dette biblioteket, vi bruger til at hjælpe kommunikationen mellem React App og Express-serveren. Vi gør dette via en proxy i React-appen. Uden dette ville vi modtage en Cross Origin Resource-fejl i browseren.

helmet: Et sikkerhedsbibliotek, der opdaterer http-overskrifter. Dette bibliotek vil gøre vores http-anmodninger mere sikre.

pg: Dette er det vigtigste bibliotek, vi bruger til at kommunikere med vores psql-database. Uden dette bibliotek er kommunikation med databasen ikke mulig.

vi kan gå videre og installere disse biblioteker

npm install pg helmet cors

Vi er færdige med at oprette vores minimale server og skal have projektstruktur, der ser sådan ud.

Nu kan vi teste for at se, om vores server fungerer. Du kører serveren uden en klientside-app . Express er en fuldt fungerende app og kører uafhængigt af en klientsidesapp . Hvis det gøres korrekt, skal du se dette i din terminal.

Vi kan holde serveren kørende, fordi vi snart bruger den.

Opretter forbindelse til klientsiden

Det er meget nemt at forbinde vores klientside-app til vores server, og vi har kun brug for en linje kode. Gå til din package.jsonfil i dit klientkatalog og indtast følgende:

“proxy”: “//localhost:5000"

Og det er det! Vores klient kan nu kommunikere med vores server via en proxy.

** Bemærk: Husk, at hvis du indstiller en anden port ud over port: 5000 i wwwfilen, skal du bruge den port i proxyen i stedet.

Her er et diagram for at nedbryde og forklare, hvad der sker, og hvordan det fungerer.

Vores localhost: 3000 fremsætter i det væsentlige anmodninger som om det var localhost: 5000 gennem en proxy-mellemmand, hvilket er det, der gør det muligt for vores server at kommunikere med vores klient .

Vores klientside er nu forbundet til vores server, og vi vil nu teste vores app.

Vi er nu nødt til at gå tilbage til serversiden og indstille expressroutingen. I dit primære mappe i Server biblioteket oprette en ny fil kaldet routes.js. Denne fil indeholder alle expressruter. som giver os mulighed for at sende data til vores klientside-app . Vi kan indstille en meget enkel rute for nu:

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

I det væsentlige, hvis der foretages et API-opkald til /helloruten, vil vores Express-server svare med en streng af "hej verden" i json-format.

Vi er også nødt til at omlægge vores app.jsfil for at bruge ekspresruterne.

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;

Nu til vores klientsidekode i vores home.jskomponent:

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;

Vi fremsætter en grundlæggende axiosfå anmodning til vores kørende expressserver, hvis det fungerer, skal vi se "hej verden" gengivet til skærmen.

Og ja det fungerer, vi har med succes konfigureret en React Node Fullstack-app!

Før du fortsætter Jeg vil gerne tage fat et par spørgsmål, du måtte have, som er, hvad forskellen er mellem axios, react routerog express routerog hvorfor Im ikke bruger en ORM ligesom Sequelize .

Axios vs Express Router vs React Router

TLDR; Vi bruger react routertil at navigere i vores app, vi bruger axiostil at kommunikere med vores expressserver, og vi bruger vores expressserver til at kommunikere med vores database.

Du undrer dig måske på dette tidspunkt, hvordan disse 3 biblioteker arbejder sammen. Vi bruger axiostil at kommunikere med vores expressserverbackend, vi signerer et opkald til vores expressserver ved at inkludere “/ api /” i URI. axioskan også bruges til at foretage direkte http-anmodninger til ethvert backend-slutpunkt. Af sikkerhedsmæssige grunde tilrådes det imidlertid ikke at stille anmodninger fra klienten til databasen.

express routerbruges primært til at kommunikere med vores database, da vi kan videregive SQL-forespørgsler i funktionens brødtekst express router. expresssammen med Node bruges til at køre kode uden for browseren, hvilket gør SQL-forespørgsler mulige. expresser også en mere sikker måde at fremsætte http-anmodninger på end axios.

Vi har dog brug axiosfor på React-klientsiden til at håndtere de asynkrone http-anmodninger, vi kan naturligvis ikke bruge express router på vores React-klientside. axioser Promise- baseret, så det også automatisk kan håndtere asynkrone handlinger.

Vi bruger react-routertil at navigere i vores app, da React er en enkelt side-app, som browseren ikke genindlæser ved en sideskift. Vores app har teknologi bag kulisserne, der automatisk ved, om vi anmoder om en rute gennem expresseller react-router.

Hvorfor ikke bruge et ORM-bibliotek som Sequelize?

TLDR; Præference for direkte arbejde med SQL, hvilket giver mere kontrol end ORM. Flere læringsressourcer til SQL end en ORM. ORM-færdigheder kan ikke overføres, SQL-færdigheder er meget overførbare.

Der er mange tutorials, der viser, hvordan man implementerer et ORM-bibliotek, der bruges i en SQL-database. Intet galt med dette, men jeg personligt foretrækker at interagere direkte med SQL. At arbejde direkte med SQL giver dig mere finkornet kontrol over koden, og jeg mener, det er værd at have en lille stigning i vanskeligheder, når du arbejder direkte med SQL.

Der er meget flere ressourcer på SQL end der er på et givet ORM-bibliotek, så hvis du har et spørgsmål eller en fejl, er det meget nemmere at finde en løsning.

Du tilføjer også en anden afhængighed og abstraktionsniveau med et ORM-bibliotek, der kan forårsage fejl på vejen. Hvis du bruger en ORM, skal du holde styr på opdateringer og bryde ændringer, når biblioteket ændres. SQL er derimod ekstremt moden og har eksisteret i årtier, hvilket betyder, at det sandsynligvis ikke har meget mange ændringer. SQL har også haft tid til at blive raffineret og perfektioneret, hvilket normalt ikke er tilfældet for ORM-biblioteker.

Endelig tager et ORM-bibliotek tid at lære, og viden kan normalt ikke overføres til noget andet. SQL er det mest anvendte databasesprog med meget bred margin (sidst kontrollerede jeg omkring 90% af de kommercielle databaser, der brugte SQL). At lære et SQL-system som PSQL giver dig mulighed for direkte at overføre disse færdigheder og viden til et andet SQL-system såsom MySQL.

Det er mine grunde til ikke at bruge et ORM-bibliotek.

Opsætning af databasen

Lad os starte med at opsætte SQL-skemaet ved at oprette en fil i hovedmappen i det kaldte serverkatalog schema.sql.

Dette holder formen og strukturen på databasen. For faktisk at opsætte databasen skal du selvfølgelig indtaste disse kommandoer i PSQL-shell. Bare at have en SQL-fil her i vores projekt gør intet , det er simpelthen en måde for os at henvise til, hvordan vores databasestruktur ser ud og give andre ingeniører adgang til vores SQL-kommandoer, hvis de vil bruge vores kode.

Men for faktisk at have en fungerende database, indtaster vi de samme kommandoer i PSQL-terminalen.

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

Så vi har 3 tabeller her, der indeholder data til vores brugere, indlæg og kommentarer. I overensstemmelse med SQL-konvention er al lille bogstav brugerdefinerede kolonne- eller tabelnavne, og al store bogstaver er SQL-kommandoer.

PRIMÆR NØGLE : Entydigt tal genereret af psql for en given kolonne

VARCHAR (255) : variabel karakter eller tekst og tal. 255 indstiller længden af ​​rækken.

BOOLEAN : Sandt eller falsk

REFERENCER : hvordan man indstiller den fremmede nøgle. Den fremmede nøgle er en primær nøgle i en anden tabel. Jeg forklarer dette mere detaljeret nedenfor.

UNIK : Forhindrer dublerede poster i en kolonne.

STANDARD : indstil en standardværdi

INT [] STANDARDARRAY [] :: INT [] : dette ser ret komplekst ud, men det er ret simpelt. Vi har først et array af heltal, derefter indstiller vi det heltalsarray til en standardværdi for et tomt array af typen array af heltal.

Brugertabel

Vi har en meget grundlæggende tabel for brugere , de fleste af disse data kommer fra auth0, som vi vil se mere af i sektionen authcheck .  

Indlægstabel

Dernæst har vi indlægstabellen. Vi får vores titel og krop fra React front-end, og vi forbinder også hvert indlæg med et user_idog og username. Vi forbinder hvert indlæg med en bruger med SQL's udenlandske nøgle.

Vi har også vores udvalg af like_user_id, dette vil indeholde alle bruger-id'erne for folk, der har ønsket et indlæg, hvilket forhindrer flere likes fra den samme bruger.

Kommentar tabel

Endelig har vi vores kommentar tabel. Vi får vores kommentar fra reagensfronten, og vi forbinder også hver bruger med en kommentar, så vi bruger feltet user idog usernamefra vores brugertabel . Og vi har også brug for det post idfra vores indlægstabel, da der kommenteres til et indlæg, findes en kommentar ikke isoleret. Så hver kommentar skal være knyttet til både en bruger og et indlæg .

PSQL Udenlandske nøgler

En fremmed nøgle er i det væsentlige et felt eller en kolonne i en anden tabel, som den originale tabel henviser til. En fremmed nøgle regel refererer til en primær nøgle i en anden tabel, men som du kan se vores indlæg tabellen, det har også en fremmed nøgle link til username, som vi har brug for indlysende grunde. For at sikre dataintegritet kan du bruge UNIQUEbegrænsningen på usernamefeltet, der gør det muligt at fungere som en fremmed nøgle.

Brug af en kolonne i en tabel, der refererer til en kolonne i en anden tabel, er det, der gør det muligt for os at have relationer mellem tabeller i vores database, hvorfor SQL-databaser kaldes "relationsdatabaser".

Den syntaks, vi bruger, er:

 column_name data_type REFERENCES other_table(column_name_in_other_table) 

Derfor skal en enkelt række i user_idkolonnen i vores indlægstabel matche en enkelt række i uidkolonnen i brugertabellen . Dette giver os mulighed for at gøre ting som at slå op på alle de indlæg, en bestemt bruger har lavet, eller slå op på alle de kommentarer, der er knyttet til et indlæg.

Begrænsning af udenlandsk nøgle

Du bliver også nødt til at være opmærksom på PSQL's begrænsninger for udenlandske nøgler. Hvilke er begrænsninger, der forhindrer dig i at slette rækker, som der henvises til af en anden tabel.

Et simpelt eksempel er at slette indlæg uden at slette de kommentarer, der er knyttet til dette indlæg . Den stillingen id fra posten tabel er en fremmed nøgle i kommentarerne tabellen og bruges til at etablere en relation mellem tabellerne .

Du kan ikke bare slette indlægget uden først at slette kommentarerne, fordi du derefter vil have en masse kommentarer i din database med en ikke-eksisterende udenlandsk nøgle til post-id .

Her er et eksempel, der viser, hvordan man sletter en bruger og deres indlæg og kommentarer.

PSQL Shell

Lad os åbne PSQL-skallen og indtaste disse kommandoer, som vi lige har oprettet her i vores schema.sqlfil. Denne PSQL-skal skulle have været installeret automatisk, da du installerede PSQL . Hvis ikke bare gå til PSQL- webstedet for at downloade og installere det igen.

Hvis du først logger ind på PSQL-shell , bliver du bedt om at indstille serveren, databasenavn, port, brugernavn og adgangskode. Lad porten være til standard 5432, og opsæt resten af ​​legitimationsoplysningerne til alt, hvad du vil have.

Så nu skal du bare se postgres#på terminalen eller hvad du end angiver databasens navn som. Dette betyder, at vi er klar til at begynde at indtaste SQL- kommandoer. I stedet for at bruge standarddatabasen, lad os oprette en ny med kommandoen CREATE DATABASE database1og derefter oprette forbindelse til den med \c database1. Hvis det gøres korrekt, skal du se database#.

Hvis du vil have en liste over alle kommandoer, kan du skrive   help  eller \? i PSQL-shell . Husk altid at afslutte dine SQL-forespørgsler med en, ;  der er en af ​​de mest almindelige fejl, når du arbejder med SQL.

Fra høre kan vi bare kopiere og indsætte vores kommandoer fra schema.sqlfilen.

For at se en liste over vores tabeller bruger vi \dtkommandoen, og du skal se dette i terminalen.

Og vi har med succes oprettet databasen!

Nu skal vi faktisk forbinde denne database til vores server . Det er ekstremt simpelt at gøre dette. Vi kan gøre dette ved at bruge pgbiblioteket. Installer pgbiblioteket, hvis du ikke allerede har gjort det, og sørg for at du er i serverkataloget, vi vil ikke installere dette bibliotek i vores React-app.

Opret en separat fil, der kaldes db.jsi hovedmappen, og indstil den som følger:

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

Disse vil være de samme legitimationsoplysninger, som du indstiller, når du opsætter PSQL- skallen .

Og det er det, vi har konfigureret vores database til brug med vores server. Vi kan nu begynde at stille spørgsmål til det fra vores ekspresserver.

Opsætning af ekspresruter og PSQL-forespørgsler

Her er opsætningen for ruter og forespørgsler. Vi har brug for vores grundlæggende CRUD-operationer til indlæg og kommentarer. Alle disse værdier kommer fra vores React-frontend, som vi opretter derefter.

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

SELECT * FROM table: Hvordan vi får data fra DB. returner alle rækkerne i en tabel.

INSERT INTO table(column1, column2): Hvordan vi gemmer data og tilføjer rækker til DB.  

UPDATE table SET column1 =$1, column2 = $2: hvordan man opdaterer eller ændrer eksisterende rækker i en db. De WHEREklausul angiver hvilke rækker der skal opdateres.

DELETE FROM table: sletter rækker baseret på WHEREklausulens betingelser. FORSIGTIG : Hvis ikke en WHEREklausul inkluderes , slettes hele tabellen.

WHEREklausul: En valgfri betinget erklæring, der skal tilføjes til forespørgsler. Dette fungerer svarende til en iferklæring i javascript.

WHERE (array @> value): Hvis værdien er indeholdt i arrayet.

Express-ruter

For at konfigurere ekspresruter bruger vi først det routerobjekt, vi definerede øverst med express.Router(). Derefter den http-metode, vi ønsker, som kan være standardmetoderne såsom GET, POST, PUT osv.

Så i parentesen passerer vi først i strengen på den rute, vi ønsker, og det andet argument er en funktion, der skal køres, når ruten kaldes fra klienten , Express lytter automatisk til disse ruteopkald fra klienten . Når ruterne matcher, kaldes funktionen i kroppen, som i vores tilfælde tilfældigvis er PSQL-forespørgsler .

Vi kan også videregive parametre inde i vores funktionsopkald. Vi bruger req, res og næste .

req: er en forkortelse for anmodning og indeholder anmodningsdata fra vores klient. Dette er i det væsentlige, hvordan vi får data fra vores front-end til vores server. Dataene fra vores React-frontend er indeholdt i dette req-objekt, og vi bruger det her på vores ruter i udstrakt grad for at få adgang til værdierne. Dataene leveres til axios som en parameter som et javascript-objekt.

For GET- anmodninger med en valgfri parameter vil dataene være tilgængelige med forespørgsel . For PUT-, POST- og DELETE- anmodninger vil dataene være tilgængelige direkte i anmodningens brødtekst med req.body . Dataene vil være et javascript-objekt, og hver ejendom kan tilgås med regelmæssig priknotation.

res: er forkortelse for svar og indeholder ekspresserversvaret . Vi ønsker at sende det svar, vi får fra vores database, til klienten, så vi videregiver databasesvaret til denne res-funktion, som derefter sender det til vores klient.

næste: er middleware, der giver dig mulighed for at viderestille tilbagekald til den næste funktion.

Bemærk inden i vores ekspresrute, vi laver, pool.queryog dette poolobjekt er det samme, der indeholder vores loginoplysninger for database , som vi tidligere har konfigureret og importeret øverst. Den forespørgsel funktionen gør det muligt for os at foretage SQL-forespørgsler til vores database i snor format. Bemærk også, at jeg bruger `` ikke tilbud, som giver mig mulighed for at have min forespørgsel på flere linjer.

Derefter har vi et komma efter vores SQL-forespørgsel og den næste parameter, som er en pilfunktion, der skal udføres efter kørsel af forespørgslen . vi sender først 2 parametre til vores pilefunktion q_errog q_resbetyder forespørgselsfejl og forespørgselssvar . For at sende data til frontend videresendes vi q_res.rowstil res.jsonfunktionen. q_res.rowser databasesvaret, da dette er SQL, og databasen giver os matchende rækker tilbage baseret på vores forespørgsel. Vi konverterer derefter disse rækker til json-format og sender det til vores frontend med resparameteren.

Vi kan også overføre valgfri værdier til vores SQL-forespørgsler ved at videregive en matrix efter forespørgslen adskilt med et komma. Derefter kan vi få adgang til de enkelte elementer i den matrix i SQL-forespørgslen med syntaksen, $1hvor $1er det første element i arrayet. $2ville få adgang til det andet element i arrayet og så videre. Bemærk, at der ikke er et 0-baseret system som i javascript, der er ingen$0

Lad os opdele hver af disse ruter og give en kort beskrivelse af hver.

Indlægsruter

  • / api / get / allposts: henter alle vores indlæg fra databasen.  ORDER BY date_created DESCgiver os mulighed for at få vist de nyeste indlæg først.
  • / api / post / posttodb: Gemmer et brugerindlæg i databasen. Vi gemmer de 4 værdier, vi har brug for: titel, brødtekst, bruger-id, brugernavn til en række værdier.
  • / api / put / post: Redigerer et eksisterende indlæg i databasen. Vi bruger SQL- UPDATE   kommandoen og videresender alle værdierne for indlægget igen. Vi ser op på indlægget med det post-id, som vi får fra vores frontend.
  • / api / delete / postcomments: Sletter alle de kommentarer, der er knyttet til et indlæg. På grund af PSQL's begrænsning af fremmednøgler er vi nødt til at slette alle de kommentarer, der er knyttet til indlægget, før vi kan slette det faktiske indlæg.
  • / api / delete / post: Sletter et indlæg med post-id'et.
  • / api / put / likes : Vi fremsætter en put-anmodning om at tilføje bruger-id for den bruger, der kunne lide indlægget til like_user_idarrayet, så øger vi likesantallet med 1.

Kommentarer Ruter

  • / api / post / commenttodb: Gemmer en kommentar i databasen
  • / api / put / commenttodb: redigerer en eksisterende kommentar i databasen
  • / api / delete / comment: Sletter en enkelt kommentar, dette adskiller sig fra at slette alle de kommentarer, der er knyttet til et indlæg.
  • / api / get / allpostcomments: Henter alle de kommentarer, der er knyttet til et enkelt indlæg

Brugerruter

  • / api / posts / userprofiletodb: Gemmer brugerprofildata fra auth0 til vores egen database. Hvis brugeren allerede eksisterer, gør PostgreSQL intet.
  • / api / get / userprofilefromdb: Henter en bruger ved at slå deres e-mail op
  • / api / get / userposts: henter indlæg fra en bruger ved at slå op på alle indlæg, der matcher deres bruger-id.
  • / api / get / otheruserprofilefromdb: Få andre brugerprofildata fra databasen og se på deres profilside.
  • / api / get / otheruserposts: Få andre brugerindlæg, når du ser deres profilside

Opsætning af global stat med reduktionsmidler, handlinger og kontekst.

Gemme brugerprofildata i vores database

Før vi kan begynde at oprette den globale tilstand, har vi brug for en måde at gemme vores brugerprofildata i vores egen database, i øjeblikket får vi lige vores data fra auth0. Vi gør dette i vores authcheck.jskomponent.

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;

Vi opsætter det meste af denne komponent i den sidste tutorial, så jeg anbefaler at se denne tutorial for en detaljeret forklaring, men her laver vi en axios-postanmodning efterfulgt straks af en anden axios get-anmodning om straks at få de brugerprofildata, vi lige har gemt til db.

Vi gør dette, fordi vi har brug for det unikke primære nøgle-id, der genereres af vores database, og dette giver os mulighed for at knytte denne bruger til deres kommentarer og indlæg . Og vi bruger brugerens e-mail til at slå dem op, da vi ikke ved, hvad deres unikke id er, når de først tilmelder sig. Endelig gemmer vi databasebrugerprofildataene i vores globale tilstand.

* Bemærk, at dette også gælder for OAuth-login som Google og Facebook-login.

Handlinger og reduktioner

Vi kan nu begynde at opsætte handlinger og reducere sammen med kontekst for at opsætte den globale tilstand til denne app.

For at indstille konteksten fra bunden se min tidligere vejledning. Her behøver vi kun tilstand for databaseprofilen og alle indlæg.

Først vores handlingstyper

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"

Nu vores handlinger

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

Endelig vores post reducer og 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 } }

Nu skal vi føje disse til  

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

Dette er det, vi er nu klar til at bruge denne globale tilstand i vores komponenter.

Client Side React-app

Dernæst opretter vi bloggen til klientsiden. Alle API-opkald i dette afsnit blev konfigureret i det forrige ekspresrute-afsnit.

Det opsættes i 6 komponenter som følger.

addpost.js : En komponent med en formular til afsendelse af indlæg.

editpost.js : En komponent til redigering af indlæg med en formular, der allerede har udfyldt felter.

posts.js : En komponent til gengivelse af alle indlæg, som i et typisk forum.

showpost.js : En komponent til gengivelse af et individuelt indlæg, efter at en bruger har klikket på et indlæg.

profile.js : En komponent, der gengiver indlæg tilknyttet en bruger. Brugerdashboardet.

showuser.js : En komponent, der viser andre brugerprofildata og indlæg.

Hvorfor ikke bruge Redux Form?

TDLR; Redux Form er overkill i de fleste brugssager.

Redux Form er et populært bibliotek, der ofte bruges i React-apps. Så hvorfor ikke bruge det her? Jeg prøvede Redux Form, men jeg kunne simpelthen ikke finde en brugssag til det her. Vi skal altid huske på den endelige brug, og jeg kunne ikke komme med et scenarie for denne app, hvor vi skulle gemme formulardata i den globale redux-tilstand.

I denne app tager vi simpelthen dataene fra en regelmæssig form og videresender dem til Axios, som derefter sender dem til ekspresserveren, som endelig gemmer dem i databasen. Den anden mulige brugssag er for en editpost-komponent, som jeg håndterer ved at videregive postdataene til en egenskab ved Link-elementet.

Prøv Redux Form og se om du kan komme med en smart brug til det, men vi har ikke brug for det i denne app. Også enhver funktionalitet, der tilbydes af Redux Form, kan udføres relativt lettere uden den.

Redux-form er simpelthen overkill i de fleste brugssager.

Samme som med en ORM er der ingen grund til at tilføje endnu et unødvendigt lag af kompleksitet til vores app.

Det er simpelthen lettere at konfigurere formularer med regelmæssig React.

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;

I addpost-komponenten har vi en simpel 2-feltform, hvor en bruger kan indtaste en titel og en brødtekst. Formularen indsendes ved hjælp af den handlesubmit()funktion, vi oprettede. den handleSubmit()funktion tager en begivenhed parameter nøgleord, som indeholder de bruger indsendte formulardata.

Vi bruger event.preventDefault()til at stoppe siden med at genindlæse, da React er en app til en enkelt side, og det ville være unødvendigt.

Den Axios efter metode tager en parameter af ”data”, der vil blive anvendt til at holde data, der er gemt i databasen. Vi får brugernavnet og user_id fra den globale tilstand, vi diskuterede i sidste afsnit.

Faktisk håndteres udstationering af data i databasen i ekspresrutefunktionen med SQL-forespørgsler, som vi så før. Vores axios API-opkald sender derefter dataene til vores ekspresserver, som gemmer informationen i databasen.

editpost.js

Dernæst har vi vores editpost.jskomponent. Dette vil være en grundlæggende komponent til redigering af brugernes indlæg. Det vil kun være tilgængeligt via brugerens profilside.

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: er en funktionalitet, der tilbydes af react-router . Når en bruger klikker på et indlæg fra sin profilside, gemmes de postdata, de klikkede på, i en tilstandsejendom i linkelementet, og at dette adskiller sig fra den lokale komponenttilstand i React fra useStatekrogen.

Denne tilgang giver os en nemmere måde at gemme data på i forhold til kontekst og sparer os også en API-anmodning. Vi vil se, hvordan dette fungerer i profile.jskomponenten.

Herefter har vi en grundlæggende kontrolleret komponentform, og vi gemmer dataene ved hvert tastetryk til tilstanden React.

I vores handleSubmit()funktion kombinerer vi alle vores data, inden vi sender dem til vores server i en axios put-anmodning.  

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;

Du vil bemærke, at vi har et ret komplekst useEffect()opkald for at få vores indlæg fra vores database. Dette skyldes, at vi gemmer vores indlæg fra vores database til den globale tilstand, så indlæggene stadig er der, selvom en bruger navigerer til en anden side.

Dette undgår unødvendige API-opkald til vores server. Dette er grunden til, at vi bruger en betinget til at kontrollere, om indlæggene allerede er gemt i konteksttilstanden.

Hvis stillingerne allerede er gemt i den globale stat, indstiller vi bare stillingerne i global tilstand til vores lokale stat, hvilket giver os mulighed for at initialisere sideinddelingen.  

Paginering

Vi har en grundlæggende paginering implementering her i page_change()funktionen. Vi har grundlæggende vores 5 pagination blokke opsætning som en matrix. Når siden ændres, opdateres arrayet med de nye værdier. Dette ses i den første ifsætning i page_change()funktionen, de andre 4 ifudsagn er bare for at håndtere de første 2 og sidste 2 side ændringer.

Vi er også nødt til at window.scrollTo()ringe til toppen for hver sideændring.

Udfordre dig selv til at se, om du kan opbygge en mere kompleks paginering, men til vores formål er denne ene funktion her til pagination fin.

vi har brug for 4 tilstandsværdier til vores sideinddeling. Vi behøver:

  • num_posts: antal indlæg
  • posts_slice: et udsnit af det samlede antal indlæg
  • currentPage: den aktuelle side
  • posts_per_page: Antallet af indlæg på hver side.

Vi er også nødt til at videregive currentPagetilstandsværdien til useEffect()krogen, det giver os mulighed for at affyre en funktion hver gang siden ændres. Vi får den indexOfLastPost ved at gange 3 gange den currentPageog få det indexOfFirstPostindlæg, vi vil vise ved at trække 3. Vi kan derefter indstille dette nye opskårne array som det nye array i vores lokale tilstand.

Nu til vores JSX. Vi bruger flexbox til at strukturere og layout vores sideinddelingsblokke i stedet for de sædvanlige vandrette lister, der traditionelt bruges.

Vi har 4 knapper, der giver dig mulighed for at gå til den allerførste side eller tilbage en side og omvendt. Derefter bruger vi en korterklæring på vores pages_slicematrix, der giver os værdierne for vores pagineringsblokke. En bruger kan også klikke på en pagineringsblok, der vil passere på siden som et argument til page_change()funktionen.

Vi har også CSS- klasser, der også giver os mulighed for at indstille styling på vores sideinddeling.  

  • .pagination-active: dette er en almindelig CSS-klasse i stedet for en pseudovælger, som du normalt ser med vandrette lister som f.eks .item:active. Vi skifter den aktive klasse i React JSX ved at sammenligne currentPagemed siden i pages_slicearrayet.
  • .pagination-item: styling til alle pagineringsblokke
  • .pagination-item:hover: styling, der skal anvendes, når brugeren svæver over en sideinddelingsblok
 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

er den funktionelle komponent, vi bruger til at gengive hvert enkelt indlæg. Titlen på indlæggene er en, Linksom når man klikker på den, fører en bruger til hvert enkelt indlæg med kommentarer. Du vil også bemærke, at vi videregiver hele posten til elementets stateejendom Link. Denne stateejendom er forskellig fra vores lokale stat, dette er faktisk en ejendom af, react-routerog vi vil se dette mere detaljeret i showpost.jskomponenten. Vi gør det samme med forfatteren af ​​indlægget også.

Du vil også bemærke et par andre ting relateret til søgning efter indlæg, som jeg vil diskutere i de senere afsnit.  

Jeg vil også diskutere "likes" -funktionaliteten i showpost.jskomponenten.

showpost.js

Her har vi langt den mest komplekse komponent i denne app. Bare rolig, jeg vil nedbryde det helt trin for trin, det er ikke så skræmmende, som det ser ud.  

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;

Du vil først bemærke et gigantisk useStateopkald. Jeg vil forklare, hvordan hver ejendom fungerer, når vi i stedet udforsker vores komponent her på én gang.

useEffect () og API-anmodninger

Den første ting, vi skal være opmærksom på, er at en bruger kan få adgang til et indlæg på 2 forskellige måder. Adgang til det fra forummet eller navigering til det ved hjælp af den 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])

Hvis de får adgang til det fra forummet, kontrollerer vi dette i vores useEffect()opkald og sætter derefter vores lokale stat til stillingen. Da vi bruger reagerings routersstate egenskab i elementet, har vi adgang til alle de indlægsdata, der allerede er tilgængelige for os via rekvisitter, hvilket sparer os for et unødvendigt API-opkald.

Hvis brugeren indtaster den direkte URL til et indlæg i browseren, har vi intet andet valg end at fremsætte en API-anmodning om at få indlægget, da en bruger skal klikke på et indlæg fra posts.jsforummet for at gemme indlægsdataene i reaktionen routerens stateejendom.

Vi udtrækker først post-id'et fra URL'en med react-routers pathnameegenskab, som vi derefter bruger som param i vores axios-anmodning . Efter API-anmodningen gemmer vi bare svaret i vores lokale stat.

Derefter skal vi også få kommentarerne med en API-anmodning . Vi kan bruge den samme indlægs-id-URL-ekstraktionsmetode til at slå op kommentarer, der er knyttet til et indlæg.

Rendér kommentarer og animationer

Her har vi vores funktionelle komponent, som vi bruger til at vise en individuel kommentar.

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

Vi starter først med at bruge et ternært udtryk inde i classNameprop af div for at skifte stilklasser. Hvis delete_comment_idi vores lokale tilstand matcher det aktuelle kommentar-id, slettes det, og en fade out-animation anvendes til kommentaren.

Vi bruger @keyframetil at lave animationerne. Jeg finder css- @keyframeanimationer meget enklere end javascript-baserede tilgange med biblioteker som react-springog react-transition-group.

Dernæst viste vi den aktuelle kommentar

Efterfulgt af et ternært udtryk, der indstiller enten den oprettede kommentardato , "Redigeret" eller "Bare nu" baseret på brugernes handlinger.  

Dernæst har vi et ret komplekst indlejret ternært udtryk. Vi sammenligner først den cur_user_id(som vi får fra vores context.dbProfileStatetilstand og indstillet i vores JSX) med kommentarens bruger-id . Hvis der er en kamp, ​​viser vi en redigeringsknap .

Hvis brugeren klikker på redigeringsknappen, indstiller vi kommentaren til edit_commenttilstanden og indstiller edit_comment_idtilstanden til kommentar-id'et . Og dette gør også isEditing prop til true, hvilket bringer formularen op og lader brugeren redigere kommentaren. Når brugeren rammer Enig, handleUpdate()kaldes funktionen, som vi vil se næste.

Kommentarer CRUD-operationer

Her har vi vores funktioner til håndtering af CRUD-operationer til kommentarer. Du vil se, at vi har 2 sæt funktioner , et sæt til at håndtere CRUD på klientsiden og et andet til at håndtere API-anmodninger . Jeg vil forklare hvorfor nedenfor.

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

Det er fordi, hvis en bruger indsender, redigerer eller sletter en kommentar, vil brugergrænsefladen ikke blive opdateret uden at genindlæse siden. Du kan løse dette ved at stille en anden API-anmodning eller have en opsætning af et web-stik, der lytter til ændringer i databasen, men en langt enklere løsning er bare at håndtere det klientsiden programmatisk.  

Alle CRUD-funktioner på klientsiden kaldes i deres respektive API-opkald.

CRUD på kundesiden:

  • handleCommentSubmit(): opdater den comments_arrved bare at tilføje kommentaren i begyndelsen af ​​arrayet.  
  • handleCommentUpdate(): Find og erstat kommentaren i matrixen med indekset, opdater derefter, og indstil den nye array til comments_arr
  • handleCommentDelete(): Find kommentaren i matrixen med kommentar-id'et, og tag.filter() den ud, og gem den nye matrix til comments_arr.

API-anmodninger:

  • handleSubmit(): vi henter vores data fra vores formular og kombinerer derefter de forskellige egenskaber, vi har brug for, og sender disse data til vores server. Den dataog submitted_commentvariable er anderledes, fordi vores klientsiden CRUD operationer behøver lidt forskellige værdier end vores database.
  • handleUpdate(): denne funktion er næsten identisk med vores handleSubmit()funktion. den største forskel er, at vi foretager en putanmodning i stedet for et indlæg .
  • handleDeleteComment(): simpel sletningsanmodning ved hjælp af kommentar-id'et.  

håndtering kan lide

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!

Vores 3. .then()erklæring sletter de faktiske indlæg, som brugeren lavede.

Vores fjerde .then()erklæring sletter endelig brugeren fra databasen. Vores 5..then() omdirigerer derefter admin til startsiden. Vores 4. .then()erklæring er inde i vores 3. .then()erklæring af samme grund som hvorfor vores 2. erklæring er inde i vores første ..then()

Alt andet er funktionalitet, vi har set flere gange før, som vil afslutte vores tutorial!

Tak for læsningen!