├── .dockerignore ├── .env.example ├── .gitignore ├── README.md ├── client ├── .env.example ├── .gitignore ├── .prettierrc ├── docker │ ├── Dockerfile │ └── default.conf ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── components │ ├── Footer │ │ ├── Footer.js │ │ └── styles.css │ ├── Loader │ │ ├── Loader.js │ │ └── styles.css │ ├── Message │ │ ├── Message.js │ │ ├── styles.css │ │ └── validation.js │ ├── MessageForm │ │ ├── MessageForm.js │ │ ├── styles.css │ │ └── validation.js │ ├── MessageList │ │ ├── MessageList.js │ │ └── styles.css │ └── Navbar │ │ ├── Navbar.js │ │ └── styles.css │ ├── constants │ └── index.js │ ├── hoc │ ├── requireAdmin.js │ └── requireAuth.js │ ├── index.css │ ├── index.js │ ├── layout │ ├── Layout.js │ └── styles.css │ ├── pages │ ├── Admin │ │ ├── Admin.js │ │ └── styles.css │ ├── Home │ │ ├── Home.js │ │ └── styles.css │ ├── Login │ │ ├── Login.js │ │ ├── styles.css │ │ └── validation.js │ ├── NotFound │ │ ├── NotFound.js │ │ └── styles.css │ ├── Profile │ │ ├── Profile.js │ │ ├── styles.css │ │ └── validation.js │ ├── Register │ │ ├── Register.js │ │ ├── styles.css │ │ └── validation.js │ └── Users │ │ ├── Users.js │ │ └── styles.css │ └── store │ ├── actions │ ├── authActions.js │ ├── messageActions.js │ ├── registerActions.js │ ├── userActions.js │ └── usersActions.js │ ├── reducers │ ├── authReducer.js │ ├── index.js │ ├── messageReducer.js │ ├── registerReducer.js │ ├── userReducer.js │ └── usersReducer.js │ └── types.js ├── docker-compose.yml ├── package.json ├── screenshots ├── Screenshot_1.png ├── Screenshot_2.png ├── Screenshot_3.png ├── Screenshot_4.png ├── Screenshot_5.png └── Screenshot_6.png └── server ├── .babelrc ├── .env.example ├── .gitignore ├── .prettierrc ├── docker ├── Dockerfile └── mongo-data │ └── .gitkeep ├── package.json ├── public └── images │ ├── avatar0.jpg │ ├── avatar1.jpg │ └── avatar2.jpg ├── security └── req.cnf └── src ├── index.js ├── middleware ├── requireJwtAuth.js └── requireLocalAuth.js ├── models ├── Message.js └── User.js ├── routes ├── api │ ├── index.js │ ├── messages.js │ └── users.js ├── facebookAuth.js ├── googleAuth.js ├── index.js └── localAuth.js ├── services ├── facebookStrategy.js ├── googleStrategy.js ├── jwtStrategy.js ├── localStrategy.js └── validators.js └── utils ├── constants.js ├── seed.js └── utils.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .gitignore 3 | **/.gitkeep 4 | **/.DS_Store 5 | .dockerignore 6 | Dockerfile* 7 | docker-compose*.yml 8 | README.md 9 | LICENCE 10 | 11 | **/node_modules 12 | **/build 13 | 14 | screenshots/ 15 | server/public/images/* -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # alternative 2 - put all env vars into one .env file and forward through docker-compose.yml 2 | 3 | # ---------------------------------- 4 | # client container 5 | 6 | REACT_APP_BASE_URL=https://mern-boilerplate.amd2.localhost3002.live 7 | 8 | # ---------------------------------- 9 | # traefik var 10 | 11 | SITE_HOSTNAME=mern-boilerplate.amd2.localhost3002.live 12 | 13 | # ---------------------------------- 14 | # mongo db container 15 | 16 | MONGO_INITDB_DATABASE= 17 | MONGO_INITDB_ROOT_USERNAME= 18 | MONGO_INITDB_ROOT_PASSWORD= 19 | 20 | # -------------------------------- 21 | # server container 22 | 23 | #db 24 | MONGO_URI_DEV=mongodb://localhost:27017/mernboilerplate 25 | # format: mongodb://user:passwword@host.com/database 26 | MONGO_URI_PROD=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mdp-mongo/${MONGO_INITDB_DATABASE} 27 | 28 | #google 29 | GOOGLE_CLIENT_ID= 30 | GOOGLE_CLIENT_SECRET= 31 | GOOGLE_CALLBACK_URL=/auth/google/callback 32 | 33 | #facebook 34 | FACEBOOK_APP_ID= 35 | FACEBOOK_SECRET= 36 | FACEBOOK_CALLBACK_URL=/auth/facebook/callback 37 | 38 | #jwt 39 | JWT_SECRET_DEV=secret 40 | JWT_SECRET_PROD= 41 | 42 | #site urls 43 | CLIENT_URL_DEV=https://localhost:3000 44 | SERVER_URL_DEV=https://localhost:5000 45 | 46 | CLIENT_URL_PROD=https://mern-boilerplate.amd2.localhost3002.live 47 | SERVER_URL_PROD=https://mern-boilerplate.amd2.localhost3002.live 48 | 49 | PORT=80 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | client/node_modules 3 | server/node_modules 4 | 5 | client/npm-debug.log* 6 | 7 | client/build 8 | 9 | **/.env* 10 | !**/.env*.example 11 | 12 | # ignore mongo data 13 | !server/docker/mongo-data 14 | server/docker/mongo-data/* 15 | !server/docker/mongo-data/.gitkeep 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN Boilerplate 2 | 3 | This is full stack boilerplate with React, Redux, Express, Mongoose and Passport. Skip the tedious part and get straight to developing your app. 4 | 5 | ## Demo 6 | 7 | - Live demo is available here: **[https://mern-boilerplate.arm1.nemanjamitic.com](https://mern-boilerplate.arm1.nemanjamitic.com)** 8 | 9 | ## Deployment with Docker (2023. update) 10 | 11 | Since Heroku is no longer free I made Docker production deployment that you can use on any Linux VPS. 12 | 13 | - original [mern-docker-prod](https://github.com/nemanjam/mern-docker-prod) repository with Docker code and instructions that you can reuse to deploy your own Mern apps 14 | - Traefik part of the deployment [traefik-proxy](https://github.com/nemanjam/traefik-proxy) and [traefik-proxy/apps/mern-boilerplate](https://github.com/nemanjam/traefik-proxy/tree/main/apps/mern-boilerplate) 15 | 16 | 17 | ## Features 18 | 19 | - Server 20 | 21 | - User and Message models with `1:N` relation 22 | - Full CRUD REST API operations for both Message and User models 23 | - Passport authentication with local `email/password`, Facebook and Google OAuth strategies and JWT protected APIs 24 | - `User` and `Admin` roles 25 | - NodeJS server with Babel for new JS syntax unified with React client 26 | - `async/await` syntax across whole app 27 | - Joi server side validation of user's input 28 | - Single `.env` file configuration 29 | - Image upload with Multer 30 | - Database seed 31 | 32 | - Client 33 | 34 | - React client with functional components and Hooks 35 | - Redux state management with Thunk for async actions 36 | - CSS agnostic, so you don't waste your time replacing my CSS framework with yours 37 | - Home, Users, Profile, Admin, Notfound, Login and Register pages 38 | - Protected routes with Higher order components 39 | - Different views for unauthenticated, authenticated and admin user 40 | - Edit/Delete forms for Message and User with Formik and Yup validation 41 | - Admin has privileges to edit and delete other users and their messages 42 | - Layout component, so you can have pages without Navbar 43 | - Loading states with Loader component 44 | - Single config file within `/constants` folder 45 | 46 | ## Installation 47 | 48 | Read on on how to set up this for development. Clone the repo. 49 | 50 | ``` 51 | $ git clone https://github.com/nemanjam/mern-boilerplate.git 52 | $ cd mern-boilerplate 53 | ``` 54 | 55 | ### Server 56 | 57 | #### .env file 58 | 59 | Rename `.env.example` to `.env` and fill in database connection strings, Google and Facebook tokens, JWT secret and your client and server production URLs. 60 | 61 | ``` 62 | #db 63 | MONGO_URI_DEV=mongodb://localhost:27017/mernboilerplate 64 | MONGO_URI_PROD= 65 | 66 | #google 67 | GOOGLE_CLIENT_ID= 68 | GOOGLE_CLIENT_SECRET= 69 | GOOGLE_CALLBACK_URL=/auth/google/callback 70 | 71 | #facebook 72 | FACEBOOK_APP_ID= 73 | FACEBOOK_SECRET= 74 | FACEBOOK_CALLBACK_URL=/auth/facebook/callback 75 | 76 | #jwt 77 | JWT_SECRET_DEV=secret 78 | JWT_SECRET_PROD= 79 | 80 | #site urls 81 | CLIENT_URL_DEV=https://localhost:3000 82 | CLIENT_URL_PROD=https://mern-boilerplate-demo.herokuapp.com 83 | SERVER_URL_DEV=https://localhost:5000 84 | SERVER_URL_PROD=https://mern-boilerplate-demo.herokuapp.com 85 | 86 | #img folder path 87 | IMAGES_FOLDER_PATH=/public/images/ 88 | ``` 89 | 90 | #### Generate certificates 91 | 92 | Facebook OAuth requires that your server runs on `https` in development as well, so you need to generate certificates. Go to `/server/security` folder and run this. 93 | 94 | ``` 95 | $ cd server/security 96 | $ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout cert.key -out cert.pem -config req.cnf -sha256 97 | ``` 98 | 99 | #### Install dependencies 100 | 101 | ``` 102 | $ cd server 103 | $ npm install 104 | ``` 105 | 106 | #### Run the server 107 | 108 | You are good to go, server will be available on `https://localhost:5000` 109 | 110 | ``` 111 | $ npm run server 112 | ``` 113 | 114 | ### Client 115 | 116 | Just install the dependencies and run the dev server. App will load on `https://localhost:3000`. 117 | 118 | ``` 119 | $ cd client 120 | $ npm install 121 | $ npm start 122 | ``` 123 | 124 | That's it as far for development setup. For production check the `Deployment on Heroku` section. 125 | 126 | ## Screenshots 127 | 128 | ![Screenshot1](/screenshots/Screenshot_1.png) 129 | 130 | ![Screenshot2](/screenshots/Screenshot_2.png) 131 | 132 | ![Screenshot3](/screenshots/Screenshot_3.png) 133 | 134 | ![Screenshot4](/screenshots/Screenshot_4.png) 135 | 136 | ![Screenshot5](/screenshots/Screenshot_5.png) 137 | 138 | ![Screenshot6](/screenshots/Screenshot_6.png) 139 | 140 | ## Deployment on Heroku 141 | 142 | #### Push to Heroku 143 | 144 | This project is already all set up for deployment on Heroku, you just need to create Heroku application add heroku remote to this repo and push it to `heroku` origin. 145 | 146 | ``` 147 | $ heroku login 148 | $ heroku create my-own-app-name 149 | $ git remote add heroku https://git.heroku.com/my-own-app-name.git 150 | $ git push heroku master 151 | $ heroku open 152 | ``` 153 | 154 | #### Database setup 155 | 156 | But before that you need MongoDB database, so go to [MongoDB Atlas](https://www.mongodb.com/cloud/atlas), create cluster, whitelist all IPs and get database URL. Set that URL in `.env` file as `MONGO_URI_PROD`. 157 | 158 | ``` 159 | MONGO_URI_PROD=mongodb+srv://:@cluster0-abcd.mongodb.net/test?retryWrites=true&w=majority 160 | ``` 161 | 162 | If you don't insert environment variables in Heroku manually via web interface or console you'll need to remove `.env` file from `server/.gitignore` and push it to Heroku. Never push `.env` file to development repo though. 163 | 164 | ``` 165 | ... 166 | #.env #comment out .env file 167 | ... 168 | ``` 169 | 170 | In the following section you can read detailed instructions about Heroku deployment process. 171 | 172 | ### Server setup 173 | 174 | #### Development 175 | 176 | Server uses Babel so that we can use the same newer JavaScript syntax like the one used on the Client. In development we are passing `server/src/index.js` file to `babel-node` executable along with `nodemon` daemon. We run that with `npm run server` script. 177 | 178 | ``` 179 | "server": "nodemon --exec babel-node src/index.js", 180 | ``` 181 | 182 | #### Production 183 | 184 | That is fine for development, we compile the source on every run but for production we want to avoid that and to compile and build code once to JavaScript version which Node.JS can execute. So we take all the code from `/server/src` folder compile it and put the output into `/server/build` destination folder. `-d` is short for destination, and `-s` flag is for sourcemaps for debugging. We make that into `build-babel` script. 185 | 186 | ``` 187 | "build-babel": "babel -d ./build ./src -s", 188 | ``` 189 | 190 | We also need to delete and make `build` folder on every deployment, so we do that with this simple script. 191 | 192 | ``` 193 | "clean": "rm -rf build && mkdir build", 194 | ``` 195 | 196 | Now we have everything to build our server code. We do that by calling 2 last scripts. 197 | 198 | ``` 199 | "build": "npm run clean && npm run build-babel", 200 | ``` 201 | 202 | Now we just need to call build script and run compiled file with node. Make sure Babel is in the production dependencies in the `server/package.json` or you'll get "babel is not defined" error on Heroku. 203 | 204 | ``` 205 | "start-prod": "npm run build && node ./build/index.js", 206 | ``` 207 | 208 | #### Running server on Heroku 209 | 210 | Our server is now all set up, all we need is to call `start-prod` script. Heroku infers runtime he needs to run the application by the type of dependencies file in the root folder, so for Node.JS we need another `package.json`. Heroku will call `start` script after building phase so we just need to pass our `start-prod` script to spin up the server with the `--prefix server` where `server` is folder in which `package.json` with that script is located. 211 | 212 | ``` 213 | "start": "npm run start-prod --prefix server", 214 | ``` 215 | 216 | #### Installing dependencies 217 | 218 | Before all this happens Heroku needs to install the dependencies for both server and client, `heroku-postbuild` script is meant for that. `NPM_CONFIG_PRODUCTION=false` variable is there to disable production environment while dependencies are being installed. Again `--prefix` flag is specifying the folder of the script being run. In this script we build our React client as well. 219 | 220 | ``` 221 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix server && npm install --prefix client && npm run build --prefix client" 222 | ``` 223 | 224 | ### Client Setup 225 | 226 | Before you push to production you'll need to set your URLs in `client/constants`. That's it. 227 | 228 | ```javascript 229 | export const FACEBOOK_AUTH_LINK = 230 | "https://my-own-app.herokuapp.com/auth/facebook"; 231 | export const GOOGLE_AUTH_LINK = "https://my-own-app.herokuapp.com/auth/google"; 232 | ``` 233 | 234 | ## References 235 | 236 | - Brad Traversy [Dev connector 2.0](https://github.com/bradtraversy/devconnector_2.0) 237 | - Brad Traversy [Learn The MERN Stack Youtube playlist](https://www.youtube.com/watch?v=PBTYxXADG_k&list=PLillGF-RfqbbiTGgA77tGO426V3hRF9iE) 238 | - Thinkster [react-redux-realworld-example-app](https://github.com/gothinkster/react-redux-realworld-example-app) 239 | - Thinkster [ 240 | node-express-realworld-example-app ](https://github.com/gothinkster/node-express-realworld-example-app) 241 | - Quinston Pimenta [Deploy React with Node (Express, configured for ES6, Babel) to Heroku (without babel-node)](https://www.youtube.com/watch?v=mvI25HLDfR4) 242 | 243 | - Kim Nguyen [How to Deploy ES6 Node.js & Express back-end to Heroku](https://medium.com/@kimtnguyen/how-to-deploy-es6-node-js-express-back-end-to-heroku-7e6743e8d2ff) 244 | 245 | ## Licence 246 | 247 | ### MIT 248 | -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | # this file must be in client root folder 2 | 3 | REACT_APP_BASE_URL=https://mern-boilerplate.arm1.localhost3002.live 4 | 5 | -------------------------------------------------------------------------------- /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/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /client/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | # both stages must use alpine 3 | FROM node:16.19.0-alpine as build 4 | 5 | WORKDIR /app 6 | 7 | COPY package*.json ./ 8 | RUN npm ci 9 | 10 | # relative to build context 11 | COPY . ./ 12 | 13 | ENV NODE_ENV=production 14 | RUN npm run build 15 | 16 | #----------------------- 17 | 18 | # this has no node, works only for CSR 19 | FROM nginx:1.23.3-alpine as webserver 20 | 21 | WORKDIR /usr/share/nginx/html 22 | 23 | RUN rm /etc/nginx/conf.d/* 24 | RUN rm -rf /usr/share/nginx/html/* 25 | 26 | COPY --from=build /app/build . 27 | COPY --from=build /app/docker/default.conf /etc/nginx/conf.d/default.conf 28 | 29 | EXPOSE 80 30 | ENTRYPOINT ["nginx", "-g", "daemon off;"] 31 | -------------------------------------------------------------------------------- /client/docker/default.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | 4 | listen 80; 5 | 6 | # handle CSR React app 7 | location / { 8 | root /usr/share/nginx/html; 9 | index index.html index.htm; 10 | try_files $uri $uri/ /index.html; 11 | } 12 | 13 | error_page 404 /404.html; 14 | error_page 403 /403.html; 15 | # allow POST on static pages 16 | error_page 405 =200 $uri; 17 | 18 | # express server handles /api /auth and /public/images 19 | # express is exposed directly thorough traefik 20 | location ^/(api|auth|public/images)/ { 21 | # nginx ignore these paths 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-boilerplate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "set HTTPS=true&&react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject" 10 | }, 11 | "dependencies": { 12 | "@testing-library/jest-dom": "^4.2.4", 13 | "@testing-library/react": "^9.3.2", 14 | "@testing-library/user-event": "^7.1.2", 15 | "axios": "^0.19.2", 16 | "formik": "^2.1.4", 17 | "js-cookie": "^2.2.1", 18 | "jwt-decode": "^2.2.0", 19 | "lodash": "^4.17.15", 20 | "moment": "^2.24.0", 21 | "react": "^16.13.1", 22 | "react-dom": "^16.13.1", 23 | "react-redux": "^7.2.0", 24 | "react-router-dom": "^5.1.2", 25 | "react-scripts": "3.4.1", 26 | "redux": "^4.0.5", 27 | "redux-thunk": "^2.3.0", 28 | "yup": "^0.28.3" 29 | }, 30 | "eslintConfig": { 31 | "extends": "react-app" 32 | }, 33 | "homepage": ".", 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemanjam/mern-boilerplate/922cece108659984fbdaaac75ddf00242c304957/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 19 | 28 | React App 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemanjam/mern-boilerplate/922cece108659984fbdaaac75ddf00242c304957/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemanjam/mern-boilerplate/922cece108659984fbdaaac75ddf00242c304957/client/public/logo512.png -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import { compose } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import Cookies from 'js-cookie'; 6 | 7 | import Login from './pages/Login/Login'; 8 | import Register from './pages/Register/Register'; 9 | import Home from './pages/Home/Home'; 10 | import Profile from './pages/Profile/Profile'; 11 | import Users from './pages/Users/Users'; 12 | import Admin from './pages/Admin/Admin'; 13 | import NotFound from './pages/NotFound/NotFound'; 14 | 15 | import Loader from './components/Loader/Loader'; 16 | 17 | import { logInUserWithOauth, loadMe } from './store/actions/authActions'; 18 | 19 | const App = ({ logInUserWithOauth, auth, loadMe }) => { 20 | useEffect(() => { 21 | loadMe(); 22 | }, [loadMe]); 23 | 24 | //redosled hookova 25 | useEffect(() => { 26 | if (window.location.hash === '#_=_') window.location.hash = ''; 27 | 28 | const cookieJwt = Cookies.get('x-auth-cookie'); 29 | if (cookieJwt) { 30 | Cookies.remove('x-auth-cookie'); 31 | logInUserWithOauth(cookieJwt); 32 | } 33 | }, []); 34 | 35 | useEffect(() => { 36 | if (!auth.appLoaded && !auth.isLoading && auth.token && !auth.isAuthenticated) { 37 | loadMe(); 38 | } 39 | }, [auth.isAuthenticated, auth.token, loadMe, auth.isLoading, auth.appLoaded]); 40 | 41 | return ( 42 | <> 43 | {auth.appLoaded ? ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) : ( 55 | 56 | )} 57 | 58 | ); 59 | }; 60 | 61 | const mapStateToProps = (state) => ({ 62 | auth: state.auth, 63 | }); 64 | 65 | export default compose(connect(mapStateToProps, { logInUserWithOauth, loadMe }))(App); 66 | -------------------------------------------------------------------------------- /client/src/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './styles.css'; 4 | 5 | const Footer = () => { 6 | return ( 7 |
8 |
9 | @nemanjam 2020 10 | 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Footer; 23 | -------------------------------------------------------------------------------- /client/src/components/Footer/styles.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | background-color: lightgray; 4 | height: 50px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .footer .footer-content { 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | 16 | .footer .username { 17 | font-size: 18px; 18 | font-weight: bold; 19 | margin-right: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/Loader/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './styles.css'; 4 | 5 | const Loader = (props) => { 6 | return ( 7 |
8 |

Loading..

9 |
10 | ); 11 | }; 12 | 13 | export default Loader; 14 | -------------------------------------------------------------------------------- /client/src/components/Loader/styles.css: -------------------------------------------------------------------------------- 1 | .loader-container { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | justify-content: center; 6 | /* align-items: center; */ 7 | } 8 | 9 | .loader .loader-content { 10 | } 11 | -------------------------------------------------------------------------------- /client/src/components/Message/Message.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import moment from 'moment'; 5 | import { useFormik } from 'formik'; 6 | 7 | import { deleteMessage, editMessage, clearMessageError } from '../../store/actions/messageActions'; 8 | import { messageFormSchema } from './validation'; 9 | 10 | import './styles.css'; 11 | 12 | const Message = ({ message, auth, deleteMessage, editMessage, clearMessageError }) => { 13 | const [isEdit, setIsEdit] = useState(false); 14 | 15 | const handleDelete = (e, id) => { 16 | e.preventDefault(); 17 | if (!isEdit) { 18 | deleteMessage(id); 19 | } 20 | }; 21 | 22 | const handleClickEdit = (e) => { 23 | e.preventDefault(); 24 | formik.setFieldValue('text', message.text); 25 | setIsEdit((oldIsEdit) => !oldIsEdit); 26 | }; 27 | 28 | const formik = useFormik({ 29 | enableReinitialize: true, 30 | initialValues: { 31 | text: '', 32 | id: message.id, 33 | }, 34 | validationSchema: messageFormSchema, 35 | onSubmit: (values, { resetForm }) => { 36 | editMessage(values.id, { text: values.text }); 37 | setIsEdit(false); 38 | // resetForm(); 39 | }, 40 | }); 41 | 42 | // dont reset form if there is an error 43 | useEffect(() => { 44 | if (!message.error && !message.isLoading) formik.resetForm(); 45 | }, [message.error, message.isLoading]); 46 | 47 | // keep edit open if there is an error 48 | useEffect(() => { 49 | if (message.error) setIsEdit(true); 50 | }, [message.error]); 51 | 52 | return ( 53 |
54 |
55 | 56 | 57 | 58 |
59 | 60 | {message.user.name} 61 | 62 | @{message.user.username} 63 | {moment(message.createdAt).fromNow()} 64 | {!moment(message.createdAt).isSame(message.updatedAt, 'minute') && ( 65 | {`Edited: ${moment( 66 | message.updatedAt, 67 | ).fromNow()}`} 68 | )} 69 |
70 |
71 |
72 | {isEdit ? ( 73 | <> 74 |