Full Stack React: Kuinka luoda oma blogisi käyttämällä Express-, Hook- ja Postgres-sovelluksia.

Tässä opetusohjelmassa aiomme rakentaa koko pinon React-blogin yhdessä blogin järjestelmänvalvojan taustapään kanssa.

Opastan sinut läpi kaikki vaiheet yksityiskohtaisesti.

Tämän opetusohjelman loppuun mennessä sinulla on tarpeeksi tietoa rakentaa melko monimutkaisia ​​täyden pinon sovelluksia käyttämällä moderneja työkaluja: React, Express ja PostgreSQL-tietokanta.

Jotta asiat olisivat ytimekkäitä, teen tyylin / asettelun vähimmäismäärän ja jätän sen lukijan vastuulle.

Valmistunut projekti:

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

Järjestelmänvalvojan sovellus:

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

Aloitusprojekti:

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

Starter-projektin rakentaminen:

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

Kuinka lisätä Fullstack-hakukone tähän projektiin:

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

Voit katsoa videon tästä opetusohjelmasta täältä

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

Ota yhteyttä Twitteriin saadaksesi lisää päivityksiä tulevista opetusohjelmista: //twitter.com/iqbal125sf

Osa 1: Express-palvelimen ja PSQL-tietokannan asetukset

  1. Projektin rakenne
  2. Express Expressin perusasetukset
  3. Yhdistetään asiakaspuolelle

    axios vs reagoiva reititin vs pikareititin

    miksi et käytä ORM: ää, kuten Sequelize?

  4. Tietokannan asettaminen

    PSQL-vieraat avaimet

    PSQL-kuori

  5. Pikareittien ja PSQL-kyselyjen määrittäminen

Osa 2: Reagoi käyttöliittymän asetuksiin

  1. Globaalin tilan luominen pelkistimillä, toiminnoilla ja kontekstilla.

    Käyttäjäprofiilien tietojen tallentaminen tietokantaan

    Toimintojen ja vähennyslaitteiden asetukset

  2. Asiakkaan puolella React-sovellus

    addpost.js

    editpost.js

    posts.js

    showpost.js

    profile.js

    showuser.js

Osa 3: Järjestelmänvalvojan sovellus

  1. Järjestelmänvalvojan sovelluksen todennus
  2. Yleiset muokkaus- ja poistamisoikeudet
  3. Järjestelmänvalvojan hallintapaneeli
  4. Käyttäjien poistaminen sekä heidän viestit ja kommentit

Projektin rakenne

Aloitamme keskustelemalla hakemistorakenteesta. Meillä on 2 hakemistoa, asiakas- ja palvelinhakemisto . Asiakas Directory pitää sisällön meidän Reagoi sovellusta me setup viime opetusohjelma ja Server pitää sisällön meidän expresspalvelimen ja pidä logiikka meidän API tietokantaan. Server Hakemisto järjestää myös meidän kaavion meidän SQL -tietokannan.

Lopullisen hakemiston rakenne näyttää tältä.

Express Expressin perusasetukset

Jos et ole vielä tehnyt niin, voit asentaa express-generatorkomennon:

npm install -g express-generator

Tämä on yksinkertainen työkalu, joka luo pikaprojektin yhdellä yksinkertaisella komennolla, samanlainen kuin create-react-app. Se säästää vähän aikaa siitä, että meidän on asetettava kaikki tyhjästä.

Voimme aloittaa suorittamalla expresskomennon Server- hakemistossa. Tämä antaa meille oletusarvoisen pikasovelluksen, mutta emme käytä oletuskokoonpanoa, jota meidän on muutettava.

Katsotaanpa ensin poistaa reittejä kansioon, näkemyksiä kansioon sekä julkisen kansion. Emme tarvitse niitä. Sinulla pitäisi olla vain 3 tiedostoa jäljellä. Www tiedosto bin hakemiston, niin app.jstiedosto ja package.jsontiedosto. Jos poistat vahingossa jonkin näistä tiedostoista, luo yksinkertaisesti toinen pikaprojekti. Koska poistimme nämä kansiot, meidän on myös muutettava koodia hieman. Refactor app.jstiedosto seuraavasti:

 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; 

Voimme myös sijoittaa app.jskansioon nimeltä tärkein .

Seuraavaksi meidän on muutettava www- tiedoston oletusportti muuhun kuin porttiin 3000, koska tämä on oletusportti, jolla React-käyttöliittymäsovelluksemme toimii.

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

Luomalla pikasovelluksen saamiemme riippuvuuksien lisäksi lisäämme myös 3 muuta kirjastoa auttamaan meitä:

cors: tätä kirjastoa käytämme apuna React-sovelluksen ja Express-palvelimen välisessä viestinnässä. Teemme tämän React-sovelluksen välityspalvelimen kautta. Ilman tätä saamme selaimessa Cross Origin Resource -virheen.

helmet: Suojakirjasto, joka päivittää http-otsikot. Tämä kirjasto tekee http-pyyntöistämme turvallisempia.

pg: Tätä pääkirjastoa käytämme yhteydenpitoon psql-tietokantamme kanssa. Ilman tätä kirjastoa viestintä tietokannan kanssa ei ole mahdollista.

voimme jatkaa ja asentaa nämä kirjastot

npm install pg helmet cors

Olemme valmiit asettamaan vähimmäispalvelimen ja meillä pitäisi olla projektirakenne, joka näyttää tältä.

Nyt voimme testata, toimiiko palvelimemme. Suoritat palvelinta ilman asiakaspuolen sovellusta . Express on täysin toimiva sovellus, ja se toimii riippumatta asiakaspuolen sovelluksesta . Jos se tehdään oikein, sinun pitäisi nähdä tämä päätelaitteessasi.

Voimme pitää palvelimen käynnissä, koska käytämme sitä pian.

Yhdistetään asiakaspuolelle

Yhdistäminen meidän Client puoli app meidän palvelin on erittäin helppoa ja tarvitsemme vain yhtä riviä koodia. Siirry package.jsontiedoston oman Client hakemistoon ja anna seuraava:

“proxy”: “//localhost:5000"

Ja siinä se! Asiakkaamme voi nyt kommunikoida palvelimemme kanssa välityspalvelimen kautta.

** Huomaa: Muista, että jos asetat tiedostossa toisen portin: portin 5000 lisäksi, wwwkäytä sen sijaan välityspalvelimen tätä porttia.

Tässä on kaavio, joka jakaa ja selittää mitä tapahtuu ja miten se toimii.

Meidän localhost: 3000 olennaisesti laatiessa ikään kuin localhost: 5000 välityspalvelimen välikäsi joka on mitä antaa meidän Server kommunikoida meidän Client .

Asiakaspuolemme on nyt yhdistetty palvelimeemme ja haluamme nyt testata sovellustamme.

Meidän on nyt palattava palvelinpuolelle ja määritettävä expressreititys. Teidän tärkein kansio Server hakemistossa luo uuden tiedoston nimeltä routes.js. Tämä tiedosto pitää sisällään kaikki expressreitit. joiden avulla voimme lähettää tietoja asiakkaan puoleiselle sovellukselle . Voimme nyt asettaa hyvin yksinkertaisen reitin:

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

Pohjimmiltaan, jos /helloreitille soitetaan API-kutsu , Express-palvelimemme vastaa "hello world" -merkkijonolla json-muodossa.

Meidän on myös muokkaettava app.jstiedosto, jotta voimme käyttää pikareittejä.

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;

Nyt home.jskomponenttimme asiakaspuolen koodi :

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;

Teemme peruspalvelupyynnön axioskäynnissä olevalle expresspalvelimellemme, jos se toimii, meidän pitäisi nähdä "hei maailma" näytöllä.

Ja kyllä, se toimii, olemme onnistuneesti asentaneet React Node Fullstack -sovelluksen!

Ennen kuin jatkat Haluaisin käsitellä muutamia kysymyksiä saatat olla joka on mitä eroa on axios, react routerja express routermiksi en ole käyttämällä ORM kuten Sequelize .

Axios vs Express -reititin vs React Router

TLDR; Käytämme react routernavigointia sovelluksessamme, axioskommunikoimme expresspalvelimemme kanssa ja käytämme expresspalvelinta palvelemaan tietokantaamme.

Saatat ihmetellä tässä vaiheessa, kuinka nämä 3 kirjastoa toimivat yhdessä. Käytämme axioskommunikointiin expresspalvelimen taustajärjestelmän kanssa, ilmoitamme puhelusta expresspalvelimellemme sisällyttämällä URI-tunnukseen "/ api /". axiosSitä voidaan käyttää myös suorien http-pyyntöjen tekemiseen mihin tahansa backend-päätepisteeseen. Turvallisuussyistä ei kuitenkaan ole suositeltavaa tehdä pyyntöjä asiakkaalta tietokantaan.

express routerkäytetään pääasiassa kommunikointiin tietokantamme kanssa, koska voimme välittää SQL-kyselyitä express routerfunktion rungossa . expressyhdessä Solmun kanssa käytetään koodin suorittamiseen selaimen ulkopuolella, mikä tekee SQL-kyselyistä mahdollisia. expresson myös turvallisempi tapa tehdä http-pyyntöjä aksioiden sijaan.

axiosReact-asiakaspuolella meidän on kuitenkin käsiteltävä asynkronisia http-pyyntöjä, emme tietenkään voi käyttää express router sitä React-asiakaspuolella. axioson lupauspohjainen, joten se pystyy käsittelemään myös asynkroniset toiminnot.

Käytämme react-routernavigointia sovelluksessamme, koska React on yhden sivun sovellus, jota selain ei lataa uudelleen sivumuutoksen yhteydessä. App on behind-the-scenes tech joka tietää automaattisesti, jos me pyydetään reittiä kautta expresstai react-router.

Miksi et käytä ORM-kirjastoa, kuten Sequelize?

TLDR; Etusija suoran työskentelyn kanssa SQL: n kanssa, mikä mahdollistaa paremman hallinnan kuin ORM. Enemmän oppimisresursseja SQL: lle kuin ORM. ORM-taitoja ei voida siirtää, SQL-taitoja voidaan siirtää hyvin.

On monia opetusohjelmia, jotka osoittavat, kuinka ORM-kirjasto otetaan käyttöön SQL-tietokannan kanssa. Mitään vikaa tässä, mutta olen henkilökohtaisesti mieluummin vuorovaikutuksessa suoraan SQL: n kanssa. Suoraan SQL: n kanssa työskenteleminen antaa sinulle tarkemman hallinnan koodista, ja uskon, että tämä kannattaa hieman lisätä vaikeuksia työskenneltäessä suoraan SQL: n kanssa.

SQL: ssä on paljon enemmän resursseja kuin missään tietyssä ORM-kirjastossa, joten jos sinulla on kysyttävää tai virheitä, ratkaisun löytäminen on paljon helpompaa.

Lisäksi lisäät toisen riippuvuuden ja abstraktiotason ORM-kirjastoon, joka voi aiheuttaa virheitä tiellä. Jos käytät ORM: ää, sinun on seurattava päivityksiä ja rikkomattomia muutoksia, kun kirjastoa muutetaan. SQL on toisaalta erittäin kypsä ja ollut olemassa vuosikymmenien ajan, mikä tarkoittaa, että sillä ei todennäköisesti ole kovin paljon rikkovia muutoksia. SQL: llä on myös ollut aikaa parantaa ja parantaa, mikä ei yleensä ole ORM-kirjastojen tapauksessa.

Viimeiseksi ORM-kirjaston oppiminen vie aikaa, ja tietoa ei yleensä voida siirtää mihinkään muuhun. SQL on käytetyin tietokannan kieli hyvin laajalla marginaalilla (viimeksi tarkistin noin 90% kaupallisista tietokannoista, jotka käyttivät SQL: ää). Yhden SQL-järjestelmän, kuten PSQL: n, oppiminen antaa sinun siirtää nämä taidot ja tiedot suoraan toiseen SQL-järjestelmään, kuten MySQL.

Nämä ovat syitä, miksi en käytä ORM-kirjastoa.

Tietokannan määrittäminen

Aloitetaan asettamalla SQL-skeema luomalla tiedosto nimeltään Server-hakemiston pääkansioon schema.sql.

Tämä pitää tietokannan muodon ja rakenteen. Tietokannan todelliseen asentamiseen sinun on tietysti syötettävä nämä komennot PSQL-kuoreen. Yksinkertaisesti SQL-tiedoston käyttäminen tässä projektissamme ei tee mitään , se on yksinkertaisesti tapa, jolla voimme viitata tietokantarakenteen ulkoasuun ja antaa muille insinööreille pääsyn SQL-komentoihimme, jos he haluavat käyttää koodiamme.

Mutta jotta meillä olisi toimiva tietokanta, kirjoitamme nämä samat komennot PSQL-päätelaitteeseen.

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

Joten meillä on 3 taulukkoa, joihin mahtuu tietoja käyttäjillemme, viestejä ja kommentteja. SQL-käytännön mukaisesti kaikki pienet kirjaimet ovat käyttäjän määrittämiä sarakkeiden tai taulukoiden nimet, ja kaikki isot kirjaimet ovat SQL-komentoja.

ENSIMMÄINEN AVAIN : psql: n tuottama yksilöllinen numero tietylle sarakkeelle

VARCHAR (255) : vaihteleva merkki tai teksti ja numerot. 255 määrittää rivin pituuden.

BOOLEAN : Tosi tai väärä

VIITTEET : miten ulkomaanavain asetetaan. Vieras avain on ensisijainen avain toisessa taulukossa. Selitän tämän yksityiskohtaisemmin alla.

UNIQUE : Estää päällekkäiset merkinnät sarakkeessa.

OLETUS : aseta oletusarvo

INT [] DEFAULT ARRAY [] :: INT [] : tämä on melko monimutkainen komento, mutta melko yksinkertainen. Meillä on ensin joukko kokonaislukuja, sitten asetamme kyseisen kokonaislukumäärän oletusarvoksi tyhjät taulukot, jotka ovat tyypiltään kokonaislukuja.

Käyttäjät-taulukko

Meillä on käyttäjille hyvin yksinkertainen taulukko , suurin osa näistä tiedoista tulee auth0: sta, josta näemme enemmän authcheck- osiossa.  

Viestitaulukko

Seuraavaksi meillä on viestitaulukko. Saamme otsikkomme ja rungomme React-käyttöliittymästä ja yhdistämme jokaisen viestin myös user_idja username. Yhdistämme jokaisen viestin käyttäjälle SQL: n vieraan avaimen kanssa.

Meillä on myös joukko like_user_id, tämä sisältää kaikki käyttäjät, jotka ovat pitäneet viestistä, estäen useita tykkäyksiä samalta käyttäjältä.

Kommenttitaulukko

Lopuksi meillä on kommenttitaulukko. Saamme myös kommentti siitä reagoivat etupään ja me myös liittää kullekin käyttäjälle , jolla on kommentti joten käytämme user idja usernamekentän meidän käyttäjät taulukosta . Ja tarvitsemme post idmyös postitaulukostamme, koska viestiin tehdään kommentti, kommenttia ei ole olemassa erikseen. Joten jokainen kommentti on liitettävä sekä käyttäjään että viestiin .

PSQL-ulkomaiset avaimet

Vieras avain on pohjimmiltaan kenttä tai sarake toisessa taulukossa, johon alkuperäinen taulukko viittaa. Ulkomainen avain viittaa yleensä ensisijaiseen avaimeen toisessa taulukossa, mutta kuten näet viestitaulukomme, sillä on myös vieras avainlinkki, usernamejota tarvitsemme ilmeisistä syistä. Tietojen eheyden varmistamiseksi voit käyttää kentän UNIQUErajoitusta, usernamejonka avulla se voi toimia vieraanä avaimena.

Sarakkeen käyttäminen taulukossa, joka viittaa sarakkeeseen eri taulukossa, antaa meille mahdollisuuden olla suhteissa tietokannassamme olevien taulukoiden välillä, miksi SQL-tietokantoihin viitataan "relaatiotietokantoina".

Käytämme syntaksia seuraavasti:

 column_name data_type REFERENCES other_table(column_name_in_other_table) 

Siksi yhden user_idtaulukon sarakkeen rivin on vastattava yhtä riviä käyttäjien taulukonuid sarakkeessa . Tämä antaa meille mahdollisuuden tehdä asioita, kuten etsiä kaikki tietyn käyttäjän tekemät viestit tai etsiä kaikki viestiin liittyvät kommentit.

Ulkomaisen avaimen rajoitus

Lisäksi sinun on oltava tietoinen PSQL: n ulkomaisista avainrajoituksista. Mitkä ovat rajoituksia, jotka estävät sinua poistamasta rivejä, joihin toinen taulukko viittaa.

Yksinkertainen esimerkki on viestien poistaminen poistamatta viestiin liittyviä kommentteja . Post id päässä post taulukossa on viiteavain vuonna kommentit taulukossa ja sitä käytetään luomaan suhdetta taulukoiden .

Et voi vain poistaa viestiä poistamatta ensin kommentteja, koska sinulla on sitten joukko kommentteja istumassa tietokannassasi olemattomalla post id id -avaimella .

Tässä on esimerkki siitä, kuinka käyttäjä ja hänen viestit ja kommentit poistetaan.

PSQL-kuori

Avaa PSQL-kuori ja kirjoita nämä juuri täällä luomamme komennot schema.sqltiedostoon. Tämä PSQL-kuori olisi pitänyt asentaa automaattisesti, kun asennit PSQL: n . Jos ei, siirry vain PSQL- sivustoon lataamaan ja asentamaan se uudelleen.

Jos kirjaudut sisään ensin PSQL-kuoreen, sinua pyydetään asettamaan palvelin, tietokannan nimi, portti, käyttäjänimi ja salasana. Jätä portti oletusarvoon 5432 ja aseta loput tunnistetiedoista mihin tahansa haluamaasi.

Joten nyt sinun pitäisi vain nähdä postgres#päätelaitteessa tai mitä tahansa asetat tietokannan nimen. Tämä tarkoittaa, että olemme valmiita aloittamaan SQL- komentojen kirjoittamisen. Oletustietokannan käyttämisen sijaan luodaan uusi komento CREATE DATABASE database1ja muodostetaan sitten yhteys siihen \c database1. Jos se tehdään oikein, sinun pitäisi nähdä database#.

Jos haluat luettelon kaikista komennoista voit kirjoittaa   help  tai \? on psql kuori . Muista aina lopettaa SQL-kyselyt sellaisella, ;  joka on yksi yleisimmistä virheistä työskennellessäsi SQL: n kanssa.

Kuulosta voimme vain kopioida ja liittää komentomme schema.sqltiedostosta.

Jos haluat nähdä luettelon taulukoistamme, käytämme \dtkomentoa, ja sinun pitäisi nähdä tämä terminaalissa.

Ja olemme perustaneet tietokannan onnistuneesti!

Nyt meidän täytyy todella yhdistää tämän tietokannan meidän palvelimelle . Tämän tekeminen on erittäin yksinkertaista. Voimme tehdä tämän hyödyntämällä pgkirjastoa. Asenna pgkirjasto, jos et ole vielä tehnyt niin, ja varmista, että olet Palvelin-hakemistossa, emmekä halua asentaa kirjastoa React-sovellukseemme.

Luo erillinen tiedosto nimeltä db.jsvuonna päähakemistoon ja asettaa se seuraavasti:

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

Nämä tulevat olemaan samat tunnistetiedot, jotka määritit PSQL- kuoren määrittämisen yhteydessä .

Ja siinä se, että olemme asettaneet tietokannan palvelimellemme. Voimme nyt alkaa kysyä sitä pikapalvelimeltamme.

Pikareittien ja PSQL-kyselyjen määrittäminen

Tässä on reittien ja kyselyjen asetukset. Tarvitsemme CRUD-perustoimintamme viestejä ja kommentteja varten. Kaikki nämä arvot tulevat React-käyttöliittymästä, jonka asetamme seuraavaksi.

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

SELECT * FROM table: Kuinka saamme tietoja DB: ltä. palauta kaikki taulukon rivit.

INSERT INTO table(column1, column2): Kuinka tallennamme tietoja ja lisätään rivejä tietokantaan.  

UPDATE table SET column1 =$1, column2 = $2: kuinka päivittää tai muokata olemassa olevia rivejä db: ssä. WHERELauseke Määrittää riviä päivittää.

DELETE FROM table: poistaa rivejä WHERElausekkeen ehtojen perusteella . VAROITUS : Jos WHERElauseketta ei sisälly, koko taulukko poistetaan.

WHERElauseke: Valinnainen ehdollinen käsky, joka lisätään kyselyihin. Tämä toimii samalla tavalla kuin ifjavascriptin lausunto.

WHERE (array @> value): Jos arvo on taulukossa.

Pikareitit

Setup näihin reitit ensin käyttää routeresineen määrittelimme huipulla kanssa express.Router(). Sitten haluamasi http-menetelmä, joka voi olla vakiomenetelmä, kuten GET, POST, PUT jne.

Sitten suluissa välitämme ensin haluamasi reitin merkkijonon ja toinen argumentti on toiminto, joka suoritetaan, kun reittiä kutsutaan asiakkaalta . Express kuuntelee näitä reittipuheluja asiakkaalta automaattisesti. Kun reitit vastaavat, kehon funktiota kutsutaan, joka tapauksessamme sattuu olemaan PSQL-kyselyitä .

Voimme myös lähettää parametreja toimintokutsumme sisällä. Käytämme req, res ja next .

req: on lyhenne kyselystä ja sisältää asiakkaan pyyntötiedot. Tällä tavalla saamme tietoja etupäästä palvelimellemme. React-käyttöliittymämme tiedot sisältyvät tähän req-objektiin, ja käytämme sitä täällä reiteillämme laajasti pääsemään arvoihin. Tiedot toimitetaan aksioille parametrina javascript-objektina.

Saat GET pyyntöihin valinnainen parametri, tiedot ovat saatavilla req.query . Saat PUT, POST ja poistaa pyytää tiedot ovat saatavilla suoraan runkoon pyynnöstä req.body . Tiedot ovat javascript-objekti, ja jokaiseen ominaisuuteen pääsee tavallisella pistemerkinnällä.

res: on lyhyt vastaus ja sisältää pikapalvelimen vastauksen. Haluamme lähettää vastauksen saamme meidän tietokannasta on asiakkaalle niin välitämme tietokannassa vastauksena tähän vara-toiminto, joka lähettää sen asiakkaan.

seuraava: on väliohjelmisto, jonka avulla voit siirtää soittopyynnöt seuraavalle toiminnolle.

Ilmoitus sisällä meidän ilmaista reitin teemme pool.query, ja tämä pooltavoite on sama, joka sisältää meidän tietokantaan kirjautumistiedot että me setup aiemmin ja tuodaan yläreunassa. Hakutoiminto antaa meille mahdollisuuden tehdä SQL-kyselyjä tietokantaan string muodossa. Huomaa myös, että käytän `` ei lainauksia '', mikä antaa minulle kyselyn useilla riveillä.

Sitten meillä on pilkku SQL- kyselymme jälkeen ja seuraava parametri, joka on nuolitoiminto, joka suoritetaan kyselyn suorittamisen jälkeen . ensin kulkea 2 parametreja meidän nuoli toiminto, q_errja q_reseli kysely virheen ja kyselyn vastaus . Lähettää dataa käyttöliittymää me kulkea q_res.rowssen res.jsontoiminnon. q_res.rowson tietokannan vastaus, koska tämä on SQL ja tietokanta antaa meille vastaavia rivejä kyselymme perusteella. Sitten muuntaa ne rivit on JSON-muodossa ja lähetä se meille käyttöliittymä kanssa resparametri.

Voimme myös kulkea asetusarvot meidän SQL-kyselyt johtamalla käytettäessä array jälkeen kyselyn erotettu pilkulla. Sitten voimme käyttää yksittäisiä elementtejä että jono on SQL-kyselyn syntaksi $1jossa $1on ensimmäinen elementti jono. $2pääsisi matriisin toiseen elementtiin ja niin edelleen. Huomaa, että se ei ole 0-pohjainen järjestelmä, kuten javascriptissä, sitä ei ole$0

Eritellään jokainen näistä reiteistä ja annetaan lyhyt kuvaus niistä.

Viestit reitit

  • / api / get / allposts: hakee kaikki viestit tietokannasta.  ORDER BY date_created DESCantaa meille mahdollisuuden näyttää uusimmat viestit ensin.
  • / api / post / posttodb: Tallentaa käyttäjän viestin tietokantaan. Tallennamme tarvittavat 4 arvoa: otsikko, runko, käyttäjätunnus, käyttäjänimi joukkoihin arvoja.
  • / api / put / post: Muokkaa tietokannan olemassa olevaa viestiä. Käytämme SQL- UPDATE   komentoa ja välitämme kaikki viestin arvot uudelleen. Etsimme postia postitunnuksella, jonka saamme käyttöliittymästä.
  • / api / delete / postcomments: Poistaa kaikki viestiin liittyvät kommentit. Koska psql n viiteavain rajoitus, meidän täytyy poistaa kaikki liittyvät kommentit tolpan voimme poistaa todellinen postitse.
  • / api / delete / post: Poistaa viestin, jonka postitunnus on.
  • / api / put / likes : Pyydämme myyntipyyntöä lisäämään viestistä tykänneen käyttäjän käyttäjätunnus like_user_idmatriisiin, minkä jälkeen likeskasvatamme lukumäärää yhdellä.

Kommentit Reitit

  • / api / post / commenttodb: Tallentaa kommentin tietokantaan
  • / api / put / commenttodb: muokkaa olemassa olevaa kommenttia tietokannassa
  • / api / delete / comment: Poistaa yhden kommentin, tämä eroaa kaikkien viestiin liittyvien kommenttien poistamisesta.
  • / api / get / allpostcomments: Hakee kaikki yhteen viestiin liittyvät kommentit

Käyttäjäreitit

  • / api / posts / userprofiletodb: Tallentaa käyttäjäprofiilin tiedot auth0: sta omaan tietokantaamme. Jos käyttäjä on jo olemassa, PostgreSQL ei tee mitään.
  • / api / get / userprofilefromdb: Hakee käyttäjän etsimällä hänen sähköpostiosoitettaan
  • / api / get / userposts: noutaa käyttäjän tekemät viestit etsimällä kaikki käyttäjän tunnusta vastaavat viestit.
  • / api / get / otheruserprofilefromdb: hae toisen käyttäjän profiilitiedot tietokannasta ja katso niiden profiilisivulta.
  • / api / get / otheruserposts: Hanki toisen käyttäjän viestejä, kun tarkastelet heidän profiilisivua

Globaalin tilan luominen pienentäjien, toimien ja kontekstin avulla

Käyttäjäprofiilien tietojen tallentaminen tietokantaan

Ennen kuin voimme aloittaa globaalin valtion asettamisen, tarvitsemme tavan tallentaa käyttäjäprofiilidata omaan tietokantaan, tällä hetkellä saamme tietoja vain auth0: lta. Teemme tämän authcheck.jskomponentissamme.

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;

Asensimme suurimman osan tästä komponentista viimeisessä opetusohjelmassa, joten suosittelen, että tutustut siihen yksityiskohtaiseen selitykseen, mutta tässä teemme axios-lähetyspyynnön, jota seuraa välittömästi toinen axios-pyyntö saada heti käyttäjätiedot, jotka olemme juuri tallentaneet db: lle.

Teemme tämän, koska tarvitsemme ainutlaatuisen ensisijaisen avaimen tunnuksen, jonka tietokanta tuottaa, ja tämän avulla voimme liittää tämän käyttäjän kommentteihinsa ja viesteihinsä . Ja käytämme käyttäjien sähköpostia etsimään heitä, koska emme tiedä mikä heidän yksilöllinen tunnuksensa on, kun he kirjautuvat ensimmäisen kerran. Lopuksi tallennamme tietokannan käyttäjäprofiilidatan globaaliin tilaan.

* Huomaa, että tämä koskee myös OAuth-kirjautumisia, kuten Google- ja Facebook-kirjautumisia.

Toiminnot ja pienentimet

Voimme nyt aloittaa toimintojen ja vähennysventtiilien asettamisen yhdessä kontekstin kanssa tämän sovelluksen globaalin tilan määrittämiseksi.

Jos haluat asettaa kontekstin alusta alkaen, katso edellinen opetusohjelma. Tässä tarvitaan vain tila tietokantaprofiilille ja kaikille viesteille.

Ensin toimintatyypit

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"

Nyt tekomme

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

Lopuksi postin vähennysventtiili ja todennuksen vähennysventtiili

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

Nyt meidän on lisättävä nämä  

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

Se on se, olemme nyt valmiita käyttämään tätä globaalia tilaa komponentteissamme.

Client Side React -sovellus

Seuraavaksi asennamme asiakaspuolen reaktioblogin. Kaikki tämän osan API-puhelut määritettiin edellisessä pikareitit-osiossa.

Se asetetaan 6 komponenttiin seuraavasti.

addpost.js : Komponentti, jolla on lomake viestien lähettämistä varten.

editpost.js : Komponentti viestien muokkaamiseen lomakkeella, jolla on jo täytettyjä kenttiä.

posts.js : Komponentti, jolla kaikki viestit hahmonnetaan, kuten tavallisessa foorumissa.

showpost.js : Komponentti yksittäisen viestin renderoimiseksi sen jälkeen, kun käyttäjä on napsauttanut viestiä.

profile.js : Komponentti, joka renderöi käyttäjään liittyvät viestit. Käyttäjän hallintapaneeli.

showuser.js : Komponentti, joka näyttää toisen käyttäjän profiilitiedot ja viestit.

Miksi et käytä Redux Formia?

TDLR; Redux Form on ylivoimainen useimmissa käyttötapauksissa.

Redux Form on suosittu kirjasto, jota käytetään yleisesti React-sovelluksissa. Joten miksi et käytä sitä täällä? Yritin Redux Formia, mutta en yksinkertaisesti löytänyt sille käyttötapaa täältä. Meidän on aina pidettävä mielessä lopullinen käyttö, enkä voinut keksiä skenaariota tälle sovellukselle, jossa meidän olisi tallennettava lomaketiedot globaaliin redux-tilaan.

Tässä sovelluksessa otamme vain tiedot tavallisesta muodosta ja välitämme ne Axiosille, joka sitten välittää ne pikapalvelimelle, joka lopulta tallentaa ne tietokantaan. Toinen mahdollinen käyttötapaus on editpost-komponentti, jota käsittelen siirtämällä postitiedot Link-elementin ominaisuudelle.

Kokeile Redux Formia ja katso, pystytkö keksimään sen älykkään käytön, mutta emme tarvitse sitä tässä sovelluksessa. Myös kaikki Redux Formin tarjoamat toiminnot voidaan saavuttaa suhteellisen helposti ilman sitä.

Redux-muoto on yksinkertaisesti ylivoimainen useimmissa käyttötapauksissa.

Sama kuin ORM: ssä, ei ole mitään syytä lisätä tarpeetonta monimutkaisuutta sovellukseemme.

Lomakkeiden asettaminen tavallisella React-toiminnolla on yksinkertaisesti helpompaa.

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;

Addpost-komponentissa meillä on yksinkertainen 2-kenttä, johon käyttäjä voi kirjoittaa otsikon ja tekstin. Lomake lähetetään luomamme handlesubmit()toiminnon avulla. handleSubmit()Funktio ottaa tapahtuman parametri avainsanan, joka sisältää käyttäjän toimittanut lomakkeen tiedot.

Käytämme event.preventDefault()sivun lataamisen lopettamista, koska React on yhden sivun sovellus ja se olisi tarpeetonta.

Axios post menetelmä ottaa parametri ”data”, jota käytetään pitämään tiedot, jotka tallennetaan tietokantaan. Saamme käyttäjätunnuksen ja user_id maailmanlaajuisesta valtion keskustelimme viimeisen osan.

Tosiasiallisesti tietojen lähettäminen tietokantaan hoidetaan pikareittifunktiossa SQL-kyselyillä, jotka näimme aiemmin. Meidän Axios sovellusliittymäkutsu sitten kulkee tiedot nimenomaista palvelimelle, joka tallentaa tiedot tietokantaan.

editpost.js

Seuraavaksi meillä on editpost.jskomponenttimme. Tämä on peruskomponentti käyttäjien viestien muokkaamiseen. Siihen pääsee vain käyttäjäprofiilisivun kautta.

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: on reagointireitittimen tarjoama toiminto . Kun käyttäjä napsauttaa viestiä profiilisivulta, napsauttamansa viestitiedot tallennetaan linkki-elementin tilaomaisuuteen ja että tämä eroaauseState koukun Reactin paikallisen komponentin tilasta .

Tämä lähestymistapa tarjoaa meille helpomman tavan tallentaa tiedot kontekstiin verrattuna ja myös API-pyynnön. Näemme, miten tämä toimii profile.jskomponentissa.

Tämän jälkeen meillä on hallittu peruskomponenttimuoto ja tallennamme jokaisen näppäimen tiedot React-tilaan.

Meidän handleSubmit()toiminto yhdistämme kaikki tiedot ennen sen lähettämistä palvelimelle käytettäessä Aksios laittaa pyynnön.  

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;

Huomaat, että meillä on melko monimutkainen useEffect()kutsu saada viestit tietokannastamme. Tämä johtuu siitä, että tallennamme viestit tietokannastamme globaaliin tilaan, joten viestit ovat edelleen olemassa, vaikka käyttäjä siirtyisikin toiselle sivulle.

Näin vältetään tarpeettomat API-puhelut palvelimellemme. Siksi käytämme ehdollista tarkistamaan, ovatko viestit jo tallennettu kontekstitilaan.

Jos viestit on jo tallennettu maailmanlaajuiseen tilaan, asetamme vain viestit globaalissa tilassa paikalliseen tilaan, mikä antaa meille mahdollisuuden aloittaa sivunumerointi.  

Sivunumerointi

Tässä page_change()toiminnossa on perussivutustoteutus . Meillä on periaatteessa 5 sivunumerolohkoasetusta matriisina. Kun sivu muuttuu, taulukko päivitetään uusilla arvoilla. Tämä näkyy funktion ensimmäisessä iflauseessa, page_change()muut 4 iflausetta ovat vain ensimmäisten 2 ja viimeisen 2 sivumuutoksen käsittelemiseksi.

Meidän on myös window.scrollTo()soitettava, jotta voimme siirtyä ylöspäin jokaisella sivumuutoksella.

Haasta itsesi selvittämään, pystytkö rakentamaan monimutkaisemman sivutustavan toteutuksen, mutta tarkoituksemme kannalta tämä ainoa sivutustamista koskeva toiminto on hieno.

tarvitsemme 4 tilan arvoa sivutustamme varten. Me tarvitsemme:

  • num_posts: viestien lukumäärä
  • posts_slice: siivu viestien kokonaismäärästä
  • currentPage: nykyinen sivu
  • posts_per_page: Viestien määrä kullakin sivulla.

Meidän on myös siirrettävä currentPagetilan arvo useEffect()koukulle, mikä antaa meille mahdollisuuden käynnistää toiminto aina, kun sivu muuttuu. Saamme indexOfLastPost kertomalla 3 kertaa currentPageja saamme indexOfFirstPostviestin, jonka haluamme näyttää vähentämällä 3. Voimme sitten asettaa tämän uuden viipaloidun taulukon uudeksi matriisiksi paikallisessa osavaltiossa.

Nyt meidän JSX. Rakennamme ja asettelemme sivutuslohkoja flexboxin avulla perinteisten horisontaalisten luetteloiden sijaan.

Meillä on 4 painiketta, joiden avulla voit siirtyä ensimmäiselle sivulle tai taaksepäin sivulle ja päinvastoin. Sitten käytämme pages_slicematriisissamme kartta-käskyä, joka antaa meille sivutuslohkojemme arvot. Käyttäjä voi myös napsauttaa sivutuslohkoa, joka siirtyy sivulla argumenttina page_change()funktiolle.

Meillä on myös CSS- luokkia, joiden avulla voimme asettaa tyylin myös sivutustamme.  

  • .pagination-active: tämä on tavallinen CSS-luokka sen näennäisvalitsimen sijaan, jonka yleensä näkee vaakaluetteloilla, kuten .item:active. Vaihdamme React JSX: n aktiivista luokkaa vertaamalla taulukon currentPagesivua pages_slice.
  • .pagination-item: muotoilu kaikille sivutuslohkoille
  • .pagination-item:hover: muotoilu käytettäväksi, kun käyttäjä vie hiiren sivutuslohkon päälle
 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

on toiminnallinen komponentti, jota käytämme jokaisen yksittäisen viestin hahmontamiseen. Viestien otsikko on sellainen, Linkjoka napsautettaessa vie käyttäjän kuhunkin yksittäiseen viestiin, jossa on kommentteja. Huomaa myös, että siirrämme koko viestin elementin stateomaisuuteen Link. Tämä stateominaisuus eroaa paikallisesta valtiostamme, tämä on itse asiassa omaisuus, react-routerja näemme sen tarkemmin showpost.jskomponentissa. Teemme samoin myös kirjoittajan kanssa.

Huomaat myös muutamia muita viestejen etsimiseen liittyviä asioita, joista keskustelen myöhemmissä osioissa.  

Keskustelen myös showpost.jskomponentin "tykkäämisestä" .

showpost.js

Nyt täällä on ylivoimaisesti monimutkaisin komponentti tässä sovelluksessa. Älä huoli, hajotan sen kokonaan askel askeleelta, se ei ole niin pelottavaa kuin miltä se näyttää.  

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;

Huomaat ensin jättimäisen useStatepuhelun. Selitän, miten kukin omaisuus toimii, kun tutkimme komponenttia sen sijaan täällä kaikki kerralla.

useEffect () - ja API-pyynnöt

Ensinnäkin meidän on oltava tietoisia siitä, että käyttäjä voi käyttää viestiä kahdella eri tavalla. Pääsy foorumille tai navigointi siihen suoralla URL-osoitteella .  

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

Jos he käyttävät sitä foorumilta, tarkistamme tämän useEffect()puhelumme yhteydessä ja asetamme sitten paikallisen valtiomme viestiin. Koska käytämme elementissä reagoivan reitittimenstate ominaisuutta , meillä on pääsy kaikkiin rekvisiittaamme jo saatavilla oleviin postitietoihin, mikä säästää tarpeetonta API-kutsua.

Jos käyttäjä syöttää suoran URL-osoitteen selaimessa olevalle viestille, meillä ei ole muuta vaihtoehtoa kuin tehdä API-pyyntö saada viesti, koska käyttäjän on napsautettava viestiä posts.jsfoorumista tallentaakseen viestitiedot reaktioon. reitittimen stateominaisuus.

Ensin ote post id päässä URL-reagoivat-reitittimen pathnameomaisuutta, jota sitten käyttää sitä param meidän Axios pyynnöstä . API-pyynnön jälkeen tallennamme vastauksen vain paikalliseen valtioomme.

Sen jälkeen meidän on saatava kommentit myös API-pyynnöllä . Voimme käyttää samaa postitunnuksen URL-osoitteen purkutapaa etsimään viestiin liittyviä kommentteja.

RenderKommentit ja animaatiot

Tässä meillä on toiminnallinen komponenttimme, jota käytämme yksittäisen kommentin näyttämiseen.

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

Aloitetaan ensin käyttämällä kolmikantaista lauseketta classNamediv: n potkurin sisällä tyyliluokkien vaihtamiseksi. Jos delete_comment_idpaikallisessa tilassa vastaava kommentti vastaa nykyistä kommenttitunnusta, se poistetaan ja kommenttiin käytetään häivytysanimaatiota .

Meillä on @keyframetapana tehdä animaatioita. Minusta css- @keyframeanimaatiot ovat paljon yksinkertaisempia kuin javascript-pohjaiset lähestymistavat kirjastojen, kuten react-springja kanssa react-transition-group.

Seuraavaksi näytimme varsinaisen kommentin

Seuraava kolmiosainen lauseke, joka asettaa joko luodun kommentin päivämäärän "Muokattu" tai "Vain nyt" käyttäjien toimien perusteella.  

Seuraavaksi meillä on melko monimutkainen sisäkkäinen kolminkertainen lauseke. Verrataan ensin cur_user_id(jonka saamme context.dbProfileStatetilastamme ja asetamme JSX: ssä) kommentin käyttäjätunnukseen . Jos on ottelu, näytämme muokkauspainiketta .

Jos käyttäjä napsauttaa muokkauspainiketta, asetamme kommentin edit_commenttilaksi ja edit_comment_idtilaksi kommenttitunnuksen . Ja tämä tekee myös isEditing- ehdotuksesta tosi, joka tuo lomakkeen esiin ja antaa käyttäjän muokata kommenttia. Kun käyttäjä osuu Hyväksy, handleUpdate()kutsutaan toiminto, jonka näemme seuraavaksi.

Kommentit CRUD Operations

Täällä meillä on toiminnot CRUD-operaatioiden käsittelemiseksi kommentteja varten. Näet, että meillä on 2 toimintosarjaa , joista toinen on asetettu käsittelemään asiakaspuolen CRUD: ta ja toinen API-pyyntöjen käsittelemiseksi . Selitän miksi alla.

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

Koska käyttäjä lähettää, muokkaa tai poistaa kommentin, käyttöliittymää ei päivitetä ilman sivun lataamista uudelleen. Voit ratkaista tämän tekemällä toisen API-pyynnön tai määrittämällä verkkopistorasiaan, joka kuuntelee muutoksia tietokantaan, mutta paljon yksinkertaisempi ratkaisu on vain käsitellä sitä asiakaspuolella ohjelmallisesti.  

Kaikki asiakaspuolen CRUD-toiminnot kutsutaan vastaavien API-kutsujensa sisään.

Asiakkaan puoli CRUD:

  • handleCommentSubmit(): päivitä comments_arrlisäämällä kommentti vain taulukon alkuun.  
  • handleCommentUpdate(): Etsi ja korvaa taulukon kommentti hakemistolla ja päivitä sitten ja aseta uusi taulukko comments_arr
  • handleCommentDelete(): Etsi kommentti taulukosta, jossa on kommentin tunnus ja sitten .filter()se, ja tallenna uusi taulukko kohteeseen comments_arr.

API-pyynnöt:

  • handleSubmit(): haemme tietoja lomakkeestamme, yhdistämme sitten tarvittavat ominaisuudet ja lähetämme tiedot palvelimellemme. dataJa submitted_commentmuuttujat ovat erilaisia, koska meidän asiakkaan puolella lika toiminnot tarvitsevat hieman erilaisia arvoja kuin meidän tietokantaan.
  • handleUpdate(): tämä toiminto on lähes identtinen handleSubmit()toimintoomme. Tärkein ero on se, että teemme put pyynnön sijaan postitse .
  • handleDeleteComment(): Yksinkertainen delete pyyntöön käyttäen kommentin id.  

tykkäysten käsittely

Nyt voimme keskustella siitä, miten käsitellä, kun käyttäjä tykkää viestistä.

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

että handleLikes()toiminto ensin asettaa virkaan tunnus ja käyttäjätunnuksen . Sitten käytämme ehdollista tarkistaa, jos nykyinen käyttäjätunnus ei ole like_user_idarray joka muistaa on kaikki käyttäjätunnukset ja käyttäjät, jotka ovat jo halunnut tätä viestiä.

Jos ei sitten teemme laittaa pyynnön palvelimelle ja sen jälkeen käytämme toista ehdollinen ja tarkistaa, onko käyttäjä has not jo halunnut tätä viestiä asiakkaan puolelta like_postvaltion omaisuutta sitten päivittää tykkää.  

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!

Meidän 3rd .then()selvitys on poistamassa varsinaista viestiä käyttäjälle tehty.

Meidän 4th .then()selvitys on lopulta poistaa käyttäjän tietokannasta. Meidän 5th.then() sitten suuntaamalla admin etusivulle. Meidän 4th .then()lausuma on sisällä meidän 3rd .then()selvitys samasta syystä kuin miksi meidän 2nd selvitys on sisällä meidän 1st ..then()

Kaikki muu on toiminnallisuus, jota olemme nähneet useita kertoja aiemmin, mikä päättää opetusohjelmamme!

Kiitos lukemisesta!