├── client ├── .gitignore ├── .nvmrc ├── src │ ├── components │ │ ├── Layout │ │ │ ├── Layout.scss │ │ │ └── Layout.js │ │ ├── Navbars │ │ │ ├── HorizontalNavbar.scss │ │ │ └── HorizontalNavbar.js │ │ ├── Jumbotron │ │ │ ├── Jumbotron.js │ │ │ └── Jumbotron.scss │ │ ├── Footer │ │ │ ├── Footer.scss │ │ │ └── FooterComponent.js │ │ └── PostsGrid │ │ │ ├── PostsGrid.scss │ │ │ └── PostsGrid.js │ ├── pages │ │ ├── NotFound │ │ │ ├── NotFound.scss │ │ │ └── NotFound.js │ │ ├── Login │ │ │ ├── Login.scss │ │ │ ├── Login.js │ │ │ └── LoginForm.js │ │ ├── Home │ │ │ ├── Home.scss │ │ │ └── Home.js │ │ ├── Posts │ │ │ ├── Post.scss │ │ │ ├── UserPosts.js │ │ │ ├── NewPost.js │ │ │ ├── EditPost.js │ │ │ └── Post.js │ │ ├── Users │ │ │ ├── UserProfile.scss │ │ │ └── UserProfile.js │ │ ├── Signup │ │ │ ├── Signup.js │ │ │ └── SignupForm.js │ │ └── Comments │ │ │ ├── Comments.scss │ │ │ ├── CommentForm.js │ │ │ ├── CommentsMobile.js │ │ │ ├── CommentsDesktop.js │ │ │ └── Comments.js │ ├── assets │ │ ├── images │ │ │ ├── blogging.ico │ │ │ ├── blogging.png │ │ │ ├── default-user.png │ │ │ └── default-post-image.jpg │ │ └── styles │ │ │ ├── variables.scss │ │ │ └── index.scss │ ├── redux │ │ ├── actions │ │ │ ├── index.js │ │ │ └── actionCreator.js │ │ ├── sagas │ │ │ ├── rootSaga.js │ │ │ └── userAuthSaga.js │ │ ├── reducers │ │ │ ├── rootReducer.js │ │ │ └── userAuthReducer.js │ │ └── store │ │ │ ├── sessionStorage.js │ │ │ └── store.js │ ├── api │ │ ├── authToken.js │ │ └── api.js │ ├── index.js │ ├── NotLoggedInRoute.js │ ├── LoggedInRoute.js │ └── App.js ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo512.png │ ├── manifest.json │ └── index.html └── package.json ├── .vscode └── settings.json ├── screenshot-1.png ├── screenshot-2.png ├── app.js ├── src ├── routes │ ├── indexRoutes.js │ ├── commentRoutes.js │ ├── userRoutes.js │ ├── postRoutes.js │ └── authRoutes.js ├── models │ ├── comment.js │ ├── post.js │ └── user.js ├── middlewares │ ├── index.js │ ├── passport.js │ └── validator.js ├── db │ └── dbConfig.js ├── services │ ├── commentServices.js │ ├── postServices.js │ └── userServices.js ├── server.js └── seedDB │ ├── seedUsers.js │ └── seedPosts.js ├── .gitignore ├── package.json └── README.md /client/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /client/.nvmrc: -------------------------------------------------------------------------------- 1 | 14.16.0 -------------------------------------------------------------------------------- /client/src/components/Layout/Layout.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/pages/NotFound/NotFound.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/components/Navbars/HorizontalNavbar.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/mern-blog/HEAD/screenshot-1.png -------------------------------------------------------------------------------- /screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/mern-blog/HEAD/screenshot-2.png -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require = require("esm")(module /*, options*/); 2 | module.exports = require("./src/server"); 3 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/mern-blog/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/mern-blog/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /client/src/assets/images/blogging.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/mern-blog/HEAD/client/src/assets/images/blogging.ico -------------------------------------------------------------------------------- /client/src/assets/images/blogging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/mern-blog/HEAD/client/src/assets/images/blogging.png -------------------------------------------------------------------------------- /client/src/assets/images/default-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/mern-blog/HEAD/client/src/assets/images/default-user.png -------------------------------------------------------------------------------- /client/src/assets/images/default-post-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/mern-blog/HEAD/client/src/assets/images/default-post-image.jpg -------------------------------------------------------------------------------- /client/src/assets/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $LightestBlue: #d2e9ff; 2 | $DarkBlue: #001529; 3 | 4 | $LightestGray: rgb(204, 204, 204); 5 | $DarkGray: rgb(97, 97, 97); 6 | -------------------------------------------------------------------------------- /client/src/redux/actions/index.js: -------------------------------------------------------------------------------- 1 | export const userAuth = { 2 | LOGIN: "LOGIN", 3 | UPDATE_USER: "UPDATE_USER", 4 | LOGIN_SUCCESS: "LOGIN_SUCCESS", 5 | LOGIN_FAILED: "LOGIN_FAILED", 6 | LOGOUT: "LOGOUT", 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/components/Jumbotron/Jumbotron.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Jumbotron.scss"; 3 | 4 | export default function Jumbotron({ children }) { 5 | return
{children}
; 6 | } 7 | -------------------------------------------------------------------------------- /client/src/pages/Login/Login.scss: -------------------------------------------------------------------------------- 1 | @import "./../../assets/styles/variables.scss"; 2 | 3 | .login { 4 | height: calc(100vh - 106px); 5 | display: flex; 6 | justify-content: space-around; 7 | align-items: center; 8 | } 9 | -------------------------------------------------------------------------------- /src/routes/indexRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.get("/", async (req, res) => { 6 | res.json({ msg: "hi" }); 7 | }); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /client/src/redux/sagas/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all, call } from "redux-saga/effects"; 2 | import { createLoginStart } from "./userAuthSaga"; 3 | 4 | export default function* rootSaga() { 5 | yield all([call(createLoginStart)]); 6 | } 7 | -------------------------------------------------------------------------------- /client/src/components/Jumbotron/Jumbotron.scss: -------------------------------------------------------------------------------- 1 | @import "./../../assets/styles/variables.scss"; 2 | 3 | .jumbotron { 4 | width: 100%; 5 | height: 50vh; 6 | background-color: $LightestBlue; 7 | color: black; 8 | padding: 1rem; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/components/Footer/Footer.scss: -------------------------------------------------------------------------------- 1 | @import "./../../assets/styles/variables.scss"; 2 | 3 | .ant-layout-footer { 4 | text-align: center; 5 | background-color: $DarkBlue !important; 6 | color: white !important; 7 | padding: 10px !important; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/redux/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { userAuthReducer } from "./userAuthReducer"; 3 | 4 | const rootReducer = combineReducers({ 5 | user: userAuthReducer, 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /client/src/components/Footer/FooterComponent.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Footer.scss"; 3 | 4 | export default function FooterComponent() { 5 | return ( 6 |
© MERN Blog 2021 by Maryam Aljanabi
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /client/src/api/authToken.js: -------------------------------------------------------------------------------- 1 | export function getToken() { 2 | return { 3 | headers: { 4 | crossdomain: true, 5 | authorization: 6 | "Bearer " + 7 | (sessionStorage.getItem("user") && 8 | JSON.parse(sessionStorage.getItem("user")).token), 9 | }, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/models/comment.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | var commentSchema = new mongoose.Schema({ 4 | content: String, 5 | createdAt: { type: Date, default: Date.now, required: true }, 6 | createdBy: { type: mongoose.Schema.ObjectId, ref: "User", required: true }, 7 | }); 8 | 9 | export default mongoose.model("Comment", commentSchema); 10 | -------------------------------------------------------------------------------- /src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | let middlewareObject = {}; 2 | 3 | //a middleware to check if a user is logged in or not 4 | middlewareObject.isNotLoggedIn = (req, res, next) => { 5 | if (!req.isAuthenticated()) { 6 | return next(); 7 | } 8 | res.redirect("/"); 9 | }; 10 | 11 | middlewareObject.isLoggedIn = (req, res, next) => { 12 | if (req.isAuthenticated()) { 13 | return next(); 14 | } 15 | res.redirect("/signin"); 16 | }; 17 | 18 | export default middlewareObject; 19 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "MERN Blog App", 3 | "name": "MERN Blog App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "128x128", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /client/src/pages/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Player } from "@lottiefiles/react-lottie-player"; 3 | 4 | export default function NotFound() { 5 | return ( 6 |
7 |

Page not found

8 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | */node_modules 5 | node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | */build/ 10 | ./client/build/ 11 | 12 | # testing 13 | /coverage 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | */.env 30 | .env 31 | 32 | -------------------------------------------------------------------------------- /client/src/components/PostsGrid/PostsGrid.scss: -------------------------------------------------------------------------------- 1 | .posts-div { 2 | margin: 2rem; 3 | text-align: center; 4 | } 5 | .posts-container { 6 | padding: 2rem; 7 | text-align: center; 8 | margin-left: auto; 9 | margin-right: auto; 10 | } 11 | 12 | .image-container { 13 | position: relative; 14 | overflow: hidden; 15 | margin: auto; 16 | border-radius: 10px 10px 0 0; 17 | height: 300px; 18 | .card-image { 19 | object-fit: cover; 20 | max-width: 100%; 21 | min-height: 100%; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./assets/styles/index.scss"; 4 | import App from "./App"; 5 | import "antd/dist/antd.css"; 6 | import { BrowserRouter } from "react-router-dom"; 7 | import { Provider } from "react-redux"; 8 | import storeConfig from "./redux/store/store"; 9 | 10 | const store = storeConfig(); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById("root") 19 | ); 20 | -------------------------------------------------------------------------------- /client/src/pages/Home/Home.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | .home-jumbotron { 3 | height: 100%; 4 | display: flex; 5 | justify-content: space-around; 6 | align-items: center; 7 | .left-section, 8 | .centered-section { 9 | h3 { 10 | font-size: 2rem; 11 | margin: 0; 12 | } 13 | h2 { 14 | font-size: 2.5rem; 15 | font-weight: bold; 16 | } 17 | } 18 | .centered-section { 19 | text-align: center; 20 | } 21 | .right-section { 22 | width: 37%; 23 | float: right; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/redux/actions/actionCreator.js: -------------------------------------------------------------------------------- 1 | import { userAuth } from "./index"; 2 | 3 | export const userAuthActions = { 4 | login: (payload) => ({ 5 | type: userAuth.LOGIN, 6 | payload: payload, 7 | }), 8 | loginSuccess: (payload) => ({ 9 | type: userAuth.LOGIN_SUCCESS, 10 | payload: payload, 11 | }), 12 | updateUser: (payload) => ({ 13 | type: userAuth.UPDATE_USER, 14 | payload: payload, 15 | }), 16 | loginFailed: (err) => ({ 17 | type: userAuth.LOGIN_FAILED, 18 | payload: err, 19 | }), 20 | logout: () => ({ 21 | type: userAuth.LOGOUT, 22 | }), 23 | }; 24 | -------------------------------------------------------------------------------- /src/db/dbConfig.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const connectDB = async () => { 4 | try { 5 | const uri = process.env.MONGO_URI; 6 | await mongoose 7 | .connect(uri, { 8 | useNewUrlParser: true, 9 | useCreateIndex: true, 10 | useUnifiedTopology: true, 11 | dbName: "mern-blog-db", 12 | }) 13 | .catch((error) => console.log(error)); 14 | const connection = mongoose.connection; 15 | console.log("MONGODB CONNECTED SUCCESSFULLY!"); 16 | } catch (error) { 17 | console.log(error); 18 | return error; 19 | } 20 | }; 21 | 22 | export default connectDB; 23 | -------------------------------------------------------------------------------- /client/src/NotLoggedInRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect, Route } from "react-router-dom"; 3 | import { useSelector } from "react-redux"; 4 | 5 | const NotLoggedInRoute = ({ component: Component, ...rest }) => { 6 | const userState = useSelector((st) => st.user); 7 | 8 | return ( 9 | 12 | !userState.isLoggedIn ? ( 13 | 14 | ) : ( 15 | 16 | ) 17 | } 18 | /> 19 | ); 20 | }; 21 | 22 | export default NotLoggedInRoute; 23 | -------------------------------------------------------------------------------- /client/src/LoggedInRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect, Route } from "react-router-dom"; 3 | import { useSelector } from "react-redux"; 4 | 5 | const LoggedInRoute = ({ component: Component, ...rest }) => { 6 | const userState = useSelector((st) => st.user); 7 | 8 | return ( 9 | 12 | userState.isLoggedIn ? ( 13 | 14 | ) : ( 15 | 18 | ) 19 | } 20 | /> 21 | ); 22 | }; 23 | 24 | export default LoggedInRoute; 25 | -------------------------------------------------------------------------------- /src/models/post.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | const Schema = mongoose.Schema; 3 | 4 | const postSchema = Schema({ 5 | title: { 6 | type: String, 7 | required: [true, "Please enter the post's title"], 8 | }, 9 | content: { 10 | type: String, 11 | required: [true, "Please enter the post's content"], 12 | }, 13 | imagePath: { 14 | type: String, 15 | }, 16 | createdAt: { type: Date, default: Date.now, required: true }, 17 | createdBy: { type: mongoose.Schema.ObjectId, ref: "User", required: true }, 18 | comments: [{ type: mongoose.Schema.Types.ObjectId, ref: "Comment" }], 19 | }); 20 | 21 | export default mongoose.model("Post", postSchema); 22 | -------------------------------------------------------------------------------- /client/src/components/Layout/Layout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import HorizontalNavbar from "./../Navbars/HorizontalNavbar"; 3 | import { Layout as AntdLayout } from "antd"; 4 | import FooterComponent from "./../Footer/FooterComponent"; 5 | 6 | const { Header, Footer, Sider, Content } = AntdLayout; 7 | 8 | export default function Layout({ children }) { 9 | return ( 10 | 11 |
12 | 13 |
14 |
15 |
{children}
16 |
17 | 18 |
19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/middlewares/passport.js: -------------------------------------------------------------------------------- 1 | import User from "./../models/user"; 2 | import PassportJwt from "passport-jwt"; 3 | const JwtStrategy = PassportJwt.Strategy; 4 | const ExtractJwt = PassportJwt.ExtractJwt; 5 | 6 | const opts = {}; 7 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken(); 8 | opts.secretOrKey = process.env.SECRET_KEY; 9 | 10 | export const passport = () => { 11 | passport.use( 12 | new JwtStrategy(opts, async (jwt_payload, done) => { 13 | try { 14 | const user = await User.findById(jwt_payload._id); 15 | if (user) return done(null, user); 16 | else return done(null, false); 17 | } catch (error) { 18 | console.log(error); 19 | } 20 | }) 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /client/src/pages/Posts/Post.scss: -------------------------------------------------------------------------------- 1 | @import "./../../assets/styles/variables.scss"; 2 | 3 | .view-post { 4 | background-color: white; 5 | color: black; 6 | margin: 2rem auto; 7 | padding: 2rem; 8 | border-radius: 10px !important; 9 | width: 70%; 10 | .post-header { 11 | h1 { 12 | font-size: 3rem; 13 | line-height: 3.5rem; 14 | } 15 | p { 16 | color: $DarkGray; 17 | } 18 | } 19 | .post-content { 20 | white-space: pre-line; 21 | margin: 2rem auto; 22 | } 23 | 24 | @media only screen and (max-width: 1050px) { 25 | width: 80%; 26 | } 27 | @media only screen and (max-width: 1000px) { 28 | width: 90%; 29 | } 30 | @media only screen and (max-width: 820px) { 31 | width: 100%; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/pages/Users/UserProfile.scss: -------------------------------------------------------------------------------- 1 | @import "./../../assets/styles/variables.scss"; 2 | 3 | .user-profile { 4 | background-color: white; 5 | color: black; 6 | margin: 2rem auto; 7 | padding: 2rem; 8 | border-radius: 10px !important; 9 | width: 60%; 10 | .user-image { 11 | margin: auto; 12 | .image { 13 | border-radius: 50%; 14 | object-fit: cover; 15 | width: 300px; 16 | height: 300px; 17 | } 18 | .ant-image-mask:hover { 19 | border-radius: 50%; 20 | } 21 | } 22 | .user-info { 23 | .form { 24 | // background-color: white; 25 | // color: black; 26 | // margin: 2rem auto; 27 | // padding: 2rem; 28 | // border-radius: 10px !important; 29 | // width: 60%; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/src/redux/store/sessionStorage.js: -------------------------------------------------------------------------------- 1 | export const loadState = () => { 2 | try { 3 | const serializedState = sessionStorage.getItem("user"); 4 | 5 | if (serializedState === null) { 6 | return { 7 | isLoggedIn: false, 8 | errors: "", 9 | token: null, 10 | }; 11 | } 12 | 13 | return JSON.parse(serializedState); 14 | } catch (error) { 15 | console.log("Load state error...", error.response ?? error); 16 | return undefined; 17 | } 18 | }; 19 | 20 | export const saveState = (state) => { 21 | try { 22 | sessionStorage.clear(); 23 | const serializedState = JSON.stringify(state); 24 | sessionStorage.setItem("user", serializedState); 25 | } catch (error) { 26 | console.log("Save state error...", error.response ?? error); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/services/commentServices.js: -------------------------------------------------------------------------------- 1 | import Post from "./../models/post"; 2 | import User from "./../models/user"; 3 | import Comment from "./../models/comment"; 4 | 5 | export const addComment = async (comment) => { 6 | //find required post 7 | const foundPost = await Post.findById(comment.postId); 8 | //create the comment 9 | const createdComment = await Comment.create(comment); 10 | //add the created comment to the found post 11 | await foundPost.comments.push(createdComment); 12 | const updatedPost = await foundPost.save(); 13 | return updatedPost; 14 | }; 15 | 16 | export const updateComment = async (comment) => { 17 | const updatedComment = await Comment.findByIdAndUpdate(comment._id, comment); 18 | return updatedComment; 19 | }; 20 | 21 | export const deleteComment = async (id) => { 22 | const deletedComment = await Comment.findByIdAndRemove({ _id: id }); 23 | return deletedComment; 24 | }; 25 | -------------------------------------------------------------------------------- /client/src/redux/sagas/userAuthSaga.js: -------------------------------------------------------------------------------- 1 | import { put, takeEvery } from "redux-saga/effects"; 2 | import { userAuth as actions } from "../actions"; 3 | import { userAuthActions } from "./../actions/actionCreator"; 4 | import { authAPI } from "./../../api/api"; 5 | import { saveState } from "./../store/sessionStorage"; 6 | 7 | function* createLogin(action) { 8 | try { 9 | const response = yield authAPI.login(action.payload); 10 | if (response.data.token) { 11 | const token = response.data.token; 12 | saveState({ 13 | isLoggedIn: true, 14 | error: "", 15 | token: token, 16 | }); 17 | yield put( 18 | userAuthActions.loginSuccess({ 19 | token, 20 | }) 21 | ); 22 | } 23 | } catch (err) { 24 | yield put(userAuthActions.loginFailed("Login data is incorrect")); 25 | } 26 | } 27 | 28 | export function* createLoginStart() { 29 | yield takeEvery(actions.LOGIN, createLogin); 30 | } 31 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import bcrypt from "bcrypt-nodejs"; 3 | const Schema = mongoose.Schema; 4 | 5 | const userSchema = Schema({ 6 | userName: { 7 | type: String, 8 | require: true, 9 | }, 10 | email: { 11 | type: String, 12 | require: true, 13 | }, 14 | password: { 15 | type: String, 16 | require: true, 17 | }, 18 | summary: { 19 | type: String, 20 | }, 21 | imagePath: { 22 | type: String, 23 | }, 24 | }); 25 | 26 | // encrypt the password before storing 27 | userSchema.methods.encryptPassword = (password) => { 28 | return bcrypt.hashSync(password, bcrypt.genSaltSync(5), null); 29 | }; 30 | 31 | userSchema.methods.validPassword = function (candidatePassword) { 32 | if (this.password != null) { 33 | return bcrypt.compareSync(candidatePassword, this.password); 34 | } else { 35 | return false; 36 | } 37 | }; 38 | 39 | export default mongoose.model("User", userSchema); 40 | -------------------------------------------------------------------------------- /client/src/redux/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import { createLogger } from "redux-logger"; 3 | import createSagaMiddleware from "redux-saga"; 4 | import rootReducer from "../reducers/rootReducer"; 5 | import rootSaga from "../sagas/rootSaga"; 6 | 7 | // const persistedState = loadState(); 8 | 9 | const onSagaUncaughtErrors = (err, errInfo) => { 10 | console.log({ onSagaError: "saga error", err, errInfo }); 11 | }; 12 | 13 | const sagaMiddleware = createSagaMiddleware({ onError: onSagaUncaughtErrors }); 14 | const loggerMiddleware = createLogger(); 15 | 16 | const middleWares = [loggerMiddleware, sagaMiddleware]; 17 | 18 | let reduxStore; 19 | 20 | const storeConfig = () => { 21 | const store = createStore(rootReducer, applyMiddleware(...middleWares)); 22 | 23 | // start redux sagas 24 | sagaMiddleware.run(rootSaga); 25 | reduxStore = store; 26 | 27 | return store; 28 | }; 29 | 30 | export { reduxStore }; 31 | export default storeConfig; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "dev": "nodemon app.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt-nodejs": "0.0.3", 15 | "body-parser": "^1.19.0", 16 | "cors": "^2.8.5", 17 | "dotenv": "^8.2.0", 18 | "esm": "^3.2.25", 19 | "express": "^4.17.1", 20 | "express-validator": "^6.10.0", 21 | "jsonwebtoken": "^8.5.1", 22 | "lodash.isempty": "^4.4.0", 23 | "mongoose": "^5.12.10", 24 | "morgan": "^1.10.0", 25 | "passport": "^0.4.1", 26 | "passport-jwt": "^4.0.0", 27 | "passport-local": "^1.0.0", 28 | "rotating-file-stream": "^2.1.4", 29 | "validator": "^13.5.2" 30 | }, 31 | "devDependencies": { 32 | "nodemon": "^2.0.7" 33 | }, 34 | "engines": { 35 | "node": "14.16.0", 36 | "npm": "6.14.11" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/pages/Signup/Signup.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Player } from "@lottiefiles/react-lottie-player"; 3 | import SignupForm from "./SignupForm"; 4 | 5 | export default function Signup() { 6 | const [width, setWidth] = useState(window.innerWidth); 7 | useEffect(() => { 8 | function handleResize() { 9 | setWidth(window.innerWidth); 10 | } 11 | window.addEventListener("resize", handleResize); 12 | return () => window.removeEventListener("resize", handleResize); 13 | }, [width]); 14 | 15 | return ( 16 |
17 | {width <= 650 ? ( 18 |
19 |

Sign up for free

20 | 21 |
22 | ) : ( 23 | <> 24 |
25 |

Sign up for free

26 | 27 |
28 | 29 | )} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/routes/commentRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | addComment, 4 | deleteComment, 5 | updateComment, 6 | } from "../services/commentServices"; 7 | 8 | const router = express.Router(); 9 | 10 | router.post("/", async (req, res) => { 11 | try { 12 | const result = await addComment(req.body.comment); 13 | res.send(result); 14 | } catch (error) { 15 | console.log(error); 16 | return res.status(500).json({ error: error.toString() }); 17 | } 18 | }); 19 | 20 | router.put("/", async (req, res) => { 21 | try { 22 | const result = await updateComment(req.body.comment); 23 | res.send(result); 24 | } catch (error) { 25 | console.log(error); 26 | return res.status(500).json({ error: error.toString() }); 27 | } 28 | }); 29 | 30 | router.delete("/:id", async (req, res) => { 31 | try { 32 | const result = await deleteComment(req.params.id); 33 | res.send(result); 34 | } catch (error) { 35 | console.log(error); 36 | return res.status(500).json({ error: error.toString() }); 37 | } 38 | }); 39 | 40 | export default router; 41 | -------------------------------------------------------------------------------- /src/services/postServices.js: -------------------------------------------------------------------------------- 1 | import Post from "./../models/post"; 2 | import User from "./../models/user"; 3 | 4 | export const getAllPosts = async () => { 5 | return await Post.find({}); 6 | }; 7 | 8 | export const getOnePost = async (id) => { 9 | //find post 10 | const post = await Post.findById(id).populate({ 11 | path: "comments", 12 | populate: { 13 | path: "createdBy", 14 | }, 15 | }); 16 | 17 | let res; 18 | if (post) { 19 | //get the name of the creator 20 | const user = await User.findById(post.createdBy); 21 | res = { 22 | ...post.toObject(), 23 | createdByName: user.userName, 24 | userImageUrl: user.imagePath, 25 | }; 26 | } 27 | return res; 28 | }; 29 | 30 | export const getPostByUserID = async (userId) => { 31 | return await Post.find({ createdBy: userId }); 32 | }; 33 | 34 | export const addPost = async (post) => { 35 | //add post 36 | return await Post.create(post); 37 | }; 38 | 39 | export const updatePost = async (post) => { 40 | return await Post.findByIdAndUpdate(post._id, post); 41 | }; 42 | 43 | export const deletePost = async (id) => { 44 | return await Post.findOneAndRemove({ _id: id }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/services/userServices.js: -------------------------------------------------------------------------------- 1 | import User from "./../models/user"; 2 | 3 | export const getAllUsers = async () => { 4 | return await User.find({}); 5 | }; 6 | 7 | export const getOneUser = async (id) => { 8 | let user = await User.findById(id); 9 | let res = user.toObject(); 10 | delete res.password; 11 | return res; 12 | }; 13 | 14 | export const addUser = async (user) => { 15 | return await User.create(user); 16 | }; 17 | 18 | export const updateUser = async (user) => { 19 | let res; 20 | //check if the user is updating the profile or the password 21 | if (user.password) { 22 | const foundUser = await User.findById(user._id); 23 | //check if the old password matches the one in the db 24 | if (!foundUser.validPassword(user.oldPassword)) { 25 | throw new Error("Incorrect old password"); 26 | } 27 | //encrypt the password 28 | foundUser.password = foundUser.encryptPassword(user.password); 29 | res = await User.findByIdAndUpdate(user._id, foundUser); 30 | } else { 31 | res = await User.findByIdAndUpdate(user._id, user); 32 | } 33 | return res; 34 | }; 35 | 36 | export const deleteUser = async (id) => { 37 | return await User.findOneAndRemove({ _id: id }); 38 | }; 39 | -------------------------------------------------------------------------------- /client/src/api/api.js: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | const axiosConfig = { 3 | baseURL: `${process.env.REACT_APP_API_URL || `http://:${window.location.hostname}:${process.env.REACT_APP_API_PORT || 5000}`}/api`, 4 | }; 5 | 6 | const axios = Axios.create(axiosConfig); 7 | 8 | // AUTH 9 | export const authAPI = { 10 | login: (data) => axios.post(`/auth/login`, data), 11 | signup: (data) => axios.post(`/auth/signup`, data), 12 | }; 13 | 14 | // USERS 15 | export const usersAPI = { 16 | getAll: () => axios.get(`/users`), 17 | getOne: (id) => axios.get(`/users/${id}`), 18 | add: (data) => axios.post(`/users`, data), 19 | update: (data) => axios.put(`/users`, data), 20 | delete: (id) => axios.delete(`/users/${id}`), 21 | }; 22 | 23 | // POSTS 24 | export const postsAPI = { 25 | getAll: () => axios.get(`/posts`), 26 | getPostByUserId: (id) => axios.get(`/posts/user/${id}`), 27 | getOne: (id) => axios.get(`/posts/${id}`), 28 | add: (data) => axios.post(`/posts`, data), 29 | update: (data) => axios.put(`/posts`, data), 30 | delete: (id) => axios.delete(`/posts/${id}`), 31 | }; 32 | 33 | // COMMENTS 34 | export const commentsAPI = { 35 | add: (data) => axios.post(`/comments`, data), 36 | update: (data) => axios.put(`/comments`, data), 37 | delete: (id) => axios.delete(`/comments/${id}`), 38 | }; 39 | -------------------------------------------------------------------------------- /client/src/pages/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Player } from "@lottiefiles/react-lottie-player"; 3 | import "./Login.scss"; 4 | import LoginForm from "./LoginForm"; 5 | 6 | export default function Login() { 7 | const [width, setWidth] = useState(window.innerWidth); 8 | useEffect(() => { 9 | function handleResize() { 10 | setWidth(window.innerWidth); 11 | } 12 | window.addEventListener("resize", handleResize); 13 | return () => window.removeEventListener("resize", handleResize); 14 | }, [width]); 15 | 16 | return ( 17 |
18 | {width <= 650 ? ( 19 |
20 | 21 |
22 | ) : ( 23 | <> 24 |
25 | 26 |
27 |
28 |

29 | #1 Bloggin website for everyone around the globe. Totally free and 30 | easy to use. 31 |

32 | 37 |
38 | 39 | )} 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import { Switch, Route } from "react-router-dom"; 2 | import Layout from "./components/Layout/Layout"; 3 | import Home from "./pages/Home/Home"; 4 | import Login from "./pages/Login/Login"; 5 | import Signup from "./pages/Signup/Signup"; 6 | import NewPost from "./pages/Posts/NewPost"; 7 | import UserPosts from "./pages/Posts/UserPosts"; 8 | import EditPost from "./pages/Posts/EditPost"; 9 | import UserProfile from "./pages/Users/UserProfile"; 10 | import Post from "./pages/Posts/Post"; 11 | import LoggedInRoute from "./LoggedInRoute"; 12 | import NotLoggedInRoute from "./NotLoggedInRoute"; 13 | import NotFound from "./pages/NotFound/NotFound"; 14 | 15 | function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import dotenv from "dotenv"; 3 | dotenv.config({ path: path.join(__dirname, "../.env") }); 4 | import postRoutes from "./routes/postRoutes"; 5 | import userRoutes from "./routes/userRoutes"; 6 | import authRoutes from "./routes/authRoutes"; 7 | import commentRoutes from "./routes/commentRoutes"; 8 | import cors from "cors"; 9 | import path from "path"; 10 | import bodyParser from "body-parser"; 11 | import connectDB from "./db/dbConfig"; 12 | import passport from "passport"; 13 | import { passport as passportMiddleware } from "./middlewares/passport"; 14 | 15 | const app = express(); 16 | 17 | const port = process.env.API_PORT || 5000; 18 | 19 | function setupServer() { 20 | connectDB(); 21 | middlewares(); 22 | app.use("/api/posts", postRoutes); 23 | app.use("/api/users", userRoutes); 24 | app.use("/api/comments", commentRoutes); 25 | app.use("/api/auth", authRoutes); 26 | app.use(express.static(path.join(__dirname, "./../client/build"))); 27 | app.get("*", (req, res) => { 28 | res.sendFile(path.join(__dirname, "./../client/build", "index.html")); 29 | }); 30 | } 31 | 32 | function middlewares() { 33 | app.use(cors()); 34 | app.use(bodyParser.json()); 35 | app.use(bodyParser.urlencoded({ extended: true })); 36 | // Passport middleware 37 | app.use(passport.initialize()); 38 | passportMiddleware; 39 | } 40 | 41 | app.listen(port, () => { 42 | console.log("Server listening on port " + port); 43 | }); 44 | 45 | setupServer(); 46 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@lottiefiles/react-lottie-player": "^3.0.1", 8 | "@testing-library/jest-dom": "^5.11.9", 9 | "@testing-library/react": "^11.2.5", 10 | "@testing-library/user-event": "^12.7.0", 11 | "antd": "^4.12.3", 12 | "axios": "^0.21.1", 13 | "final-form": "^4.20.1", 14 | "jsonwebtoken": "^8.5.1", 15 | "lodash.clonedeep": "^4.5.0", 16 | "lodash.isempty": "^4.4.0", 17 | "moment": "^2.30.1", 18 | "node-sass": "^5.0.0", 19 | "react": "^17.0.1", 20 | "react-dom": "^17.0.1", 21 | "react-final-form": "^6.5.2", 22 | "react-icons": "^4.2.0", 23 | "react-redux": "^7.2.2", 24 | "react-router-dom": "^5.2.0", 25 | "react-scripts": "4.0.2", 26 | "redux": "^4.0.5", 27 | "redux-logger": "^3.0.6", 28 | "redux-saga": "^1.1.3", 29 | "web-vitals": "^1.1.0" 30 | }, 31 | "scripts": { 32 | "start": "set PORT=3006 && react-scripts start", 33 | "build": "react-scripts build", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "react-app", 40 | "react-app/jest" 41 | ] 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/seedDB/seedUsers.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | require("dotenv").config({ path: path.join(__dirname, "../../.env") }); 3 | import User from "./../models/user"; 4 | import mongoose from "mongoose"; 5 | import connectDB from "./../db/dbConfig"; 6 | 7 | connectDB(); 8 | (async function seedDB() { 9 | async function seedUser(userName, email, password, summary, imagePath) { 10 | try { 11 | const user = await new User({ 12 | userName, 13 | email, 14 | password, 15 | summary, 16 | imagePath: imagePath, 17 | }); 18 | await user.save(function (err) { 19 | if (err) { 20 | console.log(err); 21 | return; 22 | } 23 | }); 24 | console.log("User added succefully!"); 25 | } catch (error) { 26 | console.log(error); 27 | return error; 28 | } 29 | } 30 | 31 | async function closeDB() { 32 | console.log("CLOSING CONNECTION"); 33 | await mongoose.disconnect(); 34 | } 35 | 36 | await seedUser( 37 | "newName", 38 | "test@test.com", 39 | "000", 40 | "some test user!", 41 | "https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1027&q=80" 42 | ); 43 | await seedUser( 44 | "testName", 45 | "test@test.com", 46 | "000", 47 | "some test user!", 48 | "https://images.unsplash.com/photo-1511694009171-3cdddf4484ff?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80" 49 | ); 50 | await closeDB(); 51 | })(); 52 | -------------------------------------------------------------------------------- /src/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | getAllUsers, 4 | getOneUser, 5 | addUser, 6 | updateUser, 7 | deleteUser, 8 | } from "../services/userServices"; 9 | 10 | const router = express.Router(); 11 | 12 | router.get("/", async (req, res) => { 13 | try { 14 | const users = await getAllUsers(); 15 | res.send(users); 16 | } catch (error) { 17 | console.log(error); 18 | return res.status(500).json({ error: error.toString() }); 19 | } 20 | }); 21 | 22 | router.get("/:id", async (req, res) => { 23 | try { 24 | const user = await getOneUser(req.params.id); 25 | res.send(user); 26 | } catch (error) { 27 | console.log(error); 28 | return res.status(500).json({ error: error.toString() }); 29 | } 30 | }); 31 | 32 | router.post("/", async (req, res) => { 33 | try { 34 | const result = await addUser(req.body.user); 35 | res.send(result); 36 | } catch (error) { 37 | console.log(error); 38 | return res.status(500).json({ error: error.toString() }); 39 | } 40 | }); 41 | 42 | router.put("/", async (req, res) => { 43 | try { 44 | const result = await updateUser(req.body.user); 45 | res.send(result); 46 | } catch (error) { 47 | console.log(error); 48 | return res.status(500).json({ error: error.toString() }); 49 | } 50 | }); 51 | 52 | router.delete("/:id", async (req, res) => { 53 | try { 54 | const result = await deleteUser(req.params.id); 55 | res.send(result); 56 | } catch (error) { 57 | console.log(error); 58 | return res.status(500).json({ error: error.toString() }); 59 | } 60 | }); 61 | 62 | export default router; 63 | -------------------------------------------------------------------------------- /src/seedDB/seedPosts.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | require("dotenv").config({ path: path.join(__dirname, "../../.env") }); 3 | import Post from "./../models/post"; 4 | import User from "./../models/user"; 5 | import mongoose from "mongoose"; 6 | import connectDB from "./../db/dbConfig"; 7 | 8 | connectDB(); 9 | 10 | (async function seedDB() { 11 | async function seedPost(title, content, imagePath, userName) { 12 | try { 13 | const user = await User.findOne({ userName: userName }); 14 | const post = await new Post({ 15 | title: title, 16 | content: content, 17 | imagePath: imagePath, 18 | createdBy: user._id, 19 | }); 20 | await post.save(); 21 | } catch (error) { 22 | console.log(error); 23 | return error; 24 | } 25 | } 26 | 27 | async function closeDB() { 28 | console.log("CLOSING CONNECTION"); 29 | await mongoose.disconnect(); 30 | } 31 | 32 | await seedPost( 33 | "A test post", 34 | "Just some random seed post", 35 | "https://cdn.pixabay.com/photo/2016/11/02/17/38/japan-1792369_1280.jpg", 36 | "newName" 37 | ); 38 | await seedPost( 39 | "Lorem ipsum dolor sit amet", 40 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse lacinia magna sit amet eros finibus tempus a at nisi. Maecenas sagittis dapibus turpis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas vel diam eget eros interdum pellentesque eget in neque. Nulla ac porttitor ligula. Duis et mollis metus. Vestibulum congue, sapien vel convallis consectetur, ex diam porta nisl, non molestie ipsum massa eget ante. Duis ut nibh mi.", 41 | "https://images.unsplash.com/photo-1500989145603-8e7ef71d639e?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1055&q=80", 42 | "testName" 43 | ); 44 | 45 | await closeDB(); 46 | })(); 47 | -------------------------------------------------------------------------------- /src/routes/postRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | getAllPosts, 4 | getOnePost, 5 | addPost, 6 | updatePost, 7 | deletePost, 8 | getPostByUserID, 9 | } from "../services/postServices"; 10 | 11 | const router = express.Router(); 12 | 13 | router.get("/", async (req, res) => { 14 | try { 15 | const posts = await getAllPosts(); 16 | res.send(posts); 17 | } catch (error) { 18 | console.log(error); 19 | return res.status(500).json({ error: error.toString() }); 20 | } 21 | }); 22 | 23 | router.get("/:id", async (req, res) => { 24 | try { 25 | const post = await getOnePost(req.params.id); 26 | res.send(post); 27 | } catch (error) { 28 | console.log(error); 29 | return res.status(500).json({ error: error.toString() }); 30 | } 31 | }); 32 | 33 | router.get("/user/:id", async (req, res) => { 34 | try { 35 | const result = await getPostByUserID(req.params.id); 36 | res.send(result); 37 | } catch (error) { 38 | console.log(error); 39 | return res.status(500).json({ error: error.toString() }); 40 | } 41 | }); 42 | 43 | router.post("/", async (req, res) => { 44 | try { 45 | const result = await addPost(req.body.post); 46 | res.send(result); 47 | } catch (error) { 48 | console.log(error); 49 | return res.status(500).json({ error: error.toString() }); 50 | } 51 | }); 52 | 53 | router.put("/", async (req, res) => { 54 | try { 55 | const result = await updatePost(req.body.post); 56 | res.send(result); 57 | } catch (error) { 58 | console.log(error); 59 | return res.status(500).json({ error: error.toString() }); 60 | } 61 | }); 62 | 63 | router.delete("/:id", async (req, res) => { 64 | try { 65 | const result = await deletePost(req.params.id); 66 | res.send(result); 67 | } catch (error) { 68 | console.log(error); 69 | return res.status(500).json({ error: error.toString() }); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 19 | 20 | 29 | MERN Blog 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/middlewares/validator.js: -------------------------------------------------------------------------------- 1 | import Validator from "validator"; 2 | import isEmpty from "lodash.isempty"; 3 | 4 | export const validateSignup = (data) => { 5 | let errors = {}; 6 | // Convert empty fields to an empty string so we can use validator functions 7 | data.name = data.userName ? data.userName : ""; 8 | data.email = data.email ? data.email : ""; 9 | data.password = data.password ? data.password : ""; 10 | data.password2 = data.confirmPassword ? data.confirmPassword : ""; 11 | 12 | if (Validator.isEmpty(data.userName)) { 13 | errors.userName = "userName field is required"; 14 | } 15 | 16 | if (Validator.isEmpty(data.email)) { 17 | errors.email = "Email field is required"; 18 | } else if (!Validator.isEmail(data.email)) { 19 | errors.email = "Email is invalid"; 20 | } 21 | 22 | if (Validator.isEmpty(data.password)) { 23 | errors.password = "Password field is required"; 24 | } 25 | if (Validator.isEmpty(data.confirmPassword)) { 26 | errors.confirmPassword = "Confirm password field is required"; 27 | } 28 | if (!Validator.isLength(data.password, { min: 3, max: 30 })) { 29 | errors.password = "Password must be at least 3 characters long"; 30 | } 31 | if (!Validator.equals(data.password, data.confirmPassword)) { 32 | errors.confirmPassword = "Passwords must match"; 33 | } 34 | return { 35 | errors, 36 | isValid: isEmpty(errors), 37 | }; 38 | }; 39 | 40 | export const validateSignin = (data) => { 41 | let errors = {}; 42 | // Convert empty fields to an empty string so we can use validator functions 43 | data.email = data.email ? data.email : ""; 44 | data.password = data.password ? data.password : ""; 45 | 46 | if (Validator.isEmpty(data.email)) { 47 | errors.email = "Email field is required"; 48 | } else if (!Validator.isEmail(data.email)) { 49 | errors.email = "Email is invalid"; 50 | } 51 | 52 | if (Validator.isEmpty(data.password)) { 53 | errors.password = "Password field is required"; 54 | } 55 | return { 56 | errors, 57 | isValid: isEmpty(errors), 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /client/src/pages/Comments/Comments.scss: -------------------------------------------------------------------------------- 1 | .comment-form { 2 | margin: 1rem auto 3rem auto; 3 | div.ant-row.ant-form-item { 4 | margin-bottom: 0.5rem !important; 5 | } 6 | 7 | .comments-btns-container { 8 | display: flex; 9 | gap: 0.5rem; 10 | float: right !important; 11 | } 12 | 13 | .ant-alert-error { 14 | margin: 0.5rem auto; 15 | border-radius: 10px !important; 16 | } 17 | } 18 | 19 | .full-width-comment { 20 | width: 100%; 21 | } 22 | 23 | .comment-container { 24 | background-color: #f5f5f5; 25 | border-radius: 10px; 26 | display: flex; 27 | padding: 0.5rem 1rem; 28 | flex-direction: row !important; 29 | align-items: end; 30 | margin: 0.5rem auto !important; 31 | gap: 1rem; 32 | transition: all 200ms ease-in-out; 33 | 34 | .comment-editing { 35 | .ant-form-item { 36 | margin-bottom: 0.3rem !important; 37 | } 38 | 39 | .comments-btns-container { 40 | display: flex; 41 | gap: 0.5rem; 42 | } 43 | } 44 | 45 | .cols { 46 | width: 100%; 47 | display: flex; 48 | flex-direction: row !important; 49 | justify-content: space-between; 50 | margin: 0; 51 | } 52 | .comment-text { 53 | flex: 7; 54 | } 55 | .comment-text-mobile { 56 | text-align: justify; 57 | } 58 | .comment-date { 59 | flex: 1; 60 | color: gray; 61 | text-align: center; 62 | } 63 | .comment-date-mobile { 64 | color: gray; 65 | } 66 | .icons-cols { 67 | width: 100%; 68 | display: flex; 69 | flex-direction: row !important; 70 | justify-content: space-evenly; 71 | margin: 0; 72 | height: 25px; 73 | align-items: center; 74 | .anticon { 75 | &:hover { 76 | cursor: pointer; 77 | } 78 | } 79 | } 80 | .icons-cols-mobile { 81 | width: 100%; 82 | display: flex; 83 | flex-direction: row !important; 84 | justify-content: end; 85 | gap: 1rem; 86 | margin: 0; 87 | height: 25px; 88 | align-items: center; 89 | .anticon { 90 | &:hover { 91 | cursor: pointer; 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /client/src/redux/reducers/userAuthReducer.js: -------------------------------------------------------------------------------- 1 | import { userAuth as actions } from "./../actions/index"; 2 | import jwt from "jsonwebtoken"; 3 | import { saveState, loadState } from "./../store/sessionStorage"; 4 | 5 | let initialState; 6 | const emptyState = { 7 | isLoggedIn: false, 8 | error: "", 9 | token: null, 10 | user: { 11 | id: "", 12 | userName: "", 13 | imagePath: "", 14 | }, 15 | }; 16 | if (loadState() && loadState().token) { 17 | const decodedToken = jwt.decode(loadState().token); 18 | initialState = { 19 | ...loadState(), 20 | user: { 21 | id: decodedToken.id, 22 | userName: decodedToken.userName, 23 | imagePath: decodedToken.imagePath, 24 | }, 25 | }; 26 | } else { 27 | initialState = { ...emptyState }; 28 | } 29 | 30 | export const userAuthReducer = (state = initialState, action) => { 31 | switch (action.type) { 32 | case actions.LOGIN_SUCCESS: 33 | let newState; 34 | if (loadState() && loadState().token) { 35 | const decodedToken = jwt.decode(loadState().token); 36 | newState = { 37 | ...loadState(), 38 | user: { 39 | id: decodedToken.id, 40 | userName: decodedToken.userName, 41 | imagePath: decodedToken.imagePath, 42 | }, 43 | }; 44 | } 45 | return newState; 46 | 47 | case actions.UPDATE_USER: 48 | let updatedState = { 49 | ...state, 50 | user: { 51 | ...state.user, 52 | imagePath: action.payload.imagePath, 53 | userName: action.payload.userName, 54 | }, 55 | }; 56 | saveState(updatedState); 57 | return updatedState; 58 | 59 | case actions.LOGOUT: 60 | saveState(emptyState); 61 | return emptyState; 62 | 63 | case actions.LOGIN_FAILED: 64 | if (action.payload) { 65 | let newState = { 66 | token: null, 67 | isLoggedIn: false, 68 | error: action.payload, 69 | }; 70 | saveState(newState); 71 | return newState; 72 | } 73 | 74 | default: 75 | return state; 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import middleware from "./../middlewares/index"; 3 | import { validateSignup, validateSignin } from "./../middlewares/validator"; 4 | import User from "./../models/user"; 5 | import Strategy from "passport-local"; 6 | const LocalStrategy = Strategy.Strategy; 7 | import jwt from "jsonwebtoken"; 8 | 9 | const router = express.Router(); 10 | 11 | router.post("/signup", [middleware.isNotLoggedIn], async (req, res) => { 12 | const reqUser = req.body.user; 13 | 14 | // Validation 15 | const { errors, isValid } = validateSignup(reqUser); 16 | if (!isValid) { 17 | return res.status(400).json(errors); 18 | } 19 | 20 | //Create a new user in the db 21 | try { 22 | const user = await User.findOne({ email: reqUser.email }); 23 | if (user) { 24 | return res.status(400).json({ email: "Email already exists" }); 25 | } 26 | const newUser = await new User({ ...reqUser }); 27 | newUser.password = newUser.encryptPassword(reqUser.password); 28 | await newUser.save(); 29 | return res.json(user); 30 | } catch (error) { 31 | console.log(error); 32 | return res.status(500).json({ error: error.toString() }); 33 | } 34 | }); 35 | 36 | router.post("/login", async (req, res) => { 37 | const reqUser = req.body.user; 38 | 39 | // Validation 40 | const { errors, isValid } = validateSignin(reqUser); 41 | if (!isValid) { 42 | return res.status(400).json(errors); 43 | } 44 | const email = reqUser.email; 45 | const password = reqUser.password; 46 | 47 | // Find user 48 | try { 49 | const user = await User.findOne({ email }); 50 | if (!user) return res.status(404).json({ error: "Email not found" }); 51 | if (!user.validPassword(password)) { 52 | return res.status(400).json({ error: "Incorrect password" }); 53 | } 54 | 55 | // Create and send the JWT token 56 | const payload = { 57 | id: user._id, 58 | userName: user.userName, 59 | imagePath: user.imagePath, 60 | }; 61 | jwt.sign( 62 | payload, 63 | process.env.SECRET_KEY, 64 | { 65 | expiresIn: 31556926, // 1 year in seconds 66 | }, 67 | (err, token) => { 68 | res.json({ 69 | token: token, 70 | }); 71 | } 72 | ); 73 | } catch (error) { 74 | console.log(error); 75 | return res.status(500).json({ error: error.toString() }); 76 | } 77 | }); 78 | 79 | export default router; 80 | -------------------------------------------------------------------------------- /client/src/components/Navbars/HorizontalNavbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | LoginOutlined, 4 | UserAddOutlined, 5 | SettingOutlined, 6 | GroupOutlined, 7 | FormOutlined, 8 | } from "@ant-design/icons"; 9 | import { Menu, Avatar } from "antd"; 10 | import blogLogo from "./../../assets/images/blogging.png"; 11 | import { useSelector, useDispatch } from "react-redux"; 12 | import { userAuthActions } from "./../../redux/actions/actionCreator"; 13 | const { SubMenu } = Menu; 14 | 15 | export default function HorizontalNavbar() { 16 | const userState = useSelector((st) => st.user); 17 | const dispatch = useDispatch(); 18 | 19 | return ( 20 |
21 | 22 | 23 | 24 |   Blog App 25 | 26 | 27 | 28 | {userState.isLoggedIn ? ( 29 | <> 30 | } 33 | title={" " + userState.user.userName} 34 | className="float-right unhoverable-menu-item" 35 | > 36 | }> 37 | User Profile 38 | 39 | }> 40 | User Posts 41 | 42 | } 45 | onClick={() => dispatch(userAuthActions.logout())} 46 | > 47 | Logout 48 | 49 | 50 | } 53 | className="float-right" 54 | > 55 | New Post 56 | 57 | 58 | ) : ( 59 | <> 60 | } 63 | className="float-right" 64 | > 65 | Login 66 | 67 | } 70 | className="float-right" 71 | > 72 | Signup 73 | 74 | 75 | )} 76 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /client/src/pages/Posts/UserPosts.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { postsAPI } from "./../../api/api"; 4 | import { useLocation } from "react-router-dom"; 5 | import PostsGrid from "../../components/PostsGrid/PostsGrid"; 6 | import { Form, Input, Button, Tag, message, Image, Spin, Alert } from "antd"; 7 | 8 | export default function UserPosts() { 9 | const userState = useSelector((st) => st.user); 10 | const [width, setWidth] = useState(window.innerWidth); 11 | const [postsData, setPostsData] = useState([]); 12 | const location = useLocation(); 13 | const [userName, setUserName] = useState(null); 14 | const [userID, setUserID] = useState(null); 15 | const [reload, setReload] = useState(false); 16 | const [errorMsg, setErrorMsg] = useState(null); 17 | 18 | useEffect(() => { 19 | function handleResize() { 20 | setWidth(window.innerWidth); 21 | } 22 | window.addEventListener("resize", handleResize); 23 | return () => window.removeEventListener("resize", handleResize); 24 | }, [width]); 25 | 26 | const getPostsData = async () => { 27 | try { 28 | const { data: res } = await postsAPI.getPostByUserId( 29 | userID ?? userState.user.id 30 | ); 31 | setPostsData(res); 32 | setErrorMsg(null); 33 | } catch (error) { 34 | setPostsData([]); 35 | setErrorMsg("Error loading user posts"); 36 | console.log("Error retrieving all posts...", error); 37 | } 38 | }; 39 | 40 | useEffect(() => { 41 | (async () => { 42 | if ( 43 | location.state && 44 | location.state.hasOwnProperty("userID") && 45 | location.state.hasOwnProperty("userName") 46 | ) { 47 | setUserName(location.state.userName); 48 | setUserID(location.state.userID); 49 | } 50 | 51 | getPostsData(); 52 | })(); 53 | }, [location.state]); 54 | 55 | useEffect(() => { 56 | getPostsData(); 57 | }, [reload]); 58 | 59 | return ( 60 |
61 | {errorMsg ? ( 62 |
63 | 64 |
65 | ) : Object.keys(postsData).length === 0 ? ( 66 |
67 | 68 |
69 | ) : ( 70 | <> 71 |

{userName ? `Posts of user ${userName}` : "Your posts"}

72 | {Boolean(postsData) && Boolean(postsData.length) ? ( 73 | setReload(reloadTrigger)} 76 | /> 77 | ) : ( 78 |

You have no posts yet

79 | )} 80 | 81 | )} 82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /client/src/pages/Comments/CommentForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { Form, Input, Button, Tag, message, Alert } from "antd"; 4 | import { Form as FinalForm, Field } from "react-final-form"; 5 | import isEmpty from "lodash.isempty"; 6 | import { commentsAPI } from "./../../api/api"; 7 | 8 | export default function CommentForm({ createdBy, postId, setReloadingFlag }) { 9 | const router = useHistory(); 10 | const [initialValues, setInitialValues] = useState({}); 11 | const [submissionErrors, setSubmissionErrors] = useState(null); 12 | const [reloading, setReloading] = useState(false); 13 | 14 | const onSubmit = async (event) => { 15 | if (isEmpty(event) || !event.content) { 16 | setSubmissionErrors("Can't submit an empty comment"); 17 | } else { 18 | setSubmissionErrors(null); 19 | } 20 | 21 | try { 22 | await commentsAPI.add({ 23 | comment: { ...event, createdBy: createdBy, postId: postId }, 24 | }); 25 | message.success("Comment added successfully"); 26 | setReloading(!reloading); 27 | setReloadingFlag(reloading); 28 | } catch (error) { 29 | console.log("Error adding comment...", error.response ?? error); 30 | if (error.response && error.response.data) { 31 | setSubmissionErrors(error.response.data); 32 | } else setSubmissionErrors("Error adding comment"); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 | ( 42 |
{ 44 | await handleSubmit(event); 45 | form.reset(); 46 | }} 47 | > 48 | 49 | 50 | {({ input, meta }) => ( 51 |
52 | 57 |
58 | )} 59 |
60 |
61 | 62 | {submissionErrors && ( 63 | 69 | )} 70 | 71 |
72 | 79 | 80 | 83 |
84 | 85 | )} 86 | /> 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN-Blog 2 | 3 | ## Table of contents 4 | 5 | - [Introduction](#introduction) 6 | - [Demo](#demo) 7 | - [Features](#features) 8 | - [Technology](#technology) 9 | - [Database Models](#database) 10 | - [Run](#run) 11 | - [License](#license) 12 | 13 | ## Introduction 14 | 15 | A virtual blog application using the MERN stack (MongoDB, Express js, React js, and Node js). 16 | 17 | ## Demo 18 | 19 | ![Image description](screenshot-1.png) 20 | 21 | ![Image description](screenshot-2.png) 22 | 23 | This application is deployed on Heroku and can be accessed through the following link: 24 | 25 | [MERN Blog on Heroku](https://mern-blog-01.herokuapp.com/) 26 | 27 | ## Technology 28 | 29 | The main technologies used to build this application are: 30 | 31 | - Node.js version 14.16.0 32 | - MongoDB version 4.4.3 33 | - Express.js version 4.17.1 34 | - React.js version 17.0.1 35 | - Antd, a React UI Framework, version 4.12.3 36 | 37 | ## Features 38 | 39 | A blog app with the following features. 40 | 41 | Unlogged in users can do the following: 42 | 43 | - View all posts. 44 | - View one post's content by clicking on it. 45 | - View post's comments. 46 | - View any user's profile. 47 | - Signup. 48 | 49 | In addition to the above points, logged in users can do the following: 50 | 51 | - Login or logout. 52 | - Create a new post. 53 | - View/Edit/delete their posts. 54 | - Edit their user profile or password. 55 | - Add a new comment on a post. 56 | - View/Edit/Delete their comments. 57 | 58 | ## Database 59 | 60 | All the models can be found in the models directory created using mongoose. 61 | 62 | ### User Schema: 63 | 64 | - userName (String) 65 | - email (String) 66 | - password (String) 67 | - summary (String) 68 | - imagePath (String) 69 | 70 | ### Post Schema: 71 | 72 | - title (String) 73 | - content (String) 74 | - imagePath (String) 75 | - createdAt (Date) 76 | - createdBy (ObjectID - a reference to the user's table) 77 | - comments (ObjectID - an array of comments on the post) 78 | 79 | ### Comment Schema: 80 | 81 | - content (String) 82 | - createdAt (Date) 83 | - createdBy (ObjectID - a reference to the user's table) 84 | 85 | ## Run 86 | 87 | To run this application (the master branch), you have to set your own environmental variables in the server root folder. For security reasons, some variables have been hidden from view and used as environmental variables with the help of dotenv package. Below are the variables that you need to set in order to run the application: 88 | 89 | - MONGO_URI: this is the connection string of your MongoDB Atlas database. 90 | 91 | - SECRET_KEY: you can provide any string here, it is used to encrypt the JWT authentication token. 92 | 93 | After you've set these environmental variables in the .env file at the root of the server folder, you need to navigate to the "seedDB" folder and run "node -r esm seedPosts.js" and "node -r esm seedUsers.js" to fill your empty MongoDB Atlas database. 94 | 95 | Now that the database has data and the environmental variables are all set, you should run the two folders, the client and server together to run the application. Open two terminals, navigate to the client in one and to the server in another, run "npm start" in both terminals and the application should start. 96 | 97 | ## License 98 | 99 | [![License](https://img.shields.io/:License-MIT-blue.svg?style=flat-square)](http://badges.mit-license.org) 100 | 101 | - MIT License 102 | - Copyright 2021 © [Maryam Aljanabi](https://github.com/maryamaljanabi) 103 | -------------------------------------------------------------------------------- /client/src/components/PostsGrid/PostsGrid.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Row, Col, Card, Modal, message } from "antd"; 3 | import defaultPostImage from "./../../assets/images/default-post-image.jpg"; 4 | import { EditTwoTone, DeleteTwoTone } from "@ant-design/icons"; 5 | import { useHistory } from "react-router"; 6 | import "./PostsGrid.scss"; 7 | import { postsAPI } from "./../../api/api"; 8 | import { useSelector } from "react-redux"; 9 | 10 | const { Meta } = Card; 11 | 12 | export default function PostsGrid({ data, reloadPosts }) { 13 | const router = useHistory(); 14 | const [deleteModal, setDeleteModal] = useState(false); 15 | const [deletePostID, setDeletePostID] = useState(null); 16 | const [reload, setReload] = useState(false); 17 | const userState = useSelector((st) => st.user); 18 | 19 | const confirmDelete = async () => { 20 | try { 21 | await postsAPI.delete(deletePostID); 22 | setReload(!reload); 23 | reloadPosts(!reload); 24 | setDeleteModal(false); 25 | message.success("Post deleted successfully"); 26 | } catch (error) { 27 | console.log("Error deleting post...", error.response ?? error); 28 | message.error("Error deleting post"); 29 | if (error.response && error.response.data) { 30 | message.error(error.response.data); 31 | } else message.error("Error deleting post"); 32 | setDeleteModal(false); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 | 39 | {data.map((item) => ( 40 | 41 | 47 | router.push("/posts/view", { postID: item._id }) 48 | } 49 | > 50 | {item.title} 55 |
56 | } 57 | actions={ 58 | item.createdBy === userState.user.id 59 | ? [ 60 | 63 | router.push("/posts/edit", { postID: item._id }) 64 | } 65 | />, 66 | { 70 | setDeletePostID(item._id); 71 | setDeleteModal(true); 72 | }} 73 | />, 74 | ] 75 | : [] 76 | } 77 | > 78 | router.push("/posts/view", { postID: item._id })} 82 | /> 83 | 84 | 85 | ))} 86 | 87 | 88 | confirmDelete()} 92 | onCancel={() => setDeleteModal(false)} 93 | centered 94 | > 95 |

Are you sure you want to delete post?

96 |
97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /client/src/pages/Login/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { Form, Input, Button, Tag, message } from "antd"; 4 | import { Form as FinalForm, Field } from "react-final-form"; 5 | import isEmpty from "lodash.isempty"; 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import { userAuthActions } from "./../../redux/actions/actionCreator"; 8 | 9 | export default function LoginForm() { 10 | const router = useHistory(); 11 | const [initialValues, setInitialValues] = useState({}); 12 | const [submissionErrors, setSubmissionErrors] = useState({}); 13 | const dispatch = useDispatch(); 14 | const userState = useSelector((st) => st.user); 15 | 16 | const onSubmit = async (event) => { 17 | try { 18 | dispatch(userAuthActions.login({ user: event })); 19 | } catch (error) { 20 | console.log("Error logging in user...", error.response ?? error); 21 | if (error.response && error.response.data) { 22 | setSubmissionErrors(error.response.data); 23 | } else setSubmissionErrors({ err: "Login error" }); 24 | } 25 | }; 26 | 27 | const checkValidation = (values) => { 28 | const errors = {}; 29 | if (!values.email?.trim()) { 30 | errors.email = "Please enter the email"; 31 | } 32 | if (!values.password?.trim()) { 33 | errors.password = "Please enter the password"; 34 | } 35 | return errors; 36 | }; 37 | 38 | useEffect(() => { 39 | if (userState.error) { 40 | setSubmissionErrors([userState.error]); 41 | } 42 | if (userState.isLoggedIn) { 43 | message.success("User logged in successfully"); 44 | router.push("/"); 45 | } 46 | }, [userState]); 47 | 48 | return ( 49 | ( 54 |
55 | 60 | 61 | {({ input, meta }) => ( 62 |
63 | 64 | {meta.touched && meta.error && ( 65 | {meta.error} 66 | )} 67 |
68 | )} 69 |
70 |
71 | 72 | 73 | {({ input, meta }) => ( 74 |
75 | 76 | {meta.touched && meta.error && ( 77 | {meta.error} 78 | )} 79 |
80 | )} 81 |
82 |
83 | 84 | {!isEmpty(submissionErrors) && ( 85 |
86 | {typeof submissionErrors === "object" ? ( 87 | Object.entries(submissionErrors).map(([key, value]) => ( 88 | 89 | {value} 90 | 91 | )) 92 | ) : ( 93 | 94 | {submissionErrors} 95 | 96 | )} 97 |
98 | )} 99 | 100 |
101 | 104 | 111 |
112 |
113 | )} 114 | /> 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /client/src/pages/Posts/NewPost.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { Form, Input, Button, Tag, message } from "antd"; 4 | import { Form as FinalForm, Field } from "react-final-form"; 5 | import isEmpty from "lodash.isempty"; 6 | import { useSelector } from "react-redux"; 7 | import TextArea from "antd/lib/input/TextArea"; 8 | import { postsAPI } from "./../../api/api"; 9 | 10 | export default function NewPost() { 11 | const router = useHistory(); 12 | const [initialValues, setInitialValues] = useState({}); 13 | const [submissionErrors, setSubmissionErrors] = useState({}); 14 | const userState = useSelector((st) => st.user); 15 | 16 | const onSubmit = async (event) => { 17 | try { 18 | await postsAPI.add({ 19 | post: { ...event, createdBy: userState.user.id }, 20 | }); 21 | message.success("Post created successfully"); 22 | router.push("/"); 23 | } catch (error) { 24 | console.log("Error creating a new post...", error.response ?? error); 25 | if (error.response && error.response.data) { 26 | setSubmissionErrors(error.response.data); 27 | } else setSubmissionErrors({ err: "Post error" }); 28 | } 29 | }; 30 | 31 | const checkValidation = (values) => { 32 | const errors = {}; 33 | if (!values.title?.trim()) { 34 | errors.title = "Please enter the post's title"; 35 | } 36 | if (!values.content?.trim()) { 37 | errors.content = "Please enter the post's content"; 38 | } 39 | return errors; 40 | }; 41 | 42 | return ( 43 |
44 |

Create a new post

45 | ( 50 |
51 | 56 | 57 | {({ input, meta }) => ( 58 |
59 | 60 | {meta.touched && meta.error && ( 61 | {meta.error} 62 | )} 63 |
64 | )} 65 |
66 |
67 | 68 | 69 | 70 | {({ input, meta }) => ( 71 |
72 |