├── .gitignore ├── README.md ├── api ├── psql-con.js └── routes │ ├── entries.js │ ├── userFunctions.js │ ├── users.js │ └── verifyToken.js ├── client ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.css │ ├── App.css.map │ ├── App.js │ ├── App.scss │ ├── actions │ ├── pageActions.js │ └── userActions.js │ ├── components │ ├── Entry.js │ ├── Footer.js │ ├── Home.js │ ├── Nav.js │ ├── ProtectedRoute.js │ └── forms │ │ ├── NewEntry.js │ │ ├── SignIn.js │ │ └── SignUp.js │ ├── images │ ├── Envelope.png │ └── GitHub.png │ ├── index.js │ ├── reducers │ ├── index.js │ ├── pageReducer.js │ └── userReducer.js │ └── serviceWorker.js ├── dotenv_example ├── index.js ├── package-lock.json ├── package.json └── screenshots ├── entry-example.png ├── home-page.png ├── new-entry.png ├── sign-in.png └── sign-up.png /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-blog 2 | Open source blog built with: 3 | - **React** for creating a SPA 4 | - **Redux** for global state 5 | - **Express** for making back-end development easier 6 | - **bcryptjs** for hashing passwords 7 | - **JWT** for token creation and authentication 8 | - **node-postgres** for connecting to the database (PostgreSQL) 9 | - **SASS** for better css organization and readability 10 | 11 | ## Screenshots 12 | ![home-page](/screenshots/home-page.png) 13 | ![entry-example](/screenshots/entry-example.png) 14 | ![new-entry](/screenshots/new-entry.png) 15 | ![sign-in](/screenshots/sign-in.png) 16 | ![sign-up](/screenshots/sign-up.png) 17 | 18 | ## Features 19 | - Create and read entries 20 | - Create users 21 | - Hashed passwords 22 | - Token authentication 23 | - Protected routes 24 | - Sanitized inputs 25 | - Entry content accepts html tags so that the user has power to add images, lists and so forth 26 | 27 | ## How to install 28 | 29 | Step 1: installing dependencies 30 | ```bash 31 | git clone https://github.com/alochaus/react-blog.git 32 | cd react-blog/ 33 | 34 | #install server dependencies 35 | npm i 36 | 37 | #install client dependencies 38 | cd client/ 39 | npm i 40 | 41 | cd .. 42 | 43 | #if you want to run ONLY the server 44 | npm run server 45 | 46 | #if you want to run ONLY the client 47 | npm run client 48 | 49 | #if you want to run BOTH 50 | npm run dev 51 | ``` 52 | 53 | Step 2: creating tables in database 54 | ``` 55 | CREATE TABLE entries( 56 | header char(50) NOT NULL, 57 | subheader char(300) NOT NULL, 58 | cateogry char(100) NOT NULL, 59 | content text NOT NULL, 60 | author char(50) NOT NULL, 61 | date char(20) NOT NULL 62 | ); 63 | CREATE TABLE users( 64 | email char(100) NOT NULL, 65 | username char(20) NOT NULL, 66 | hash text NOT NULL 67 | ); 68 | ``` 69 | 70 | Step 3: setting environment variables 71 | Fill the dotenv_example with database information, create a jwtsecretkey and then save the file as ".env". 72 | 73 | Example: 74 | ``` 75 | PGUSER=Josh 76 | PGPASSWORD=mysupersecretpassword 77 | PGDATABASE=blog 78 | PGPORT=5432 79 | PGHOST=myhostname 80 | JWTSECRETKEY=as8dhj0a98sfh 81 | ``` 82 | 83 | -------------------------------------------------------------------------------- /api/psql-con.js: -------------------------------------------------------------------------------- 1 | const {Pool} = require('pg'); 2 | const pool = new Pool({ 3 | user: process.env.PGUSER, 4 | host: process.env.PGHOST, 5 | database: process.env.PGDATABASE, 6 | password: process.env.PGPASSWORD, 7 | port: process.env.PGPORT, 8 | }); 9 | 10 | pool.connect((err, client, release) => { 11 | if (err) { 12 | return console.error('Error acquiring client', err.stack) 13 | } 14 | client.query('SELECT NOW()', (err, result) => { 15 | release() 16 | if (err) { 17 | return console.error('Error executing query', err.stack) 18 | } 19 | console.log(result.rows) 20 | }) 21 | }); 22 | 23 | module.exports = { 24 | query: (text, params, callback) => { 25 | return pool.query(text, params, callback) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/routes/entries.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const db = require("../psql-con.js"); 3 | const verify = require("./verifyToken.js"); 4 | 5 | router.get("/:title", async (req, res) => { 6 | const { title } = req.params; 7 | const regex = /^[\w\s-!?'‘’.,()<>:\[\]]+$/; 8 | if (regex.test(title)) { 9 | const header = title.replace(/-/g, ""); 10 | const { 11 | rows 12 | } = await db.query("SELECT * FROM entries WHERE LOWER(header)=LOWER($1)", [ 13 | header 14 | ]); 15 | if (rows.length) { 16 | res.status(200).send(rows); 17 | } else { 18 | res.status(204).send([]); 19 | } 20 | } else { 21 | res.status(400).send([]); 22 | } 23 | }); 24 | 25 | router.get("/page/:page", async (req, res) => { 26 | const entriesPerPage = 6; 27 | const offset = (Number(req.params.page) - 1) * entriesPerPage; 28 | if (typeof offset == "number" && offset >= 0) { 29 | let { 30 | rows 31 | } = await db.query( 32 | "SELECT * FROM (SELECT *, ROW_NUMBER() OVER() FROM entries) x ORDER BY ROW_NUMBER DESC OFFSET $1 ROWS FETCH FIRST $2 ROWS ONLY", 33 | [offset, entriesPerPage] 34 | ); 35 | rows.map((row, index) => { 36 | let url = row.header; 37 | link = url.toLowerCase().replace(/\s/g, "-"); 38 | rows[index].link = link; 39 | }); 40 | res.status(200).send(await rows); 41 | } else { 42 | res.status(204).send([]); 43 | } 44 | }); 45 | 46 | router.post("/new", verify, async (req, res) => { 47 | const { header, subheader, category, content } = req.body; 48 | const author = req.user.username; 49 | 50 | const date = new Date(); 51 | const hhmm = 52 | String(date.getHours()).padStart(2, "0") + 53 | ":" + 54 | String(date.getMinutes()).padStart(2, "0"); 55 | 56 | const dd = String(date.getDate()).padStart(2, "0"); 57 | const mm = String(date.getMonth() + 1).padStart(2, "0"); 58 | const yy = String(date.getFullYear()).padStart(2, "0"); 59 | 60 | const dateStr = dd + "/" + mm + "/" + yy + " " + hhmm; 61 | 62 | if ( 63 | header.trim() != "" && 64 | subheader.trim() != "" && 65 | category.trim() != "" && 66 | content.trim() != "" 67 | ) { 68 | const { 69 | rows 70 | } = await db.query("INSERT INTO entries VALUES($1, $2, $3, $4, $5, $6)", [ 71 | header, 72 | subheader, 73 | category, 74 | content, 75 | author, 76 | dateStr 77 | ]); 78 | res.status(201).send({ msg: "Your entry has been added to the database." }); 79 | } else { 80 | res.status(400).send({ msg: "Please fill all the fields." }); 81 | } 82 | }); 83 | 84 | module.exports = router; 85 | -------------------------------------------------------------------------------- /api/routes/userFunctions.js: -------------------------------------------------------------------------------- 1 | const db = require('../psql-con.js'); 2 | 3 | module.exports = { 4 | // returns true if the input inserted by the user meets the requirements in the form. 5 | validUser: (email, username, password) => { 6 | const emailRegEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 7 | const validEmail = emailRegEx.test(email); 8 | 9 | const validUsername = typeof username == 'string' && username.trim().length > 2; 10 | 11 | // minimum eight characters, at least one uppercase letter, one lowercase letter and one number 12 | const passwordRegEx = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/; 13 | const validPassword = passwordRegEx.test(password); 14 | return validEmail && validUsername && validPassword; 15 | }, 16 | 17 | // returns true if email exists 18 | emailExists: async (email) => { 19 | const {rows} = await db.query('SELECT email FROM users WHERE email = $1', [email]); 20 | if(rows.length === 0){ 21 | return false; 22 | } 23 | return true; 24 | }, 25 | 26 | // returns true if username exists 27 | usernameExists: async (username) => { 28 | const {rows} = await db.query('SELECT username FROM users WHERE username = $1', [username]); 29 | if(rows.length === 0){ 30 | return false; 31 | } 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/routes/users.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const bcrypt = require('bcryptjs'); 3 | const db = require('../psql-con.js'); 4 | const jwt = require('jsonwebtoken'); 5 | const verify = require('./verifyToken.js'); 6 | const {validUser, emailExists, usernameExists} = require('./userFunctions.js'); 7 | 8 | router.post('/signup', async (req, res) => { 9 | const {email, username, password, c_password} = req.body; 10 | var err = []; 11 | if(validUser(email, username, password)){ 12 | const emailResponse = await emailExists(email); 13 | err = emailResponse ? [...err, "This email is already in use."] : [...err]; 14 | 15 | const usernameResponse = await usernameExists(username) 16 | err = usernameResponse ? [...err, "This username is already in use."] : [...err]; 17 | 18 | if(Object.keys(err).length === 0){ 19 | const hash = bcrypt.hashSync(password, 10); 20 | await db.query('INSERT INTO users VALUES($1, $2, $3)', [email, username, hash]); 21 | res.status(201).send(["User successfully registered!"]) 22 | } else{ 23 | res.status(400).send(err); 24 | } 25 | } else{ 26 | res.status(400).send(["Inputted data does not meet the requirements."]); 27 | } 28 | }); 29 | 30 | router.post('/signin', async (req, res) => { 31 | const {email, password} = req.body; 32 | const emailResponse = await emailExists(email); 33 | if(!emailResponse){ 34 | res.status(401).send({msg:"Email or password is incorrect."}); 35 | } else{ 36 | const {rows} = await db.query('SELECT username, email, hash FROM users WHERE email = $1', [email]); 37 | const hash = rows[0].hash; 38 | if(bcrypt.compareSync(password, hash)){ 39 | const {email, username} = rows[0]; 40 | const token = jwt.sign({username, email}, process.env.JWTSECRETKEY, {expiresIn:"1h"}); 41 | res.status(200).send({"token":token, user:{username, email}}); 42 | } else{ 43 | res.status(401).send({msg:"Email or password is incorrect."}); 44 | } 45 | } 46 | }); 47 | 48 | router.post('/isLogged', verify, async (req, res) => { 49 | res.status(200).send({isLogged:true, email: req.user.email, username: req.user.username}); 50 | }); 51 | 52 | module.exports = router; 53 | -------------------------------------------------------------------------------- /api/routes/verifyToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | module.exports = (req, res, next) => { 4 | const {token} = req.body; 5 | const tokenRegEx = /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/; 6 | if(!token) return res.status(401); 7 | if(tokenRegEx.test(token)){ 8 | try{ 9 | const verified = jwt.verify(token, process.env.JWTSECRETKEY); 10 | req.user = verified; 11 | next(); 12 | } catch(err){ 13 | res.status(401).send({isLogged:false, email:'', username:''}); 14 | } 15 | } else{ 16 | res.status(401).send({isLogged:false, email:'', username:''}); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-redux": "^7.1.0", 9 | "react-router": "^5.0.1", 10 | "react-router-dom": "^5.0.1", 11 | "react-scripts": "^3.4.1", 12 | "redux": "^4.0.4", 13 | "redux-logger": "^3.0.6", 14 | "redux-thunk": "^2.3.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | }, 37 | "proxy": "http://localhost:5000" 38 | } 39 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alochaus/react-blog/3f7a7cb1d0a32a1df6b571c9d51182ae8abb00da/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | React App 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | text-align: center; 5 | } 6 | 7 | body { 8 | background-color: #eee; 9 | } 10 | 11 | .navbar { 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | background-color: #024cb5; 16 | color: #fff; 17 | } 18 | .navbar ul { 19 | display: flex; 20 | } 21 | .navbar li { 22 | display: block; 23 | list-style: none; 24 | padding: 1rem; 25 | } 26 | .navbar a { 27 | color: #fff; 28 | font-size: 10pt; 29 | font-weight: bold; 30 | text-decoration: none; 31 | } 32 | 33 | .title { 34 | margin: 0.5rem; 35 | } 36 | .title h1 > a { 37 | text-decoration: none; 38 | font-size: 25pt; 39 | color: white; 40 | } 41 | 42 | .footer { 43 | position: fixed; 44 | bottom: 0; 45 | width: 100%; 46 | height: 40px; 47 | background-color: #024cb5; 48 | } 49 | .footer a img { 50 | height: 80%; 51 | transform: translateY(13%); 52 | padding-right: 10px; 53 | } 54 | .footer a .envelope { 55 | height: 50%; 56 | transform: translateY(-5%); 57 | } 58 | 59 | .togglebtn { 60 | cursor: pointer; 61 | position: absolute; 62 | font-size: 25pt; 63 | display: none; 64 | top: 0.7rem; 65 | right: 0.5rem; 66 | } 67 | 68 | .entry { 69 | margin: 0 auto; 70 | margin-top: 10px; 71 | padding: 15px; 72 | width: 700px; 73 | border: 3px solid #f7f7f7; 74 | border-radius: 10px; 75 | background-color: #fff; 76 | } 77 | .entry div { 78 | text-align: left; 79 | margin-bottom: 50px; 80 | } 81 | .entry div ul { 82 | margin-left: 20px; 83 | } 84 | .entry div ul ul { 85 | margin-left: 15px; 86 | } 87 | .entry div ul li { 88 | text-align: left; 89 | } 90 | .entry div img { 91 | margin: 20px auto; 92 | width: 100%; 93 | } 94 | .entry div p { 95 | margin: 10px 0; 96 | } 97 | .entry a { 98 | text-decoration: none; 99 | } 100 | .entry p { 101 | text-align: left; 102 | } 103 | .entry .header { 104 | font-size: 20pt; 105 | font-weight: bold; 106 | color: #024cb5; 107 | } 108 | .entry .subheader { 109 | font-size: 15pt; 110 | color: #111; 111 | } 112 | .entry .category { 113 | font-size: 8pt; 114 | margin-top: 1px; 115 | color: #333; 116 | } 117 | .entry .author { 118 | float: right; 119 | font-size: 8pt; 120 | } 121 | .entry .date { 122 | font-size: 8pt; 123 | } 124 | 125 | .mfooter { 126 | margin-bottom: 70px; 127 | } 128 | 129 | .page-btn { 130 | cursor: pointer; 131 | border: 3px solid #024cb5; 132 | background: #fff; 133 | color: #024cb5; 134 | padding: 10px 20px; 135 | letter-spacing: 3px; 136 | transition: 0.5s; 137 | } 138 | .page-btn:hover { 139 | border: 3px solid #9dc0fa; 140 | color: #9dc0fa; 141 | } 142 | 143 | .form { 144 | margin: 10px auto; 145 | margin-bottom: 60px; 146 | padding: 12px; 147 | width: 50%; 148 | border: 3px solid #f7f7f7; 149 | border-radius: 10px; 150 | background-color: #fff; 151 | } 152 | .form .newEntry input[type=text] { 153 | width: 50%; 154 | } 155 | .form .requirements { 156 | border: 3px solid #9dc0fa; 157 | border-radius: 10px; 158 | background-color: #c2d7f9; 159 | margin-bottom: 10px; 160 | } 161 | .form .requirements h2 { 162 | padding-top: 10px; 163 | margin-bottom: 10px; 164 | } 165 | .form .requirements ul { 166 | padding-left: 20px; 167 | padding-bottom: 10px; 168 | } 169 | .form .requirements ul li { 170 | text-align: left; 171 | } 172 | .form h1 { 173 | font-size: 24px; 174 | text-align: left; 175 | color: #14171a; 176 | padding-bottom: 15px; 177 | } 178 | .form form { 179 | padding-right: 12px; 180 | text-align: left; 181 | border-radius: none; 182 | } 183 | .form form input, .form form textarea { 184 | margin-bottom: 10px; 185 | font-size: 11pt; 186 | text-align: left; 187 | } 188 | .form form textarea { 189 | box-shadow: none; 190 | width: 99.5%; 191 | height: 240px; 192 | padding: 7px; 193 | } 194 | 195 | @media (max-width: 769px) { 196 | .togglebtn { 197 | display: flex; 198 | } 199 | 200 | .links { 201 | display: none; 202 | width: 100%; 203 | } 204 | 205 | .active { 206 | display: block; 207 | } 208 | 209 | .navbar { 210 | align-items: flex-start; 211 | flex-direction: column; 212 | } 213 | 214 | .navbar ul { 215 | flex-direction: column; 216 | width: 100%; 217 | } 218 | .navbar ul li:hover { 219 | background-color: #1f86fc; 220 | } 221 | 222 | .entry, .form { 223 | width: 84.7%; 224 | } 225 | } 226 | 227 | /*# sourceMappingURL=App.css.map */ 228 | -------------------------------------------------------------------------------- /client/src/App.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["App.scss"],"names":[],"mappings":"AAcA;EACE;EACA;EACA;;;AAGF;EACE,kBAbW;;;AAgBb;EACE;EACA;EACA;EACA,kBA3BU;EA4BV,OAvBS;;AAwBT;EACE;;AAEF;EACE;EACA;EACA;;AAEF;EACE,OAjCO;EAkCP;EACA;EACA;;;AAIJ;EACE;;AACA;EACE;EACF;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA,kBA3DU;;AA6DR;EACE;EACA;EACA;;AAEF;EACE;EACA;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA,kBApFS;;AAqFT;EACE;EACA;;AACA;EACE;;AACA;EACE;;AAEF;EACE;;AAGJ;EACE;EACA;;AAEF;EACE;;AAIJ;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA,OA1HQ;;AA6HV;EACE;EACA,OArHO;;AAwHT;EACE;EACA;EACA,OA1HO;;AA6HT;EACE;EACA;;AAEF;EACE;;;AAIJ;EACE;;;AAGF;EACE;EACA;EACA,YAnJS;EAoJT,OAzJU;EA0JV;EACA;EACA;;AAEA;EACE;EACA,OA9JS;;;AAkKb;EACE;EACA;EACA;EACA;EACA;EACA;EACA,kBAtKS;;AAyKP;EACE;;AAIJ;EAYE;EACA;EACA;EACA;;AAdA;EACE;EACA;;AAEF;EACE;EACA;;AACA;EACA;;AASJ;EACE;EACA;EACA;EACA;;AAEF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEF;EACE;EACA;EACA;EACA;;;AAKN;EACE;IACE;;;EAEF;IACE;IACA;;;EAEF;IACE;;;EAEF;IACE;IACA;;;EAEF;IACE;IACA;;EAEA;IACE,kBAjPQ;;;EAqPZ;IACE","file":"App.css"} -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'; 3 | import {useDispatch} from 'react-redux'; 4 | import {verifyToken} from './actions/userActions.js' 5 | 6 | import './App.css'; 7 | 8 | /* COMPONENTS */ 9 | import {PrivateRoute} from './components/ProtectedRoute.js'; 10 | import Home from './components/Home.js'; 11 | import Entry from './components/Entry.js'; 12 | import Nav from './components/Nav.js'; 13 | import Footer from './components/Footer.js'; 14 | import SignIn from './components/forms/SignIn.js'; 15 | import SignUp from './components/forms/SignUp.js'; 16 | import NewEntry from './components/forms/NewEntry.js'; 17 | 18 | export default function App() { 19 | const dispatch = useDispatch(); 20 | 21 | useEffect(() => { 22 | dispatch(verifyToken()); 23 | }, [dispatch]) 24 | 25 | return ( 26 |
27 | 28 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /client/src/App.scss: -------------------------------------------------------------------------------- 1 | $colourOne: #00134e; 2 | $colourTwo: #024cb5; 3 | $colourThree: #1f86fc; 4 | $colourFour: #9dc0fa; 5 | $colourFive: #e8dacd; 6 | 7 | $whiteOne: #fff; 8 | $whiteTwo: #f7f7f7; 9 | $whiteThree: #eee; 10 | $whiteFour: #dcdcdc; 11 | 12 | $blackOne: #111; 13 | $blackTwo: #333; 14 | 15 | *{ 16 | margin:0; 17 | padding:0; 18 | text-align:center; 19 | } 20 | 21 | body{ 22 | background-color:$whiteThree; 23 | } 24 | 25 | .navbar{ 26 | display:flex; 27 | justify-content:space-between; 28 | align-items:center; 29 | background-color:$colourTwo; 30 | color:$whiteOne; 31 | ul{ 32 | display:flex; 33 | } 34 | li{ 35 | display:block; 36 | list-style:none; 37 | padding:1rem; 38 | } 39 | a{ 40 | color:$whiteOne; 41 | font-size:10pt; 42 | font-weight:bold; 43 | text-decoration:none; 44 | } 45 | } 46 | 47 | .title{ 48 | margin:.5rem; 49 | h1 > a{ 50 | text-decoration:none; 51 | font-size:25pt; 52 | color:white; 53 | } 54 | } 55 | 56 | .footer{ 57 | position:fixed; 58 | bottom:0; 59 | width:100%; 60 | height:40px; 61 | background-color:$colourTwo; 62 | a{ 63 | img{ 64 | height:80%; 65 | transform: translateY(13%); 66 | padding-right:10px; 67 | } 68 | .envelope{ 69 | height:50%; 70 | transform: translateY(-5%); 71 | } 72 | } 73 | } 74 | 75 | .togglebtn{ 76 | cursor:pointer; 77 | position:absolute; 78 | font-size:25pt; 79 | display:none; 80 | top:0.7rem; 81 | right:0.5rem; 82 | } 83 | 84 | .entry{ 85 | margin:0 auto; 86 | margin-top:10px; 87 | padding:15px; 88 | width:700px; 89 | border:3px solid $whiteTwo; 90 | border-radius:10px; 91 | background-color:$whiteOne; 92 | div{ 93 | text-align:left; 94 | margin-bottom:50px; 95 | ul{ 96 | margin-left:20px; 97 | ul{ 98 | margin-left:15px; 99 | } 100 | li{ 101 | text-align:left 102 | } 103 | } 104 | img{ 105 | margin:20px auto; 106 | width:100%; 107 | } 108 | p{ 109 | margin:10px 0; 110 | } 111 | } 112 | 113 | a{ 114 | text-decoration:none; 115 | } 116 | 117 | p{ 118 | text-align:left; 119 | } 120 | 121 | .header{ 122 | font-size:20pt; 123 | font-weight:bold; 124 | color:$colourTwo; 125 | } 126 | 127 | .subheader{ 128 | font-size:15pt; 129 | color:$blackOne; 130 | } 131 | 132 | .category{ 133 | font-size:8pt; 134 | margin-top:1px; 135 | color:$blackTwo; 136 | } 137 | 138 | .author{ 139 | float:right; 140 | font-size:8pt; 141 | } 142 | .date{ 143 | font-size:8pt; 144 | } 145 | } 146 | 147 | .mfooter{ 148 | margin-bottom:70px; 149 | } 150 | 151 | .page-btn{ 152 | cursor:pointer; 153 | border:3px solid $colourTwo; 154 | background:$whiteOne; 155 | color:$colourTwo; 156 | padding:10px 20px; 157 | letter-spacing:3px; 158 | transition:0.5s; 159 | 160 | &:hover{ 161 | border:3px solid $colourFour; 162 | color:$colourFour; 163 | } 164 | } 165 | 166 | .form{ 167 | margin:10px auto; 168 | margin-bottom:60px; 169 | padding:12px; 170 | width:50%; 171 | border:3px solid $whiteTwo; 172 | border-radius:10px; 173 | background-color:$whiteOne; 174 | 175 | .newEntry{ 176 | input[type=text]{ 177 | width:50%; 178 | } 179 | } 180 | 181 | .requirements{ 182 | h2{ 183 | padding-top:10px; 184 | margin-bottom:10px; 185 | } 186 | ul{ 187 | padding-left:20px; 188 | padding-bottom:10px; 189 | li{ 190 | text-align:left; 191 | } 192 | } 193 | border:3px solid $colourFour; 194 | border-radius:10px; 195 | background-color:#c2d7f9; 196 | margin-bottom:10px; 197 | } 198 | 199 | h1{ 200 | font-size:24px; 201 | text-align:left; 202 | color:#14171a; 203 | padding-bottom:15px; 204 | } 205 | form{ 206 | padding-right:12px; 207 | text-align:left; 208 | border-radius:none; 209 | 210 | input, textarea{ 211 | margin-bottom:10px; 212 | font-size:11pt; 213 | text-align:left; 214 | } 215 | textarea{ 216 | box-shadow:none; 217 | width:99.5%; 218 | height:240px; 219 | padding:7px; 220 | } 221 | } 222 | } 223 | 224 | @media(max-width:769px){ 225 | .togglebtn{ 226 | display:flex; 227 | } 228 | .links{ 229 | display:none; 230 | width:100%; 231 | } 232 | .active{ 233 | display:block; 234 | } 235 | .navbar{ 236 | align-items:flex-start; 237 | flex-direction:column; 238 | } 239 | .navbar ul{ 240 | flex-direction:column; 241 | width:100%; 242 | 243 | li:hover{ 244 | background-color:$colourThree; 245 | } 246 | } 247 | 248 | .entry, .form{ 249 | width:84.7%; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /client/src/actions/pageActions.js: -------------------------------------------------------------------------------- 1 | export const setPage = (payload) => { 2 | if(typeof payload === 'number'){ 3 | return{type:'SET_PAGE', payload}; 4 | } else{ 5 | return{type:'SET_PAGE', payload:0}; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/actions/userActions.js: -------------------------------------------------------------------------------- 1 | export const verifyToken = () => { 2 | return dispatch => { 3 | fetch('/api/user/isLogged', { 4 | method:'POST', 5 | headers:{'Content-Type':'application/json'}, 6 | body:JSON.stringify({token: localStorage.getItem('token')}) 7 | }) 8 | .then((res) => res.json()) 9 | .then((payload) => dispatch({type: 'VERIFY', payload})); 10 | } 11 | } 12 | 13 | export const signOut = () => { 14 | return{type:'SIGN_OUT'}; 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/Entry.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | 3 | export default function(props){ 4 | const [entry, setEntry] = useState({}); 5 | 6 | useEffect(() => { 7 | const fetchEntry = async () => { 8 | let res = await fetch(`/api/entries/${props.match.params.id}`); 9 | res = await res.json(); 10 | setEntry({...res[0]}); 11 | } 12 | fetchEntry(); 13 | }, [props.match.params.id]); 14 | 15 | const displayEntry = 16 |
17 |

{entry.header}

18 |

{entry.subheader}

19 |

{entry.category}

20 |
21 |

Written by: {entry.author}

22 |

{entry.date}

23 |
24 | 25 | return( 26 | <> 27 | {displayEntry} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /client/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GitHub from '../images/GitHub.png'; 3 | import Envelope from '../images/Envelope.png'; 4 | 5 | export default function Footer(){ 6 | return( 7 |
8 | GitHub 9 | Envelope 10 |
11 | ); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /client/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import {useSelector, useDispatch} from 'react-redux'; 4 | import {setPage} from '../actions/pageActions.js'; 5 | 6 | export default function Home(props){ 7 | const [entries, setEntries] = useState([]); 8 | const dispatch = useDispatch(); 9 | const page = useSelector(state => state.pageReducer); 10 | 11 | useEffect(() => { 12 | const fetchEntries = async () => { 13 | let res = await fetch(`/api/entries/page/${page}`); 14 | res = await res.json(); 15 | setEntries([...res]); 16 | }; 17 | 18 | fetchEntries(); 19 | }, [page]); 20 | 21 | const lastEntry = entries[entries.length - 1]; 22 | 23 | const displayEntries = entries.map((entries) => 24 |
25 |

{entries.header}

26 |

{entries.subheader}

27 |

{entries.category}

28 |

Written by: {entries.author}

29 |

{entries.date}

30 |
31 | ); 32 | 33 | const displayPrevBtn = (lastEntry === undefined || page === 1) ? 34 | null : dispatch(setPage(page - 1))}>; 35 | 36 | const displayNextBtn = (lastEntry === undefined || lastEntry.row_number === '1') ? 37 | null : dispatch(setPage(page + 1))}>; 38 | 39 | return( 40 | <> 41 | {displayEntries} 42 |
43 |
44 | {displayPrevBtn}{displayNextBtn} 45 |
46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import {useSelector, useDispatch} from 'react-redux'; 4 | import {signOut} from '../actions/userActions.js'; 5 | 6 | import {setPage} from '../actions/pageActions.js'; 7 | 8 | function Nav(){ 9 | const [active, setActive] = useState(false); 10 | const dispatch = useDispatch(); 11 | const isLogged = useSelector((state) => state.userReducer.isLogged); 12 | const hamburger = () => { 13 | setActive(!active); 14 | }; 15 | const removeToken = async () => { 16 | await setActive(false); 17 | localStorage.removeItem('token'); 18 | dispatch(signOut()); 19 | } 20 | 21 | const links = (!isLogged) ? ( 22 | <> 23 |
  • setActive(false)}>Sign in
  • 24 |
  • setActive(false)}>Sign up
  • 25 | ) : ( 26 | <> 27 |
  • setActive(false)}>New Entry
  • 28 |
  • Sign out
  • 29 | ); 30 | 31 | const isActive = (active) ? (" active") : (""); 32 | 33 | return( 34 |
    35 | 52 |
    53 | ); 54 | } 55 | 56 | export default Nav; 57 | -------------------------------------------------------------------------------- /client/src/components/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import {Route} from 'react-router-dom'; 3 | import SignIn from './forms/SignIn'; 4 | import {useSelector, useDispatch} from 'react-redux'; 5 | import {verifyToken} from '../actions/userActions.js' 6 | 7 | export const PrivateRoute = ({component: Component, ...rest}) => { 8 | const dispatch = useDispatch(); 9 | const isLogged = useSelector(state => state.userReducer.isLogged); 10 | 11 | 12 | useEffect(() => { 13 | dispatch(verifyToken()); 14 | }, [dispatch]); 15 | 16 | return( 17 | (isLogged) ? : 19 | } /> 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /client/src/components/forms/NewEntry.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | 3 | export default function SignIn(){ 4 | const [data, setData] = useState({ 5 | header:"", 6 | subheader:"", 7 | category:"", 8 | content:"", 9 | msg:"", 10 | }); 11 | 12 | const onChange = (event) => { 13 | setData({...data, [event.target.name]: event.target.value}); 14 | }; 15 | 16 | const onSubmit = async (event) => { 17 | event.preventDefault(); 18 | const {header, subheader, category, content} = data; 19 | let res = await fetch('/api/entries/new', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({token:localStorage.getItem('token'), header, subheader, category, content})}); 20 | res = await res.json(); 21 | setData({...data, msg:res.msg, header:"", subheader:"", category:"", content:""}); 22 | } 23 | 24 | const result = (data.msg) ? (

    {data.msg}

    ) : null; 25 | 26 | return( 27 |
    28 |

    New Entry

    29 |
    30 | 31 |
    32 | 33 |
    34 | 35 |
    36 |