Pilna kaudzes reakcija: kā izveidot savu emuāru, izmantojot Express, Hooks un Postgres.

Šajā apmācībā mēs izveidosim pilnu React emuāru kopā ar emuāra administratora aizmuguri.

Es detalizēti iepazīstināšu jūs ar visiem soļiem.

Šīs apmācības beigās jums būs pietiekami daudz zināšanu, lai izveidotu diezgan sarežģītas pilnas kaudzes lietotnes, izmantojot modernus rīkus: React, Express un PostgreSQL datu bāzi.

Lai lietas būtu kodolīgas, es izdarīšu minimālo stilu / izkārtojumu un atstāju to lasītāja ziņā.

Pabeigtais projekts:

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

Administratora lietotne:

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

Startera projekts:

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

Kā izveidot startera projektu:

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

Kā šim projektam pievienot meklētājprogrammu Fullstack:

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

Šīs apmācības video versiju varat noskatīties šeit

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

Sazinieties ar mani Twitter vietnē, lai iegūtu vairāk atjauninājumu par turpmākajām apmācībām: //twitter.com/iqbal125sf

1. sadaļa: Express Server un PSQL datu bāzes iestatīšana

  1. Projekta struktūra
  2. Express Express iestatīšana
  3. Savienojuma izveide ar klienta pusi

    axios vs reaģēt-router vs express router

    kāpēc neizmantot tādu ORM kā Sequelize?

  4. Datu bāzes iestatīšana

    PSQL ārzemju atslēgas

    PSQL apvalks

  5. Express Routes un PSQL vaicājumu iestatīšana

2. sadaļa: Reaģējiet priekšgala iestatīšanu

  1. Globālā stāvokļa izveidošana ar reduktoriem, darbībām un kontekstu.

    Lietotāja profila datu saglabāšana mūsu datu bāzē

    Darbību un reduktoru iestatīšana

  2. Klienta pusē reaģējiet lietotni

    addpost.js

    editpost.js

    posts.js

    showpost.js

    profile.js

    showuser.js

3. sadaļa: Administratora lietotne

  1. Administratora lietotnes autentifikācija
  2. Vispārīgas rediģēšanas un dzēšanas privilēģijas
  3. Administratora vadības panelis
  4. Lietotāju dzēšana kopā ar viņu ziņām un komentāriem

Projekta struktūra

Mēs sāksim apspriest direktoriju struktūru. Mums būs 2 direktoriji, klientu un serveru direktorijs. Klients Directory rīkos saturu mūsu React app mēs iestatīšana pēdējā apmācība un Server rīkos saturu mūsu expressservera un turiet to loģiku mūsu API zvaniem uz mūsu datu bāzē. Serveris direktorija arī turēt mūsu shēmu mūsu SQL datu bāzi.

Final Directory struktūra izskatīsies šādi.

Express Express iestatīšana

Ja vēl neesat to izdarījis, varat instalēt express-generatorkomandu ar komandu:

npm install -g express-generator

Šis ir vienkāršs rīks, kas ģenerēs ekspress pamatprojektu ar vienu vienkāršu komandu, līdzīgu create-react-app. Tas ietaupīs mums mazliet laika no nepieciešamības visu iestatīt no nulles.

Mēs varam sākt, palaižot expresskomandu direktorijā Server . Tas mums dos noklusējuma ātro lietotni, taču mēs neizmantosim noklusējuma konfigurāciju, kas mums būs jāmaina.

Vispirms izdzēsīsim mapi maršruti , skatu mapi un publisko mapi. Mums tie nebūs vajadzīgi. Jums vajadzētu būt atlikušiem tikai 3 failiem. Www fails bin direktorijā, app.jsfailu un package.jsonfailu. Ja nejauši izdzēsāt kādu no šiem failiem, vienkārši izveidojiet citu ātro projektu. Tā kā mēs izdzēsām šīs mapes, mums būs nedaudz jāmaina arī kods. Refaktorējiet app.jsfailu šādi:

 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; 

Mēs varam ievietot app.jsarī mapē ar nosaukumu main .

Tālāk mums jāmaina noklusējuma ports www failā uz kaut ko citu, nevis portu 3000, jo tā ir noklusējuma osta, kurā darbosies mūsu React priekšgala lietotne.

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

Papildus atkarībām, kuras ieguvām, ģenerējot ātrās lietotnes, mēs arī pievienosim vēl 3 bibliotēkas, kas mums palīdzēs:

cors: šo bibliotēku mēs izmantosim, lai palīdzētu saziņai starp React App un Express serveri. Mēs to izdarīsim, izmantojot lietotni React starpniekserveri. Pretējā gadījumā mēs pārlūkprogrammā saņemsim kļūdas par izcelsmes avotu kļūdu.

helmet: Drošības bibliotēka, kas atjaunina http galvenes. Šī bibliotēka padarīs mūsu http pieprasījumus drošākus.

pg: Šī ir galvenā bibliotēka, kuru izmantosim, lai sazinātos ar mūsu psql datu bāzi. Bez šīs bibliotēkas saziņa ar datu bāzi nebūs iespējama.

mēs varam turpināt un instalēt šīs bibliotēkas

npm install pg helmet cors

Mēs esam pabeiguši iestatīt mūsu minimālo serveri, un mums vajadzētu būt projekta struktūrai, kas izskatās šādi.

Tagad mēs varam pārbaudīt, vai mūsu serveris darbojas. Serveri palaižat bez klienta puses lietotnes . Express ir pilnībā funkcionējoša lietotne, un tā darbosies neatkarīgi no klienta puses lietotnes . Ja tas izdarīts pareizi, tas jums vajadzētu redzēt savā terminālā.

Mēs varam saglabāt servera darbību, jo drīz to izmantosim.

Savienojuma izveide ar klienta pusi

Klienta puses lietotnes savienošana ar mūsu serveri ir ļoti vienkārša, un mums ir nepieciešama tikai viena koda rindiņa. Klientu direktorijā dodieties uz savu package.jsonfailu un ievadiet šo:

“proxy”: “//localhost:5000"

Un viss! Tagad mūsu klients var sazināties ar mūsu serveri, izmantojot starpniekserveri.

** Piezīme. Atcerieties: ja wwwfailā iestatījāt citu portu: portu 5000, tā vietā izmantojiet šo portu starpniekserverī.

Šeit ir shēma, lai sadalītu un paskaidrotu, kas notiek un kā tas darbojas.

Mūsu localhost: 3000 būtībā iesniedz pieprasījumus tā, it kā tas būtu localhost: 5000, izmantojot starpniekserveri, kas ļauj mūsu serverim sazināties ar klientu .

Klienta puse tagad ir savienota ar mūsu serveri, un mēs vēlamies tagad pārbaudīt mūsu lietotni.

Tagad mums ir jāatgriežas servera pusē un jāiestata expressmaršrutēšana. Savā galvenajā mapē servera direktorijā izveidot jaunu failu ar nosaukumu routes.js. Šis fails saturēs visus expressmaršrutus. kas ļauj mums nosūtīt datus mūsu klienta lietotnei . Pagaidām mēs varam noteikt ļoti vienkāršu maršrutu:

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

Būtībā, ja /hellomaršrutam tiek veikts API izsaukums , mūsu Express serveris atbildēs ar “sveika pasaule” virkni json formātā.

Mums arī jāpārstrādā mūsu app.jsfails, lai izmantotu ātrgaitas maršrutus.

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;

Tagad mūsu home.jskomponentes klienta puses kods :

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;

Mēs iesniedzam pamata axiospieprasījumu mūsu darbības expressserverim, ja tas darbojas, mums vajadzētu redzēt ekrānā redzamo "sveika pasaule".

Jā, tas darbojas, mēs esam veiksmīgi iestatījuši lietotni React Node Fullstack!

Pirms turpināt Es gribētu pievērsties pāris jautājumiem, jums varētu būt, kas ir, kāda ir atšķirība starp axios, react routerun express routerun kāpēc Im neizmantojat ORM piemēram Sequelize .

Axios vs Express Router vs React Router

TLDR; Mēs izmantojam, react routerlai pārvietotos savā lietotnē, mēs izmantojam axiossaziņai ar mūsu expressserveri un mēs izmantojam savu expressserveri, lai sazinātos ar mūsu datu bāzi.

Šajā brīdī jums var būt jautājums, kā šīs 3 bibliotēkas darbojas kopā. Mēs izmantojam axiossaziņai ar expressservera aizmuguri, mēs apzīmēsim zvanu uz mūsu expressserveri, iekļaujot “/ api /” URI. axiosvar izmantot arī tiešu http pieprasījumu veikšanai jebkuram aizmugures beigu punktam. Tomēr drošības apsvērumu dēļ nav ieteicams klientam pieprasīt datu bāzi.

express routergalvenokārt tiek izmantots saziņai ar mūsu datu bāzi, jo express routerfunkcijas pamattekstā mēs varam nodot SQL vaicājumus . expresskopā ar mezglu tiek izmantots, lai palaistu kodu ārpus pārlūkprogrammas, kas padara SQL vaicājumus iespējamus. expressir arī drošāks veids, kā iesniegt http pieprasījumus, nevis axios.

Tomēr mums axiosjāreaģē uz klienta pusi, lai apstrādātu asinhronos http pieprasījumus, mēs acīmredzami nevaram tos izmantot express router mūsu React klienta pusē. axiosir balstīts uz solījumiem, lai tas varētu automātiski apstrādāt arī asinhronas darbības.

Mēs izmantojam, react-routerlai pārvietotos savā lietotnē, jo React ir vienas lapas lietotne, pārlūkprogramma netiek atkārtoti ielādēta, mainot lapu. Mūsu lietotnei ir aizkulišu tehnoloģija, kas automātiski uzzinās, ja mēs pieprasām maršrutu caur expressvai react-router.

Kāpēc neizmantot tādu ORM bibliotēku kā Sequelize?

TLDR; Priekšroka tieši darbam ar SQL, kas ļauj vairāk kontrolēt nekā ORM. Vairāk mācību resursu SQL nekā ORM. ORM prasmes nav nododamas, SQL prasmes ir ļoti nododamas.

Ir daudzas apmācības, kas parāda, kā ieviest ORM bibliotēku, kas tiek izmantota ar SQL datu bāzi. Nekas nepareizs ar šo, bet es personīgi dodu priekšroku tieši mijiedarboties ar SQL. Darbs tieši ar SQL dod jums precīzāku kontroli pār kodu, un es uzskatu, ka tas ir vērts nedaudz palielināt grūtības, strādājot tieši ar SQL.

SQL ir daudz vairāk resursu nekā jebkurā ORM bibliotēkā, tādēļ, ja jums ir jautājums vai kļūda, ir daudz vieglāk atrast risinājumu.

Jūs pievienojat vēl vienu atkarību un abstrakcijas līmeni ar ORM bibliotēku, kas varētu radīt kļūdas pa ceļu. Ja izmantojat ORM, jums būs jāseko atjauninājumiem un izmaiņu pārtraukšanai, kad tiek mainīta bibliotēka. No otras puses, SQL ir ārkārtīgi nobriedis un pastāv jau vairākus gadu desmitus, kas nozīmē, ka tai, visticamāk, nebūs ļoti daudz pārsteidzošu izmaiņu. SQL arī ir bijis laiks pilnveidoties un pilnveidot, kas parasti neattiecas uz ORM bibliotēkām.

Visbeidzot, ORM bibliotēkas apgūšanai nepieciešams laiks, un zināšanas parasti nav nododamas nekam citam. SQL ir visplašāk izmantotā datubāzes valoda ar ļoti plašu starpību (pēdējo reizi es pārbaudīju apmēram 90% no komerciālajām datu bāzēm, kas izmantoja SQL). Apgūstot vienu SQL sistēmu, piemēram, PSQL, jūs varēsit šīs prasmes un zināšanas tieši pārsūtīt uz citu SQL sistēmu, piemēram, MySQL.

Šie ir mani iemesli, kāpēc neizmantoju ORM bibliotēku.

Datu bāzes iestatīšana

Sāksim ar SQL shēmas iestatīšanu, izveidojot failu servera direktorija galvenajā mapē ar nosaukumu schema.sql.

Tas saglabās datu bāzes formu un struktūru. Lai faktiski iestatītu datu bāzi, jums, protams, būs jāievada šīs komandas PSQL čaulā. Vienkārši, ja mūsu projektā ir SQL fails, tas neko nedara , tas ir vienkārši veids, kā mēs varam atsaukties uz to, kā izskatās mūsu datu bāzes struktūra, un ļauj citiem inženieriem piekļūt mūsu SQL komandām, ja viņi vēlas izmantot mūsu kodu.

Bet, lai faktiski būtu funkcionējoša datu bāze, mēs ievadīsim šīs pašas komandas PSQL terminālā.

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

Tāpēc mums šeit ir 3 tabulas, kurās būs dati par mūsu lietotājiem, ziņas un komentāri. Saskaņā ar SQL konvenciju viss mazais teksts ir lietotāja definēts kolonnu vai tabulu nosaukums, un viss lielais teksts ir SQL komandas.

PRIMĀRĀS ATSLĒGAS : unikālais skaitlis, ko psql ģenerējis attiecīgajai kolonnai

VARCHAR (255) : mainīgs raksturs vai teksts un cipari. 255 nosaka rindas garumu.

BOOLEAN : patiess vai nepatiess

ATSAUCES : kā iestatīt ārzemju atslēgu. Ārējā atslēga ir primārā atslēga citā tabulā. Es to sīkāk paskaidroju tālāk.

UNIKĀLA : novērš ierakstu dublikātus kolonnā.

DEFAULT : iestatiet noklusējuma vērtību

INT [] DEFAULT ARRAY [] :: INT [] : šī komanda ir diezgan sarežģīta izskata, bet diezgan vienkārša. Vispirms mums ir veselu skaitļu masīvs, pēc tam mēs iestatām šo veselu skaitļu masīvu uz noklusējuma vērtību tukša masīva ar veselu skaitļu masīvu.

Lietotāju tabula

Mums ir ļoti vienkārša lietotāju tabula , lielākā daļa šo datu tiks iegūti no auth0, ko mēs vairāk redzēsim authcheck sadaļā.  

Ziņu tabula

Tālāk mums ir ziņojumu tabula. Mēs iegūsim savu nosaukumu un virsrakstu no React front-end, un mēs katru ziņu saistām arī ar user_idun username. Katru ziņu mēs saistām ar lietotāju ar SQL svešo atslēgu.

Mums ir arī mūsu klāsts like_user_id, tajā būs visi to lietotāju lietotāju ID, kuriem ir paticis ziņojums, novēršot viena un tā paša lietotāja vairāku atzīmju Patīk.

Komentāru tabula

Visbeidzot, mums ir mūsu komentāru tabula. Mēs saņemsim savu komentāru no reaģēšanas front-end un arī katru lietotāju saistīsim ar komentāru, lai mēs izmantotu mūsu lietotāju tabulas lauku user idun . Mums ir vajadzīgs arī mūsu ziņu tabula, jo ziņai tiek izteikts komentārs, komentārs nepastāv atsevišķi. Tātad katram komentāram jābūt saistītam gan ar lietotāju, gan ar ziņu .usernamepost id

PSQL ārzemju atslēgas

Ārvalstu atslēga būtībā ir lauks vai kolonna citā tabulā, uz kuru atsaucas sākotnējā tabula. Ārzemju atslēga parasti atsaucas uz primāro atslēgu citā tabulā, taču, kā redzat mūsu ziņojumu tabulu, tai ir arī ārzemju atslēgas saite uz to, usernamekas mums ir vajadzīgs acīmredzamu iemeslu dēļ. Lai nodrošinātu datu integritāti, laukā varat izmantot UNIQUEierobežojumu, usernamekas ļauj tam darboties kā svešai atslēgai.

Izmantojot kolonnu tabulā, kas atsaucas uz kolonnu citā tabulā, tas ļauj mums izveidot sakarības starp tabulām mūsu datu bāzē, tāpēc SQL datu bāzes tiek dēvētas par “relāciju datu bāzēm”.

Izmantotā sintakse ir šāda:

 column_name data_type REFERENCES other_table(column_name_in_other_table) 

Tādējādi vienai rindai user_idmūsu ziņojumu tabulas slejā būs jāatbilst vienai rindai lietotāju tabulasuid slejā . Tas ļaus mums rīkoties, piemēram, meklēt visas ziņas, ko izveidojis konkrēts lietotājs, vai meklēt visus komentārus, kas saistīti ar ziņu.

Ārvalstu atslēgu ierobežojums

Jums būs jāņem vērā arī PSQL ārvalstu atslēgu ierobežojumi. Kuri ir ierobežojumi, kas neļauj izdzēst rindas, uz kurām atsaucas cita tabula.

Vienkāršs piemērs ir ziņu dzēšana, neizdzēšot ar šo ziņu saistītos komentārus . Post id no pēc tabulas ir ārējā atslēga ar komentāriem tabulā , un tiek izmantota, lai izveidotu saikni starp tabulām .

Jūs nevarat vienkārši izdzēst ziņu, vispirms neizdzēšot komentārus, jo pēc tam jūsu datu bāzē sēdēs virkne komentāru ar neesošu pasta id svešo atslēgu .

Šeit ir piemērs, kurā parādīts, kā izdzēst lietotāju, viņa ziņas un komentārus.

PSQL čaula

Atvērsim PSQL apvalku un ievadīsim šajās komandā, kuras tikko izveidojām šeit, mūsu schema.sqlfailā. Šis PSQL apvalks bija jāinstalē automātiski, kad instalējāt PSQL . Ja ne, vienkārši dodieties uz PSQL vietni, lai to lejupielādētu un instalētu vēlreiz.

Ja pirmo reizi piesakāties PSQL čaulā, jums tiks lūgts iestatīt serveri, datu bāzes nosaukumu, portu, lietotājvārdu un paroli. Atstājiet portu pēc noklusējuma 5432 un iestatiet pārējos akreditācijas datus uz visu, ko vēlaties.

Tātad tagad jums vienkārši vajadzētu redzēt postgres#terminālā vai citā veidā, kā iestatāt datu bāzes nosaukumu. Tas nozīmē, ka mēs esam gatavi sākt ievadīt SQL komandās. Tā vietā, lai izmantotu noklusējuma datu bāzi, izveidosim jaunu ar komandu CREATE DATABASE database1un pēc tam izveidosim savienojumu ar to \c database1. Ja izdarīts pareizi, jums vajadzētu redzēt database#.

Ja vēlaties visu komandu sarakstu, kuras varat ierakstīt,   help  vai \? arī PSQL čaulā . Vienmēr atcerieties izbeigt SQL vaicājumus ar to, ;  kas ir viena no visbiežāk sastopamajām kļūdām, strādājot ar SQL.

No dzirdes mēs varam vienkārši nokopēt un ielīmēt komandas no schema.sqlfaila.

Lai redzētu mūsu tabulu sarakstu, mēs izmantojam \dtkomandu, un tas jums vajadzētu redzēt terminālā.

Un mēs esam veiksmīgi izveidojuši datu bāzi!

Tagad mums šī datu bāze ir faktiski jāpievieno mūsu serverim . Tas ir ārkārtīgi vienkārši. Mēs to varam izdarīt, izmantojot pgbibliotēku. Instalējiet pgbibliotēku, ja vēl neesat to izdarījis, un pārliecinieties, ka atrodaties servera direktorijā, un mēs nevēlamies instalēt šo bibliotēku mūsu lietotnē React.

Izveidojiet atsevišķu failu, ko sauc db.jspar galveno direktoriju, un iestatiet to šādi:

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

Tie būs tie paši akreditācijas dati, kurus iestatījāt, iestatot PSQL čaulu .

Un tas ir tas, ka mēs esam iestatījuši savu datu bāzi lietošanai kopā ar mūsu serveri. Tagad mēs varam sākt vaicāt par to no sava ekspress servera.

Express Routes un PSQL vaicājumu iestatīšana

Šeit ir iestatīti maršruti un vaicājumi. Ziņām un komentāriem mums ir vajadzīgas pamata CRUD operācijas. Visas šīs vērtības nāks no mūsu React priekšgala, kuru mēs iestatīsim tālāk.

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 komandas

SELECT * FROM table: Kā mēs iegūstam datus no DB. atgriezt visas tabulas rindas.

INSERT INTO table(column1, column2): Kā mēs saglabājam datus un pievienojam rindas DB.  

UPDATE table SET column1 =$1, column2 = $2: kā atjaunināt vai modificēt esošās rindas db. Par WHEREpunktā noteikts, kuras rindas atjaunināt.

DELETE FROM table: dzēš rindas, pamatojoties uz WHEREklauzulas nosacījumiem . UZMANĪBU : neiekļaujot WHEREklauzulu, tiek izdzēsta visa tabula.

WHEREklauzula: neobligāts nosacījuma paziņojums, kas jāpievieno vaicājumiem. Tas darbojas līdzīgi ifapgalvojumam javascript.

WHERE (array @> value): Ja vērtība ir iekļauta masīvā.

Ekspress maršruti

Lai iestatītu ātros maršrutus, vispirms izmantojam routerobjektu, ar kuru mēs definējām augšpusē express.Router(). Tad vēlamo http metodi, kas var būt standarta metodes, piemēram, GET, POST, PUT utt.

Pēc tam iekavās vispirms ievadām vajadzīgā maršruta virkni, un otrais arguments ir funkcija, kas jāizpilda, kad maršruts tiek izsaukts no klienta , Express automātiski klausās šos maršruta izsaukumus no klienta . Kad maršruti sakrīt, tiek saukta funkcija ķermenī, kas mūsu gadījumā notiek kā PSQL vaicājumi .

Mēs varam arī ievadīt parametrus mūsu funkcijas izsaukumā. Mēs izmantojam req, res un nākamo .

req: ir īss pieprasījums un satur pieprasījuma datus no mūsu klienta. Būtībā šādi mēs saņemam datus no mūsu priekšgala uz mūsu serveri. Dati no mūsu React frontend ir ietverti šajā req objektā, un mēs tos plaši izmantojam savos maršrutos, lai piekļūtu vērtībām. Dati tiks piegādāti axios kā parametrs kā javascript objekts.

Par GET pieprasījumu ar izvēles parametrs, dati būs pieejami ar req.query . Par sakot, POST un dzēst pieprasījumiem dati būs pieejami tieši pamattekstā pieprasījuma req.body . Dati būs javascript objekts, un katram īpašumam var piekļūt ar parastu punktu apzīmējumu.

res: ir atbildes saīsinājums un satur ekspress servera atbildi. Mēs vēlamies nosūtīt klientam atbildi, ko mēs saņēmām no mūsu datu bāzes , tāpēc mēs nododam datu bāzes atbildi šai res funkcijai, kas pēc tam to nosūta mūsu klientam.

nākamais: ir starpprogrammatūra, kas ļauj pārsūtīt atzvanīšanu nākamajai funkcijai.

Ievērojiet mūsu ekspress maršrutu, ko mēs darām, pool.queryun šis poolobjekts ir tas pats, kas satur mūsu iepriekš iestatītos un augšpusē importētos datu bāzes pieteikšanās akreditācijas datus. Vaicājums funkcija ļauj mums veikt SQL vaicājumus mūsu datu bāzē stīgu formātā. Ievērojiet arī to, ka es izmantoju “nevis pēdiņas”, kas ļauj man vaicāt vairākās rindiņās.

Tad mums ir komats aiz mūsu SQL vaicājuma un nākamais parametrs, kas ir bultiņas funkcija, kas jāizpilda pēc vaicājuma izpildes . mēs vispirms nododam 2 parametrus mūsu bultiņas funkcijai, q_errun tas q_resnozīmē vaicājuma kļūdu un vaicājuma atbildi . Lai nosūtītu datus uz Frontend mēs pāriet q_res.rowsuz res.jsonfunkciju. q_res.rowsir datu bāzes atbilde, jo šī ir SQL, un datu bāze mums atgriezīs atbilstošās rindas, pamatojoties uz mūsu vaicājumu. Tad mēs pārvērstu šos rindas uz json formātā , un nosūtīt to uz mūsu tīmekļa saskarnē ar resparametru.

Mēs varam arī norādīt izvēles vērtības mūsu SQL vaicājumiem , nododot masīvu pēc vaicājuma, atdalot ar komatu. Tad mēs varam piekļūt atsevišķus elementus šajā masīvā ar SQL vaicājumu ar sintaksi $1, kur $1ir pirmais elements masīvā. $2piekļūtu masīva otrajam elementam un tā tālāk. Ņemiet vērā, ka tā nav sistēma, kuras pamatā ir 0, piemēram, javascript, tādas nav$0

Sadalīsim katru no šiem maršrutiem un sniegsim īsu katra aprakstu.

Ziņu maršruti

  • / api / get / allposts: izgūst visus mūsu ierakstus no datu bāzes.  ORDER BY date_created DESCļauj mums vispirms rādīt jaunākās ziņas.
  • / api / post / posttodb: Lietotāja ziņu saglabā datu bāzē. Mēs saglabājam 4 nepieciešamās vērtības: nosaukums, pamatteksts, lietotāja ID, lietotājvārds vērtību masīvā.
  • / api / put / post: rediģē esošo datu bāzē. Mēs izmantojam UPDATE   komandu SQL un vēlreiz ievadām visas ziņas vērtības. Mēs uzmeklējam pastu ar pasta ID, kuru mēs iegūstam no mūsu priekšpuses.
  • / api / delete / postcomments: izdzēš visus ar ziņu saistītos komentārus. Sakarā ar PSQL ārējās atslēgas ierobežojumiem, mums ir izdzēst visus komentārus, kas saistīti ar amata pirms mēs varam izdzēst faktisko ziņu.
  • / api / delete / post: izdzēš ziņu ar ziņas ID.
  • / api / put / likes : Mēs iesniedzam pieprasījumu, lai pievienotu like_user_idmasīvam tā lietotāja ID, kuram patika ziņa, pēc tam palielinām likesskaitu par 1.

Komentāru maršruti

  • / api / post / commenttodb: Saglabā komentāru datu bāzē
  • / api / put / commenttodb: rediģē esošu komentāru datu bāzē
  • / api / delete / comment: Izdzēš vienu komentāru, tas atšķiras no visu ar ziņu saistīto komentāru dzēšanas.
  • / api / get / allpostcomments: izgūst visus ar vienu ziņu saistītos komentārus

Lietotāju maršruti

  • / api / posts / userprofiletodb: Saglabā lietotāja profila datus no auth0 mūsu pašu datu bāzē. Ja lietotājs jau pastāv, PostgreSQL neko nedara.
  • / api / get / userprofilefromdb: izgūst lietotāju, meklējot viņa e-pastu
  • / api / get / userposts: izgūst lietotāja izveidotās ziņas, meklējot visas ziņas, kas atbilst lietotāja ID.
  • / api / get / otheruserprofilefromdb: iegūstiet citu lietotāju profila datus no datu bāzes un skatiet viņu profila lapā.
  • / api / get / otheruserposts: iegūstiet citu lietotāju ziņas, kad skatāt viņu profila lapu

Globālā stāvokļa izveidošana ar reduktoriem, darbībām un kontekstu.

Lietotāja profila datu saglabāšana mūsu datu bāzē

Pirms mēs varam sākt globālā stāvokļa iestatīšanu, mums ir nepieciešams veids, kā saglabāt mūsu lietotāja profila datus mūsu pašu datu bāzē, pašlaik mēs tikai iegūstam datus no auth0. Mēs to darīsim savā 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;

Mēs uzstādījām lielāko daļu šī komponenta pēdējā apmācībā, tāpēc es iesaku apskatīt šo apmācību, lai iegūtu detalizētu skaidrojumu, bet šeit mēs veicam axios post pieprasījumu, kam seko tūlīt cits axios get pieprasījums nekavējoties iegūt lietotāja profila datus, kurus tikko saglabājām db.

Mēs to darām, jo ​​mums ir nepieciešams unikāls primārās atslēgas ID, ko ģenerē mūsu datu bāze, un tas ļauj mums saistīt šo lietotāju ar viņu komentāriem un ziņām . Un mēs izmantojam lietotāju e-pastu viņu meklēšanai, jo mēs nezinām, kāds ir viņu unikālais ID, kad viņi pirmo reizi reģistrējas. Visbeidzot, mēs saglabājam šīs datubāzes lietotāja profila datus mūsu globālajā stāvoklī.

* Ņemiet vērā, ka tas attiecas arī uz OAuth pieteikšanās gadījumiem, piemēram, Google un Facebook pieteikšanos.

Darbības un reduktori

Tagad mēs varam sākt iestatīt darbības un reduktorus kopā ar kontekstu, lai iestatītu šīs lietotnes globālo stāvokli.

Lai iestatītu kontekstu no nulles, skatiet manu iepriekšējo apmācību. Šeit mums būs nepieciešams tikai datu bāzes profila un visu ziņu statuss.

Vispirms mūsu darbību veidi

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"

Tagad mūsu rīcība

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

Visbeidzot, mūsu amata reduktors un autora reduktors

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

Tagad mums tie jāpievieno  

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

Tas ir viss, mēs tagad esam gatavi izmantot šo globālo stāvokli savās sastāvdaļās.

Klienta puses reakcijas lietotne

Tālāk mēs iestatīsim klienta puses reakcijas emuāru. Visi API izsaukumi šajā sadaļā tika iestatīti iepriekšējā ātro maršrutu sadaļā.

Tas tiks iestatīts 6 komponentos šādi.

addpost.js : Komponents ar veidlapu ziņu iesniegšanai.

editpost.js : Komponents ziņu rediģēšanai ar formu, kurā jau ir aizpildīti lauki.

posts.js : Komponents visu ziņu renderēšanai, tāpat kā tipiskā forumā.

showpost.js : Komponents atsevišķas ziņas renderēšanai pēc tam, kad lietotājs ir noklikšķinājis uz ziņas.

profile.js : Komponents, kas atveido ar lietotāju saistītās ziņas. Lietotāja informācijas panelis.

showuser.js : Komponents, kas parāda citu lietotāju profila datus un ziņas.

Kāpēc neizmantot Redux Form?

TDLR; Redux forma ir pārspīlēta vairumā lietošanas gadījumu.

Redux Form ir populāra bibliotēka, ko parasti izmanto React lietotnēs. Tātad, kāpēc to neizmantot šeit? Es izmēģināju Redux Form, taču šeit vienkārši nevarēju atrast tā izmantošanas gadījumu. Mums vienmēr jāpatur prātā galīgais lietojums, un es nevarēju izdomāt šīs lietotnes scenāriju, kurā mums būtu jāsaglabā veidlapas dati globālajā redukcijas stāvoklī.

Šajā lietotnē mēs vienkārši paņemam datus no parastās formas un nododam tos Axios, kas pēc tam tos pārsūta uz ātro serveri, kas tos beidzot saglabā datu bāzē. Otrs iespējamais gadījums ir editpost komponentam, kuru es apstrādāju, nododot ziņas datus elementa Link īpašībai.

Izmēģiniet Redux Form un pārbaudiet, vai varat to izdomāt gudri, taču mums tas nebūs vajadzīgs šajā lietotnē. Arī jebkuru Redux Form piedāvāto funkcionalitāti bez tā var veikt salīdzinoši vieglāk.

Redux forma lielākajā daļā gadījumu ir vienkārši pārspīlēta.

Tāpat kā ar ORM, mūsu lietotnei nav pamata pievienot vēl vienu nevajadzīgu sarežģītības slāni.

Veidlapas ir vienkārši vieglāk iestatīt, izmantojot parasto 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;

Addpost komponentā mums ir vienkārša 2 lauka forma, kurā lietotājs var ievadīt virsrakstu un pamattekstu. Veidlapa tiek iesniegta, izmantojot handlesubmit()mūsu izveidoto funkciju. handleSubmit()funkcija aizņem notikuma parametra atslēgvārdu, kas satur lietotāja iesniegto veidlapas datus uz.

Mēs izmantosim, event.preventDefault()lai apturētu lapas atkārtotu ielādi, jo React ir vienas lapas lietotne, un tas nebūtu vajadzīgs.

Axios post metode aizņem parametrs "dati", kas tiks izmantoti, lai turēt datus, kas tiks saglabāti datu bāzē. Mēs saņemt lietotājvārdu un USER_ID no pasaules valstī mēs apspriedām pēdējā nodaļā.

Faktiski datu ievietošana datu bāzē tiek veikta ātro maršrutu funkcijā ar SQL vaicājumiem, kurus mēs redzējām iepriekš. Pēc tam mūsu axios API izsaukums pārsūta datus uz mūsu ātro serveri, kas informāciju saglabās datu bāzē.

editpost.js

Tālāk mums ir mūsu editpost.jssastāvdaļa. Tas būs pamata komponents, lai rediģētu lietotāju ziņas. Tam varēs piekļūt tikai caur lietotāja profila lapu.

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: ir funkcionalitāte, ko piedāvā reaģēšanas maršrutētājs . Kad lietotājs noklikšķina uz ziņas no sava profila lapas, ziņas dati, uz kuriem viņi noklikšķināja, saites elementā tiek saglabāti štata rekvizītā un tas atšķiras no lokālā komponenta stāvokļa React no useStateāķa.

Šī pieeja mums piedāvā vienkāršāku datu saglabāšanas veidu, salīdzinot ar kontekstu, kā arī ietaupa API pieprasījumu. Mēs redzēsim, kā tas darbojas profile.jskomponentā.

Pēc tam mums ir pamata kontrolēta komponenta veidlapa, un mēs saglabājam datus par katru taustiņu uz React stāvokli.

Mūsu handleSubmit()funkcijā mēs apvienojam visus savus datus, pirms tos nosūtīt uz mūsu serveri axios put pieprasījumā.  

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;

Jūs pamanīsit, ka mums ir diezgan sarežģīts useEffect()zvans, lai saņemtu mūsu ziņas no mūsu datu bāzes. Tas ir tāpēc, ka mēs saglabājam savas ziņas no savas datu bāzes globālajā stāvoklī, lai ziņas joprojām būtu tur pat tad, ja lietotājs pāriet uz citu lapu.

Tas ļauj izvairīties no nevajadzīgiem API izsaukumiem uz mūsu serveri. Tāpēc mēs izmantojam nosacījumu, lai pārbaudītu, vai ziņas jau ir saglabātas konteksta stāvoklī.

Ja ziņas jau ir saglabātas globālajā štatā, mēs vienkārši iestatām globālajā stāvoklī tās vietējai valstij, kas ļauj mums inicializēt lapošanu.  

Lapošana

Šeit page_change()funkcijā mums ir pamata paginēšanas ieviešana . Mums pamatā ir mūsu 5 lapošanas bloku iestatīšana kā masīvs. Kad lapa mainās, masīvs tiek atjaunināts ar jaunajām vērtībām. Tas ir redzams funkcijas pirmajā ifpaziņojumā, page_change()pārējie 4 ifapgalvojumi ir tikai, lai apstrādātu pirmās 2 un pēdējās 2 lappušu izmaiņas.

Mums ir arī window.scrollTo()jāaicina, lai ritinātu uz augšu katras lapas izmaiņas gadījumā.

Izaiciniet sevi, lai uzzinātu, vai jūs varat izveidot sarežģītāku lapošanas ieviešanu, taču mūsu vajadzībām šī vienotā funkcija šeit ir laba.

mūsu lapošanai ir nepieciešamas 4 stāvokļa vērtības. Mums vajag:

  • num_posts: amatu skaits
  • posts_slice: daļa no visiem ierakstiem
  • currentPage: pašreizējā lapa
  • posts_per_page: Ziņu skaits katrā lapā.

Mums arī jānodod currentPagevalsts vērtība useEffect()āķim, kas ļauj mums aktivizēt funkciju katru reizi, kad tiek mainīta lapa. Mēs iegūstam indexOfLastPost , reizinot 3 reizes, currentPageun iegūstam indexOfFirstPostziņu, kuru vēlamies parādīt, atņemot 3. Pēc tam mēs varam iestatīt šo jauno sagriezto masīvu kā jaunu masīvu mūsu vietējā valstī.

Tagad par mūsu JSX. Mēs izmantojam flexbox, lai strukturētu un izkārtotu mūsu lapošanas blokus, nevis tradicionālos tradicionālos horizontālos sarakstus.

Mums ir 4 pogas, kas ļauj jums pāriet uz pašu pirmo lapu vai atpakaļ atpakaļ un otrādi. Tad mēs savā pages_slicemasīvā izmantojam kartes paziņojumu, kas mums dod mūsu lapošanas bloku vērtības. Lietotājs var arī noklikšķināt uz lapošanas bloka, kas lapā tiks nosūtīts kā arguments page_change()funkcijai.

Mums ir arī CSS klases, kas ļauj mums iestatīt stilu arī mūsu pagination.  

  • .pagination-active: šī ir parasta CSS klase, nevis pseido atlasītājs, kuru parasti redzat ar horizontāliem sarakstiem, piemēram .item:active,. Mēs pārslēdzam aktīvo klasi React JSX, salīdzinot currentPagear pages_slicemasīva lapu.
  • .pagination-item: visu paginēšanas bloku ieveidošana
  • .pagination-item:hover: stils, ko lietot, kad lietotājs novieto kursoru virs lapošanas bloka
 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

ir funkcionālā sastāvdaļa, kuru mēs izmantojam katra atsevišķa ziņojuma renderēšanai. Ziņu nosaukums ir tāds, uz Linkkura noklikšķinot, lietotājs tiks novirzīts uz katru atsevišķo ziņu ar komentāriem. Jūs arī pamanīsit, ka visā ziņojumā mēs nododam elementa stateīpašumam Link. Šis stateīpašums atšķiras no mūsu vietējā štata, tas faktiski ir īpašums, react-routerun mēs to sīkāk redzēsim showpost.jskomponentā. Mēs darām to pašu arī ar ziņas autoru.

Jūs pamanīsit arī dažas citas lietas, kas saistītas ar amatu meklēšanu, kuras es apspriedīšu nākamajās sadaļās.  

Es apspriedīšu arī showpost.jskomponenta "patīk" funkcionalitāti .

showpost.js

Tagad šeit mums ir neapšaubāmi vissarežģītākais komponents šajā lietotnē. Neuztraucieties, es to sadalīšu pilnīgi soli pa solim, tas nav tik biedējoši, kā izskatās.  

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;

Vispirms jūs pamanīsit gigantisku useStatezvanu. Es izskaidrošu, kā darbojas katrs īpašums, kad mēs vienlaikus izpētām savu komponentu.

useEffect () un API pieprasījumi

Vispirms mums jāapzinās, ka lietotājs var piekļūt ziņai divos dažādos veidos. Piekļuve tam no foruma vai pāreja uz to, izmantojot tiešo 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])

Ja viņi piekļūst tam no foruma, mēs to pārbaudām savā useEffect()aicinājumā un pēc tam iestatām vietējo valsti šim amatam. Tā kā mēs elementā izmantojam reaģēt maršrutētājastate rekvizītus , mums ir piekļuve visiem ziņu datiem, kas mums jau ir pieejami, izmantojot rekvizītus, kas ietaupa nevajadzīgu API izsaukumu.

Ja lietotājs pārlūkprogrammā ievada ziņas tiešo URL , mums nav citas izvēles, kā iesniegt API pieprasījumu, lai saņemtu ziņu, jo lietotājam ir jānoklikšķina uz ziņas no posts.jsforuma, lai ziņas datus saglabātu maršrutētāja stateīpašums.

Vispirms mēs izvelk ziņas ID no URL ar reaģētāja maršrutētāja pathnamerekvizītu, kuru pēc tam izmantojam kā param mūsu axios pieprasījumā . Pēc API pieprasījuma mēs vienkārši saglabājam atbildi mūsu vietējai valstij.

Pēc tam mums jāsaņem arī komentāri ar API pieprasījumu . Mēs varam izmantot to pašu ziņas ID URL iegūšanas metodi, lai meklētu ar ziņu saistītos komentārus.

RenderComments un animācijas

Šeit mums ir mūsu funkcionālais komponents, kuru mēs izmantojam, lai parādītu individuālu komentāru.

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

Vispirms mēs sākam, izmantojot divrindu trīskāršo izteicienu className, lai pārslēgtu stila klases. Ja delete_comment_idmūsu vietējā štats atbilst pašreizējam komentāra ID, tas tiek izdzēsts un komentāram tiek piemērota izbalēšanas animācija .

Mēs izmantojam @keyframeanimācijas. Es uzskatu, ka css @keyframeanimācijas ir daudz vienkāršākas nekā javascript balstītas pieejas ar tādām bibliotēkām kā react-springun react-transition-group.

Tālāk mēs parādījām faktisko komentāru

Seko trīskāršā izteiksme, kas nosaka izveidotā komentāra datumu “Rediģēts” vai “Tikai tūlīt”, pamatojoties uz lietotāju darbībām.  

Tālāk mums ir diezgan sarežģīta ligzdota trīskāršā izteiksme. Vispirms mēs salīdzinām cur_user_id(ko mēs iegūstam no mūsu context.dbProfileStatevalsts un iestatījām mūsu JSX) ar komentāra lietotāja ID . Ja ir atbilstība, mēs parādām rediģēšanas pogu .

Ja lietotājs noklikšķina uz pogas Rediģēt, mēs iestatām komentāram edit_commentstatusu un statusam edit_comment_idkomentāra ID . Un tas arī padara isEditing prop par patiesu, kas atver formu un ļauj lietotājam rediģēt komentāru. Kad lietotājs handleUpdate()noklikšķina uz Piekrītu, tiek izsaukta funkcija, kuru mēs redzēsim tālāk.

Komentāri CRUD operācijas

Šeit mums ir mūsu funkcijas apstrādāt CRUD operācijas komentāriem. Jūs redzēsiet, ka mums ir 2 funkciju kopas, no kurām viena ir paredzēta klienta puses CRUD apstrādei un otra API pieprasījumu apstrādei . Es paskaidrošu, kāpēc tālāk.

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

Tas ir tāpēc, ka, ja lietotājs iesniedz, rediģē vai izdzēš komentāru, lietotāja interfeiss netiks atjaunināts bez lapas atkārtotas ielādēšanas. To var atrisināt, veicot vēl vienu API pieprasījumu vai izveidojot tīmekļa kontaktligzdas iestatījumu, kas uzklausa datu bāzes izmaiņas, taču daudz vienkāršāks risinājums ir tikai programmatiski rīkoties ar klienta pusi.  

Visas klienta puses CRUD funkcijas tiek izsauktas attiecīgajos API izsaukumos.

Klienta puse CRUD:

  • handleCommentSubmit(): atjauniniet comments_arr, vienkārši pievienojot komentāru masīva sākumā.  
  • handleCommentUpdate(): Atrodiet un aizstājiet masīva komentāru ar indeksu, pēc tam atjauniniet un iestatiet jauno masīvu uz comments_arr
  • handleCommentDelete(): Atrodiet masīvā esošo komentāru ar komentāra ID, pēc tam .filter()to un saglabājiet jauno masīvu mapē comments_arr.

API pieprasījumi:

  • handleSubmit(): mēs iegūstam datus no savas veidlapas, pēc tam apvienojot dažādas nepieciešamās īpašības un nosūtot šos datus uz mūsu serveri. dataUn submitted_commentmainīgie ir atšķirīgi, jo mūsu klienta pusē Crud operācijas nepieciešams nedaudz atšķirīgas vērtības nekā mūsu datu bāzē.
  • handleUpdate(): šī funkcija ir gandrīz identiska mūsu handleSubmit()funkcijai. galvenā atšķirība ir tā, ka mēs veicam pārdošanas pieprasījumu, nevis amatu .
  • handleDeleteComment(): vienkārši izdzēst pieprasījumu, izmantojot komentāra ID.  

apstrāde Patīk

Tagad mēs varam apspriest, kā rīkoties, ja lietotājam patīk ziņa.

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

ar handleLikes()funkciju, mēs vispirms noteikt pasta id un lietotāja id . Tad mēs izmantojam nosacījumu, lai pārbaudītu, vai pašreizējais lietotāja ID nav like_user_idmasīvā, kurš atceras, ka ir visi to lietotāju lietotāju ID , kuriem šī ziņa jau ir iepatikusies.

Ja nē, tad mēs iesniedzam pieprasījumu savam serverim un pēc tam, kad esam izmantojuši citu nosacījumu, un pārbaudiet, vai lietotājam jau nav paticis šis amata klienta puse ar like_postvalsts īpašumu, tad atjauniniet patīk.  

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!

Mūsu trešajā .then()paziņojumā tiek dzēsti faktiskie lietotāja veiktie ieraksti.

Mūsu 4. .then()paziņojums beidzot izdzēš lietotāju no datu bāzes. Pēc tam mūsu piektais.then() novirza administratoru uz sākumlapu. Mūsu 4. .then()paziņojums atrodas mūsu 3. .then()paziņojumā tā paša iemesla dēļ, kāpēc mūsu 2. paziņojums atrodas mūsu pirmajā paziņojumā ..then()

Viss pārējais ir funkcionalitāte, kuru mēs jau iepriekš esam redzējuši vairākkārt, un tā noslēdz mūsu apmācību!

Paldies, ka lasījāt!