├── .gitattributes ├── 9781484243909.jpg ├── Contributing.md ├── LICENSE.txt ├── README.md ├── errata.md ├── pro-mern-stack-2-02.08-automate ├── .gitignore ├── README.md ├── commands.md ├── package.json ├── public │ └── index.html ├── server │ └── server.js └── src │ ├── .babelrc │ └── App.jsx ├── pro-mern-stack-2-03.05-dynamic-composition ├── .gitignore ├── README.md ├── commands.md ├── package.json ├── public │ └── index.html ├── server │ └── server.js └── src │ ├── .babelrc │ └── App.jsx ├── pro-mern-stack-2-04.06-stateless-components ├── .gitignore ├── README.md ├── commands.md ├── package.json ├── public │ └── index.html ├── server │ └── server.js └── src │ ├── .babelrc │ └── App.jsx ├── pro-mern-stack-2-05.10-displaying-errors ├── .gitignore ├── README.md ├── commands.md ├── package.json ├── public │ └── index.html ├── server │ ├── schema.graphql │ └── server.js └── src │ ├── .babelrc │ └── App.jsx ├── pro-mern-stack-2-06.06-writing-to-mongodb ├── .gitignore ├── README.md ├── commands.md ├── mongoCommands.md ├── package.json ├── public │ └── index.html ├── scripts │ ├── init.mongo.js │ └── trymongo.js ├── server │ ├── schema.graphql │ └── server.js └── src │ ├── .babelrc │ └── App.jsx ├── pro-mern-stack-2-07.06-react-proptypes ├── .gitignore ├── README.md ├── api │ ├── .eslintrc │ ├── package.json │ ├── sample.env │ ├── schema.graphql │ ├── scripts │ │ ├── init.mongo.js │ │ └── trymongo.js │ └── server.js ├── commands.md ├── mongoCommands.md └── ui │ ├── .eslintrc │ ├── package.json │ ├── public │ └── index.html │ ├── sample.env │ ├── src │ ├── .babelrc │ ├── .eslintrc │ └── App.jsx │ └── uiserver.js ├── pro-mern-stack-2-08.06-debugging ├── .gitignore ├── README.md ├── api │ ├── .eslintrc │ ├── about.js │ ├── api_handler.js │ ├── db.js │ ├── graphql_date.js │ ├── issue.js │ ├── package.json │ ├── sample.env │ ├── schema.graphql │ ├── scripts │ │ ├── init.mongo.js │ │ └── trymongo.js │ └── server.js ├── commands.md ├── mongoCommands.md └── ui │ ├── .eslintrc │ ├── package.json │ ├── public │ └── index.html │ ├── sample.env │ ├── src │ ├── .babelrc │ ├── .eslintrc │ ├── App.jsx │ ├── IssueAdd.jsx │ ├── IssueFilter.jsx │ ├── IssueList.jsx │ ├── IssueTable.jsx │ └── graphQLFetch.js │ ├── uiserver.js │ └── webpack.config.js ├── pro-mern-stack-2-09.07-browser-history-router ├── .gitignore ├── README.md ├── api │ ├── .eslintrc │ ├── about.js │ ├── api_handler.js │ ├── db.js │ ├── graphql_date.js │ ├── issue.js │ ├── package.json │ ├── sample.env │ ├── schema.graphql │ ├── scripts │ │ ├── init.mongo.js │ │ └── trymongo.js │ └── server.js ├── commands.md ├── mongoCommands.md └── ui │ ├── .eslintrc │ ├── package.json │ ├── public │ └── index.html │ ├── sample.env │ ├── src │ ├── .babelrc │ ├── .eslintrc │ ├── App.jsx │ ├── Contents.jsx │ ├── IssueAdd.jsx │ ├── IssueDetail.jsx │ ├── IssueEdit.jsx │ ├── IssueFilter.jsx │ ├── IssueList.jsx │ ├── IssueReport.jsx │ ├── IssueTable.jsx │ ├── Page.jsx │ └── graphQLFetch.js │ ├── uiserver.js │ └── webpack.config.js ├── pro-mern-stack-2-10.13-deleting-an-issue ├── .gitignore ├── README.md ├── api │ ├── .eslintrc │ ├── about.js │ ├── api_handler.js │ ├── db.js │ ├── graphql_date.js │ ├── issue.js │ ├── package.json │ ├── sample.env │ ├── schema.graphql │ ├── scripts │ │ ├── init.mongo.js │ │ └── trymongo.js │ └── server.js ├── commands.md ├── mongoCommands.md └── ui │ ├── .eslintrc │ ├── package.json │ ├── public │ └── index.html │ ├── sample.env │ ├── src │ ├── .babelrc │ ├── .eslintrc │ ├── App.jsx │ ├── Contents.jsx │ ├── DateInput.jsx │ ├── IssueAdd.jsx │ ├── IssueDetail.jsx │ ├── IssueEdit.jsx │ ├── IssueFilter.jsx │ ├── IssueList.jsx │ ├── IssueReport.jsx │ ├── IssueTable.jsx │ ├── NumInput.jsx │ ├── Page.jsx │ ├── TextInput.jsx │ └── graphQLFetch.js │ ├── uiserver.js │ └── webpack.config.js ├── pro-mern-stack-2-11.12-modals ├── .gitignore ├── README.md ├── api │ ├── .eslintrc │ ├── about.js │ ├── api_handler.js │ ├── db.js │ ├── graphql_date.js │ ├── issue.js │ ├── package.json │ ├── sample.env │ ├── schema.graphql │ ├── scripts │ │ ├── init.mongo.js │ │ └── trymongo.js │ └── server.js ├── commands.md ├── mongoCommands.md └── ui │ ├── .eslintrc │ ├── package.json │ ├── public │ ├── bootstrap │ └── index.html │ ├── sample.env │ ├── src │ ├── .babelrc │ ├── .eslintrc │ ├── App.jsx │ ├── Contents.jsx │ ├── DateInput.jsx │ ├── IssueAddNavItem.jsx │ ├── IssueDetail.jsx │ ├── IssueEdit.jsx │ ├── IssueFilter.jsx │ ├── IssueList.jsx │ ├── IssueReport.jsx │ ├── IssueTable.jsx │ ├── NumInput.jsx │ ├── Page.jsx │ ├── TextInput.jsx │ ├── Toast.jsx │ └── graphQLFetch.js │ ├── uiserver.js │ └── webpack.config.js ├── pro-mern-stack-2-12.14-redirects ├── .gitignore ├── README.md ├── api │ ├── .eslintrc │ ├── about.js │ ├── api_handler.js │ ├── db.js │ ├── graphql_date.js │ ├── issue.js │ ├── package.json │ ├── sample.env │ ├── schema.graphql │ ├── scripts │ │ ├── init.mongo.js │ │ └── trymongo.js │ └── server.js ├── commands.md ├── mongoCommands.md └── ui │ ├── .eslintrc │ ├── browser │ ├── .eslintrc │ └── App.jsx │ ├── package.json │ ├── public │ └── bootstrap │ ├── sample.env │ ├── server │ ├── .eslintrc │ ├── render.jsx │ ├── template.js │ └── uiserver.js │ ├── src │ ├── .eslintrc │ ├── About.jsx │ ├── Contents.jsx │ ├── DateInput.jsx │ ├── IssueAddNavItem.jsx │ ├── IssueDetail.jsx │ ├── IssueEdit.jsx │ ├── IssueFilter.jsx │ ├── IssueList.jsx │ ├── IssueReport.jsx │ ├── IssueTable.jsx │ ├── NotFound.jsx │ ├── NumInput.jsx │ ├── Page.jsx │ ├── TextInput.jsx │ ├── Toast.jsx │ ├── graphQLFetch.js │ ├── routes.js │ └── store.js │ ├── webpack.config.js │ └── webpack.serverHMR.js ├── pro-mern-stack-2-13.10-search-bar ├── .gitignore ├── README.md ├── api │ ├── .eslintrc │ ├── about.js │ ├── api_handler.js │ ├── db.js │ ├── graphql_date.js │ ├── issue.js │ ├── package.json │ ├── sample.env │ ├── schema.graphql │ ├── scripts │ │ ├── generate_data.mongo.js │ │ ├── init.mongo.js │ │ └── trymongo.js │ └── server.js ├── commands.md ├── mongoCommands.md └── ui │ ├── .eslintrc │ ├── browser │ ├── .eslintrc │ └── App.jsx │ ├── package.json │ ├── public │ └── bootstrap │ ├── sample.env │ ├── server │ ├── .eslintrc │ ├── render.jsx │ ├── template.js │ └── uiserver.js │ ├── src │ ├── .eslintrc │ ├── About.jsx │ ├── Contents.jsx │ ├── DateInput.jsx │ ├── IssueAddNavItem.jsx │ ├── IssueDetail.jsx │ ├── IssueEdit.jsx │ ├── IssueFilter.jsx │ ├── IssueList.jsx │ ├── IssueReport.jsx │ ├── IssueTable.jsx │ ├── NotFound.jsx │ ├── NumInput.jsx │ ├── Page.jsx │ ├── Search.jsx │ ├── TextInput.jsx │ ├── Toast.jsx │ ├── graphQLFetch.js │ ├── routes.js │ ├── store.js │ └── withToast.jsx │ ├── webpack.config.js │ └── webpack.serverHMR.js ├── pro-mern-stack-2-14.11-cookie-domain ├── .gitignore ├── README.md ├── api │ ├── .eslintrc │ ├── about.js │ ├── api_handler.js │ ├── auth.js │ ├── db.js │ ├── graphql_date.js │ ├── issue.js │ ├── package.json │ ├── sample.env │ ├── schema.graphql │ ├── scripts │ │ ├── generate_data.mongo.js │ │ ├── init.mongo.js │ │ └── trymongo.js │ └── server.js ├── commands.md ├── mongoCommands.md └── ui │ ├── .eslintrc │ ├── browser │ ├── .eslintrc │ └── App.jsx │ ├── package.json │ ├── public │ └── bootstrap │ ├── sample.env │ ├── server │ ├── .eslintrc │ ├── render.jsx │ ├── template.js │ └── uiserver.js │ ├── src │ ├── .eslintrc │ ├── About.jsx │ ├── Contents.jsx │ ├── DateInput.jsx │ ├── IssueAddNavItem.jsx │ ├── IssueDetail.jsx │ ├── IssueEdit.jsx │ ├── IssueFilter.jsx │ ├── IssueList.jsx │ ├── IssueReport.jsx │ ├── IssueTable.jsx │ ├── NotFound.jsx │ ├── NumInput.jsx │ ├── Page.jsx │ ├── Search.jsx │ ├── SignInNavItem.jsx │ ├── TextInput.jsx │ ├── Toast.jsx │ ├── UserContext.js │ ├── graphQLFetch.js │ ├── routes.js │ ├── store.js │ └── withToast.jsx │ ├── webpack.config.js │ └── webpack.serverHMR.js └── pro-mern-stack-2-15.07-non-proxy-mode ├── .gitignore ├── README.md ├── api ├── .eslintrc ├── .gitignore ├── about.js ├── api_handler.js ├── auth.js ├── db.js ├── graphql_date.js ├── issue.js ├── package.json ├── sample.env ├── schema.graphql ├── scripts │ ├── generate_data.mongo.js │ ├── init.mongo.js │ └── trymongo.js └── server.js ├── commands.md ├── mongoCommands.md └── ui ├── .eslintrc ├── .gitignore ├── browser ├── .eslintrc └── App.jsx ├── package.json ├── public └── bootstrap ├── sample.env ├── server ├── .eslintrc ├── render.jsx ├── template.js └── uiserver.js ├── src ├── .eslintrc ├── About.jsx ├── Contents.jsx ├── DateInput.jsx ├── IssueAddNavItem.jsx ├── IssueDetail.jsx ├── IssueEdit.jsx ├── IssueFilter.jsx ├── IssueList.jsx ├── IssueReport.jsx ├── IssueTable.jsx ├── NotFound.jsx ├── NumInput.jsx ├── Page.jsx ├── Search.jsx ├── SignInNavItem.jsx ├── TextInput.jsx ├── Toast.jsx ├── UserContext.js ├── graphQLFetch.js ├── routes.js ├── store.js └── withToast.jsx ├── webpack.config.js └── webpack.serverHMR.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /9781484243909.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-mern-stack/811109bb3c40c8f886202c4647dd182f595ac30c/9781484243909.jpg -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Apress Source Code 2 | 3 | Copyright for Apress source code belongs to the author(s). However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author(s) and other readers. 4 | 5 | ## How to Contribute 6 | 7 | 1. Make sure you have a GitHub account. 8 | 2. Fork the repository for the relevant book. 9 | 3. Create a new branch on which to make your change, e.g. 10 | `git checkout -b my_code_contribution` 11 | 4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted. 12 | 5. Submit a pull request. 13 | 14 | Thank you for your contribution! -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Freeware License, some rights reserved 2 | 3 | Copyright (c) 2019 Vasan Subramanian 4 | 5 | Permission is hereby granted, free of charge, to anyone obtaining a copy 6 | of this software and associated documentation files (the "Software"), 7 | to work with the Software within the limits of freeware distribution and fair use. 8 | This includes the rights to use, copy, and modify the Software for personal use. 9 | Users are also allowed and encouraged to submit corrections and modifications 10 | to the Software for the benefit of other users. 11 | 12 | It is not allowed to reuse, modify, or redistribute the Software for 13 | commercial use in any way, or for a user’s educational materials such as books 14 | or blog articles without prior permission from the copyright holder. 15 | 16 | The above copyright notice and this permission notice need to be included 17 | in all copies or substantial portions of the software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS OR APRESS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apress Source Code 2 | 3 | This repository accompanies [*Pro MERN Stack*](https://www.apress.com/9781484243909) by Vasan Subramanian (Apress, 2019). 4 | 5 | [comment]: #cover 6 | ![Cover image](9781484243909.jpg) 7 | 8 | Download the files as a zip using the green button, or clone the repository to your machine using Git. 9 | 10 | ## Releases 11 | 12 | Release v1.0 corresponds to the code in the published book, without corrections or updates. 13 | 14 | ## Contributions 15 | 16 | See the file Contributing.md for more information on how you can contribute to this repository. -------------------------------------------------------------------------------- /errata.md: -------------------------------------------------------------------------------- 1 | # Errata for *Book Title* 2 | 3 | On **page xx** [Summary of error]: 4 | 5 | Details of error here. Highlight key pieces in **bold**. 6 | 7 | *** 8 | 9 | On **page xx** [Summary of error]: 10 | 11 | Details of error here. Highlight key pieces in **bold**. 12 | 13 | *** -------------------------------------------------------------------------------- /pro-mern-stack-2-02.08-automate/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | public/App.js 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-02.08-automate/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-02.08-automate/commands.md: -------------------------------------------------------------------------------- 1 | # Command-line commands 2 | 3 | This is a list of all command-line commands used in the book. It includes 4 | installation and other shell-based commands used to try out things or 5 | run scripts manually. 6 | 7 | ## Chapter 2: Hello World 8 | 9 | ### Project Set Up 10 | 11 | ``` 12 | nvm install 10 13 | nvm alias default 10 14 | node --version 15 | npm --version 16 | npm install -g npm@6 17 | npm init 18 | npm install express 19 | npm uninstall express 20 | npm install express@4 21 | ``` 22 | 23 | ### Express 24 | ``` 25 | node server.js 26 | npm start 27 | ``` 28 | 29 | ### JSX Transform 30 | ``` 31 | npm install --save-dev @babel/core@7 @babel/cli@7 32 | node_modules/.bin/babel --version 33 | npx babel --version 34 | npm install --save-dev @babel/preset-react@7 35 | npx babel src --presets @babel/react --out-dir public 36 | ``` 37 | 38 | ### Older Browsers Support 39 | ``` 40 | npm install --no-save @babel/plugin-transform-arrow-functions@7 41 | npx babel src --presets @babel/react --plugins=@babel/plugin-transform-arrow-functions --out-dir public 42 | npm uninstall @babel/plugin-transform-arrow-functions@7 43 | npm install --save-dev @babel/preset-env 44 | npx babel src --out-dir public 45 | ``` 46 | 47 | ### Automate 48 | ``` 49 | npm install nodemon@1 50 | ``` 51 | -------------------------------------------------------------------------------- /pro-mern-stack-2-02.08-automate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition)", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -w server server/server.js", 8 | "compile": "babel src --out-dir public", 9 | "watch": "babel src --out-dir public --watch --verbose", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 15 | }, 16 | "author": "vasan.promern@gmail.com", 17 | "license": "ISC", 18 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 19 | "dependencies": { 20 | "express": "^4.16.4", 21 | "nodemon": "^1.18.9" 22 | }, 23 | "devDependencies": { 24 | "@babel/cli": "^7.2.3", 25 | "@babel/core": "^7.2.2", 26 | "@babel/preset-env": "^7.2.3", 27 | "@babel/preset-react": "^7.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pro-mern-stack-2-02.08-automate/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /pro-mern-stack-2-02.08-automate/server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | 5 | app.use(express.static('public')); 6 | 7 | app.listen(3000, function () { 8 | console.log('App started on port 3000'); 9 | }); 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-02.08-automate/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-02.08-automate/src/App.jsx: -------------------------------------------------------------------------------- 1 | const continents = ['Africa','America','Asia','Australia','Europe']; 2 | const helloContinents = Array.from(continents, c => `Hello ${c}!`); 3 | const message = helloContinents.join(' '); 4 | 5 | const element = ( 6 |
7 |

{message}

8 |
9 | ); 10 | 11 | ReactDOM.render(element, document.getElementById('contents')); 12 | -------------------------------------------------------------------------------- /pro-mern-stack-2-03.05-dynamic-composition/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | public/App.js 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-03.05-dynamic-composition/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-03.05-dynamic-composition/commands.md: -------------------------------------------------------------------------------- 1 | # Command-line commands 2 | 3 | This is a list of all command-line commands used in the book. It includes 4 | installation and other shell-based commands used to try out things or 5 | run scripts manually. 6 | 7 | ## Chapter 2: Hello World 8 | 9 | ### Project Set Up 10 | 11 | ``` 12 | nvm install 10 13 | nvm alias default 10 14 | node --version 15 | npm --version 16 | npm install -g npm@6 17 | npm init 18 | npm install express 19 | npm uninstall express 20 | npm install express@4 21 | ``` 22 | 23 | ### Express 24 | ``` 25 | node server.js 26 | npm start 27 | ``` 28 | 29 | ### JSX Transform 30 | ``` 31 | npm install --save-dev @babel/core@7 @babel/cli@7 32 | node_modules/.bin/babel --version 33 | npx babel --version 34 | npm install --save-dev @babel/preset-react@7 35 | npx babel src --presets @babel/react --out-dir public 36 | ``` 37 | 38 | ### Older Browsers Support 39 | ``` 40 | npm install --no-save @babel/plugin-transform-arrow-functions@7 41 | npx babel src --presets @babel/react --plugins=@babel/plugin-transform-arrow-functions --out-dir public 42 | npm uninstall @babel/plugin-transform-arrow-functions@7 43 | npm install --save-dev @babel/preset-env 44 | npx babel src --out-dir public 45 | ``` 46 | 47 | ### Automate 48 | ``` 49 | npm install nodemon@1 50 | ``` 51 | -------------------------------------------------------------------------------- /pro-mern-stack-2-03.05-dynamic-composition/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition)", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -w server server/server.js", 8 | "compile": "babel src --out-dir public", 9 | "watch": "babel src --out-dir public --watch --verbose", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 15 | }, 16 | "author": "vasan.promern@gmail.com", 17 | "license": "ISC", 18 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 19 | "dependencies": { 20 | "express": "^4.16.4", 21 | "nodemon": "^1.18.9" 22 | }, 23 | "devDependencies": { 24 | "@babel/cli": "^7.2.3", 25 | "@babel/core": "^7.2.2", 26 | "@babel/preset-env": "^7.2.3", 27 | "@babel/preset-react": "^7.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pro-mern-stack-2-03.05-dynamic-composition/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /pro-mern-stack-2-03.05-dynamic-composition/server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | 5 | app.use(express.static('public')); 6 | 7 | app.listen(3000, function () { 8 | console.log('App started on port 3000'); 9 | }); 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-03.05-dynamic-composition/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-04.06-stateless-components/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | public/App.js 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-04.06-stateless-components/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-04.06-stateless-components/commands.md: -------------------------------------------------------------------------------- 1 | # Command-line commands 2 | 3 | This is a list of all command-line commands used in the book. It includes 4 | installation and other shell-based commands used to try out things or 5 | run scripts manually. 6 | 7 | ## Chapter 2: Hello World 8 | 9 | ### Project Set Up 10 | 11 | ``` 12 | nvm install 10 13 | nvm alias default 10 14 | node --version 15 | npm --version 16 | npm install -g npm@6 17 | npm init 18 | npm install express 19 | npm uninstall express 20 | npm install express@4 21 | ``` 22 | 23 | ### Express 24 | ``` 25 | node server.js 26 | npm start 27 | ``` 28 | 29 | ### JSX Transform 30 | ``` 31 | npm install --save-dev @babel/core@7 @babel/cli@7 32 | node_modules/.bin/babel --version 33 | npx babel --version 34 | npm install --save-dev @babel/preset-react@7 35 | npx babel src --presets @babel/react --out-dir public 36 | ``` 37 | 38 | ### Older Browsers Support 39 | ``` 40 | npm install --no-save @babel/plugin-transform-arrow-functions@7 41 | npx babel src --presets @babel/react --plugins=@babel/plugin-transform-arrow-functions --out-dir public 42 | npm uninstall @babel/plugin-transform-arrow-functions@7 43 | npm install --save-dev @babel/preset-env 44 | npx babel src --out-dir public 45 | ``` 46 | 47 | ### Automate 48 | ``` 49 | npm install nodemon@1 50 | ``` 51 | -------------------------------------------------------------------------------- /pro-mern-stack-2-04.06-stateless-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition)", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -w server server/server.js", 8 | "compile": "babel src --out-dir public", 9 | "watch": "babel src --out-dir public --watch --verbose", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 15 | }, 16 | "author": "vasan.promern@gmail.com", 17 | "license": "ISC", 18 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 19 | "dependencies": { 20 | "express": "^4.16.4", 21 | "nodemon": "^1.18.9" 22 | }, 23 | "devDependencies": { 24 | "@babel/cli": "^7.2.3", 25 | "@babel/core": "^7.2.2", 26 | "@babel/preset-env": "^7.2.3", 27 | "@babel/preset-react": "^7.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pro-mern-stack-2-04.06-stateless-components/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /pro-mern-stack-2-04.06-stateless-components/server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | 5 | app.use(express.static('public')); 6 | 7 | app.listen(3000, function () { 8 | console.log('App started on port 3000'); 9 | }); 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-04.06-stateless-components/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-05.10-displaying-errors/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | public/App.js 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-05.10-displaying-errors/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-05.10-displaying-errors/commands.md: -------------------------------------------------------------------------------- 1 | # Command-line commands 2 | 3 | This is a list of all command-line commands used in the book. It includes 4 | installation and other shell-based commands used to try out things or 5 | run scripts manually. 6 | 7 | ## Chapter 2: Hello World 8 | 9 | ### Project Set Up 10 | 11 | ``` 12 | nvm install 10 13 | nvm alias default 10 14 | node --version 15 | npm --version 16 | npm install -g npm@6 17 | npm init 18 | npm install express 19 | npm uninstall express 20 | npm install express@4 21 | ``` 22 | 23 | ### Express 24 | ``` 25 | node server.js 26 | npm start 27 | ``` 28 | 29 | ### JSX Transform 30 | ``` 31 | npm install --save-dev @babel/core@7 @babel/cli@7 32 | node_modules/.bin/babel --version 33 | npx babel --version 34 | npm install --save-dev @babel/preset-react@7 35 | npx babel src --presets @babel/react --out-dir public 36 | ``` 37 | 38 | ### Older Browsers Support 39 | ``` 40 | npm install --no-save @babel/plugin-transform-arrow-functions@7 41 | npx babel src --presets @babel/react --plugins=@babel/plugin-transform-arrow-functions --out-dir public 42 | npm uninstall @babel/plugin-transform-arrow-functions@7 43 | npm install --save-dev @babel/preset-env 44 | npx babel src --out-dir public 45 | ``` 46 | 47 | ### Automate 48 | ``` 49 | npm install nodemon@1 50 | ``` 51 | 52 | ## Chapter 5: Express GraphQL APIs 53 | 54 | ### About API 55 | ``` 56 | npm install graphql@0 apollo-server-express@2 57 | curl "http://localhost:3000/graphql?query=query+\{+about+\}" 58 | ``` 59 | -------------------------------------------------------------------------------- /pro-mern-stack-2-05.10-displaying-errors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition)", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -w server -e js,graphql server/server.js", 8 | "compile": "babel src --out-dir public", 9 | "watch": "babel src --out-dir public --watch --verbose", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 15 | }, 16 | "author": "vasan.promern@gmail.com", 17 | "license": "ISC", 18 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 19 | "dependencies": { 20 | "apollo-server-express": "^2.3.1", 21 | "express": "^4.16.4", 22 | "graphql": "^0.13.2", 23 | "nodemon": "^1.18.9" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "^7.2.3", 27 | "@babel/core": "^7.2.2", 28 | "@babel/preset-env": "^7.2.3", 29 | "@babel/preset-react": "^7.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-05.10-displaying-errors/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pro-mern-stack-2-05.10-displaying-errors/server/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar GraphQLDate 2 | 3 | enum StatusType { 4 | New 5 | Assigned 6 | Fixed 7 | Closed 8 | } 9 | 10 | type Issue { 11 | id: Int! 12 | title: String! 13 | status: StatusType! 14 | owner: String 15 | effort: Int 16 | created: GraphQLDate! 17 | due: GraphQLDate 18 | } 19 | 20 | "Toned down Issue, used as inputs, without server generated values." 21 | input IssueInputs { 22 | title: String! 23 | "Optional, if not supplied, will be set to 'New'" 24 | status: StatusType = New 25 | owner: String 26 | effort: Int 27 | due: GraphQLDate 28 | } 29 | 30 | ##### Top level declarations 31 | 32 | type Query { 33 | about: String! 34 | issueList: [Issue!]! 35 | } 36 | 37 | type Mutation { 38 | setAboutMessage(message: String!): String 39 | issueAdd(issue: IssueInputs!): Issue! 40 | } 41 | -------------------------------------------------------------------------------- /pro-mern-stack-2-05.10-displaying-errors/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-06.06-writing-to-mongodb/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | public/App.js 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-06.06-writing-to-mongodb/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-06.06-writing-to-mongodb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition)", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -w server -e js,graphql server/server.js", 8 | "compile": "babel src --out-dir public", 9 | "watch": "babel src --out-dir public --watch --verbose", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 15 | }, 16 | "author": "vasan.promern@gmail.com", 17 | "license": "ISC", 18 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 19 | "dependencies": { 20 | "apollo-server-express": "^2.3.1", 21 | "express": "^4.16.4", 22 | "graphql": "^0.13.2", 23 | "mongodb": "^3.1.10", 24 | "nodemon": "^1.18.9" 25 | }, 26 | "devDependencies": { 27 | "@babel/cli": "^7.2.3", 28 | "@babel/core": "^7.2.2", 29 | "@babel/preset-env": "^7.2.3", 30 | "@babel/preset-react": "^7.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pro-mern-stack-2-06.06-writing-to-mongodb/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pro-mern-stack-2-06.06-writing-to-mongodb/scripts/init.mongo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Run using the mongo shell. For remote databases, ensure that the 3 | * connection string is supplied in the command line. For example: 4 | * localhost: 5 | * mongo issuetracker scripts/init.mongo.js 6 | * Atlas: 7 | * mongo mongodb+srv://user:pwd@xxx.mongodb.net/issuetracker scripts/init.mongo.js 8 | * MLab: 9 | * mongo mongodb://user:pwd@xxx.mlab.com:33533/issuetracker scripts/init.mongo.js 10 | */ 11 | 12 | db.issues.remove({}); 13 | 14 | const issuesDB = [ 15 | { 16 | id: 1, status: 'New', owner: 'Ravan', effort: 5, 17 | created: new Date('2019-01-15'), due: undefined, 18 | title: 'Error in console when clicking Add', 19 | }, 20 | { 21 | id: 2, status: 'Assigned', owner: 'Eddie', effort: 14, 22 | created: new Date('2019-01-16'), due: new Date('2019-02-01'), 23 | title: 'Missing bottom border on panel', 24 | }, 25 | ]; 26 | 27 | db.issues.insertMany(issuesDB); 28 | const count = db.issues.count(); 29 | print('Inserted', count, 'issues'); 30 | 31 | db.counters.remove({ _id: 'issues' }); 32 | db.counters.insert({ _id: 'issues', current: count }); 33 | 34 | db.issues.createIndex({ id: 1 }, { unique: true }); 35 | db.issues.createIndex({ status: 1 }); 36 | db.issues.createIndex({ owner: 1 }); 37 | db.issues.createIndex({ created: 1 }); 38 | -------------------------------------------------------------------------------- /pro-mern-stack-2-06.06-writing-to-mongodb/server/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar GraphQLDate 2 | 3 | enum StatusType { 4 | New 5 | Assigned 6 | Fixed 7 | Closed 8 | } 9 | 10 | type Issue { 11 | _id: ID! 12 | id: Int! 13 | title: String! 14 | status: StatusType! 15 | owner: String 16 | effort: Int 17 | created: GraphQLDate! 18 | due: GraphQLDate 19 | } 20 | 21 | "Toned down Issue, used as inputs, without server generated values." 22 | input IssueInputs { 23 | title: String! 24 | "Optional, if not supplied, will be set to 'New'" 25 | status: StatusType = New 26 | owner: String 27 | effort: Int 28 | due: GraphQLDate 29 | } 30 | 31 | ##### Top level declarations 32 | 33 | type Query { 34 | about: String! 35 | issueList: [Issue!]! 36 | } 37 | 38 | type Mutation { 39 | setAboutMessage(message: String!): String 40 | issueAdd(issue: IssueInputs!): Issue! 41 | } 42 | -------------------------------------------------------------------------------- /pro-mern-stack-2-06.06-writing-to-mongodb/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | ui/public/App.js 4 | .env 5 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": "true" 5 | }, 6 | rules: { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-api", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -e js,graphql -w . -w .env server.js", 8 | "lint": "eslint .", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 14 | }, 15 | "author": "vasan.promern@gmail.com", 16 | "license": "ISC", 17 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 18 | "dependencies": { 19 | "apollo-server-express": "^2.3.1", 20 | "dotenv": "^6.2.0", 21 | "express": "^4.16.4", 22 | "graphql": "^0.13.2", 23 | "mongodb": "^3.1.10", 24 | "nodemon": "^1.18.9" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^5.12.0", 28 | "eslint-config-airbnb-base": "^13.1.0", 29 | "eslint-plugin-import": "^2.14.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/api/sample.env: -------------------------------------------------------------------------------- 1 | ## DB 2 | # Local 3 | DB_URL=mongodb://localhost/issuetracker 4 | 5 | # Atlas - replace UUU: user, PPP: password, XXX: hostname 6 | # DB_URL=mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true 7 | 8 | # mLab - replace UUU: user, PPP: password, XXX: hostname, YYY: port 9 | # DB_URL=mongodb://UUU:PPP@XXX.mlab.com:YYY/issuetracker 10 | 11 | 12 | ## Server Port 13 | API_SERVER_PORT=3000 14 | 15 | ## Enable CORS (default: true) 16 | # ENABLE_CORS=false 17 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/api/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar GraphQLDate 2 | 3 | enum StatusType { 4 | New 5 | Assigned 6 | Fixed 7 | Closed 8 | } 9 | 10 | type Issue { 11 | _id: ID! 12 | id: Int! 13 | title: String! 14 | status: StatusType! 15 | owner: String 16 | effort: Int 17 | created: GraphQLDate! 18 | due: GraphQLDate 19 | } 20 | 21 | "Toned down Issue, used as inputs, without server generated values." 22 | input IssueInputs { 23 | title: String! 24 | "Optional, if not supplied, will be set to 'New'" 25 | status: StatusType = New 26 | owner: String 27 | effort: Int 28 | due: GraphQLDate 29 | } 30 | 31 | ##### Top level declarations 32 | 33 | type Query { 34 | about: String! 35 | issueList: [Issue!]! 36 | } 37 | 38 | type Mutation { 39 | setAboutMessage(message: String!): String 40 | issueAdd(issue: IssueInputs!): Issue! 41 | } 42 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/api/scripts/init.mongo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Run using the mongo shell. For remote databases, ensure that the 3 | * connection string is supplied in the command line. For example: 4 | * localhost: 5 | * mongo issuetracker scripts/init.mongo.js 6 | * Atlas: 7 | * mongo mongodb+srv://user:pwd@xxx.mongodb.net/issuetracker scripts/init.mongo.js 8 | * MLab: 9 | * mongo mongodb://user:pwd@xxx.mlab.com:33533/issuetracker scripts/init.mongo.js 10 | */ 11 | 12 | /* global db print */ 13 | /* eslint no-restricted-globals: "off" */ 14 | 15 | db.issues.remove({}); 16 | 17 | const issuesDB = [ 18 | { 19 | id: 1, 20 | status: 'New', 21 | owner: 'Ravan', 22 | effort: 5, 23 | created: new Date('2019-01-15'), 24 | due: undefined, 25 | title: 'Error in console when clicking Add', 26 | }, 27 | { 28 | id: 2, 29 | status: 'Assigned', 30 | owner: 'Eddie', 31 | effort: 14, 32 | created: new Date('2019-01-16'), 33 | due: new Date('2019-02-01'), 34 | title: 'Missing bottom border on panel', 35 | }, 36 | ]; 37 | 38 | db.issues.insertMany(issuesDB); 39 | const count = db.issues.count(); 40 | print('Inserted', count, 'issues'); 41 | 42 | db.counters.remove({ _id: 'issues' }); 43 | db.counters.insert({ _id: 'issues', current: count }); 44 | 45 | db.issues.createIndex({ id: 1 }, { unique: true }); 46 | db.issues.createIndex({ status: 1 }); 47 | db.issues.createIndex({ owner: 1 }); 48 | db.issues.createIndex({ created: 1 }); 49 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-ui", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) - UI", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -w uiserver.js -w .env uiserver.js", 8 | "lint": "eslint . --ext js,jsx --ignore-pattern public", 9 | "compile": "babel src --out-dir public", 10 | "watch": "babel src --out-dir public --watch --verbose" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 15 | }, 16 | "author": "vasan.promern@gmail.com", 17 | "license": "ISC", 18 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 19 | "dependencies": { 20 | "dotenv": "^6.2.0", 21 | "express": "^4.16.4", 22 | "http-proxy-middleware": "^0.19.1", 23 | "nodemon": "^1.18.9" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "^7.2.3", 27 | "@babel/core": "^7.2.2", 28 | "@babel/preset-env": "^7.2.3", 29 | "@babel/preset-react": "^7.0.0", 30 | "eslint": "^5.12.0", 31 | "eslint-config-airbnb": "^17.1.0", 32 | "eslint-plugin-import": "^2.14.0", 33 | "eslint-plugin-jsx-a11y": "^6.1.2", 34 | "eslint-plugin-react": "^7.12.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/ui/sample.env: -------------------------------------------------------------------------------- 1 | UI_SERVER_PORT=8000 2 | UI_API_ENDPOINT=http://localhost:3000/graphql 3 | # API_PROXY_TARGET=http://localhost:3000 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/ui/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/ui/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true 5 | }, 6 | rules: { 7 | "react/prop-types": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-07.06-react-proptypes/ui/uiserver.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const proxy = require('http-proxy-middleware'); 4 | 5 | const app = express(); 6 | 7 | app.use(express.static('public')); 8 | 9 | const apiProxyTarget = process.env.API_PROXY_TARGET; 10 | if (apiProxyTarget) { 11 | app.use('/graphql', proxy({ target: apiProxyTarget })); 12 | } 13 | 14 | const UI_API_ENDPOINT = process.env.UI_API_ENDPOINT 15 | || 'http://localhost:3000/graphql'; 16 | const env = { UI_API_ENDPOINT }; 17 | 18 | app.get('/env.js', (req, res) => { 19 | res.send(`window.ENV = ${JSON.stringify(env)}`); 20 | }); 21 | 22 | const port = process.env.UI_SERVER_PORT || 8000; 23 | 24 | app.listen(port, () => { 25 | console.log(`UI started on port ${port}`); 26 | }); 27 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | ui/public/*.js 4 | ui/public/*.map 5 | .env 6 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": "true" 5 | }, 6 | rules: { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/about.js: -------------------------------------------------------------------------------- 1 | let aboutMessage = 'Issue Tracker API v1.0'; 2 | 3 | function setMessage(_, { message }) { 4 | aboutMessage = message; 5 | return aboutMessage; 6 | } 7 | 8 | function getMessage() { 9 | return aboutMessage; 10 | } 11 | 12 | module.exports = { getMessage, setMessage }; 13 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/api_handler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | require('dotenv').config(); 3 | const { ApolloServer } = require('apollo-server-express'); 4 | 5 | const GraphQLDate = require('./graphql_date.js'); 6 | const about = require('./about.js'); 7 | const issue = require('./issue.js'); 8 | 9 | const resolvers = { 10 | Query: { 11 | about: about.getMessage, 12 | issueList: issue.list, 13 | }, 14 | Mutation: { 15 | setAboutMessage: about.setMessage, 16 | issueAdd: issue.add, 17 | }, 18 | GraphQLDate, 19 | }; 20 | 21 | const server = new ApolloServer({ 22 | typeDefs: fs.readFileSync('schema.graphql', 'utf-8'), 23 | resolvers, 24 | formatError: (error) => { 25 | console.log(error); 26 | return error; 27 | }, 28 | }); 29 | 30 | function installHandler(app) { 31 | const enableCors = (process.env.ENABLE_CORS || 'true') === 'true'; 32 | console.log('CORS setting:', enableCors); 33 | server.applyMiddleware({ app, path: '/graphql', cors: enableCors }); 34 | } 35 | 36 | module.exports = { installHandler }; 37 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { MongoClient } = require('mongodb'); 3 | 4 | let db; 5 | 6 | async function connectToDb() { 7 | const url = process.env.DB_URL || 'mongodb://localhost/issuetracker'; 8 | const client = new MongoClient(url, { useNewUrlParser: true }); 9 | await client.connect(); 10 | console.log('Connected to MongoDB at', url); 11 | db = client.db(); 12 | } 13 | 14 | async function getNextSequence(name) { 15 | const result = await db.collection('counters').findOneAndUpdate( 16 | { _id: name }, 17 | { $inc: { current: 1 } }, 18 | { returnOriginal: false }, 19 | ); 20 | return result.value.current; 21 | } 22 | 23 | function getDb() { 24 | return db; 25 | } 26 | 27 | module.exports = { connectToDb, getNextSequence, getDb }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/graphql_date.js: -------------------------------------------------------------------------------- 1 | const { GraphQLScalarType } = require('graphql'); 2 | const { Kind } = require('graphql/language'); 3 | 4 | const GraphQLDate = new GraphQLScalarType({ 5 | name: 'GraphQLDate', 6 | description: 'A Date() type in GraphQL as a scalar', 7 | serialize(value) { 8 | return value.toISOString(); 9 | }, 10 | parseValue(value) { 11 | const dateValue = new Date(value); 12 | return Number.isNaN(dateValue.getTime()) ? undefined : dateValue; 13 | }, 14 | parseLiteral(ast) { 15 | if (ast.kind === Kind.STRING) { 16 | const value = new Date(ast.value); 17 | return Number.isNaN(value.getTime()) ? undefined : value; 18 | } 19 | return undefined; 20 | }, 21 | }); 22 | 23 | module.exports = GraphQLDate; 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/issue.js: -------------------------------------------------------------------------------- 1 | const { UserInputError } = require('apollo-server-express'); 2 | const { getDb, getNextSequence } = require('./db.js'); 3 | 4 | async function list() { 5 | const db = getDb(); 6 | const issues = await db.collection('issues').find({}).toArray(); 7 | return issues; 8 | } 9 | 10 | function validate(issue) { 11 | const errors = []; 12 | if (issue.title.length < 3) { 13 | errors.push('Field "title" must be at least 3 characters long.'); 14 | } 15 | if (issue.status === 'Assigned' && !issue.owner) { 16 | errors.push('Field "owner" is required when status is "Assigned"'); 17 | } 18 | if (errors.length > 0) { 19 | throw new UserInputError('Invalid input(s)', { errors }); 20 | } 21 | } 22 | 23 | async function add(_, { issue }) { 24 | const db = getDb(); 25 | validate(issue); 26 | 27 | const newIssue = Object.assign({}, issue); 28 | newIssue.created = new Date(); 29 | newIssue.id = await getNextSequence('issues'); 30 | 31 | const result = await db.collection('issues').insertOne(newIssue); 32 | const savedIssue = await db.collection('issues') 33 | .findOne({ _id: result.insertedId }); 34 | return savedIssue; 35 | } 36 | 37 | module.exports = { list, add }; 38 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-api", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -e js,graphql -w . -w .env server.js", 8 | "lint": "eslint .", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 14 | }, 15 | "author": "vasan.promern@gmail.com", 16 | "license": "ISC", 17 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 18 | "dependencies": { 19 | "apollo-server-express": "^2.3.1", 20 | "dotenv": "^6.2.0", 21 | "express": "^4.16.4", 22 | "graphql": "^0.13.2", 23 | "mongodb": "^3.1.10", 24 | "nodemon": "^1.18.9" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^5.12.0", 28 | "eslint-config-airbnb-base": "^13.1.0", 29 | "eslint-plugin-import": "^2.14.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/sample.env: -------------------------------------------------------------------------------- 1 | ## DB 2 | # Local 3 | DB_URL=mongodb://localhost/issuetracker 4 | 5 | # Atlas - replace UUU: user, PPP: password, XXX: hostname 6 | # DB_URL=mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true 7 | 8 | # mLab - replace UUU: user, PPP: password, XXX: hostname, YYY: port 9 | # DB_URL=mongodb://UUU:PPP@XXX.mlab.com:YYY/issuetracker 10 | 11 | 12 | ## Server Port 13 | API_SERVER_PORT=3000 14 | 15 | ## Enable CORS (default: true) 16 | # ENABLE_CORS=false 17 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar GraphQLDate 2 | 3 | enum StatusType { 4 | New 5 | Assigned 6 | Fixed 7 | Closed 8 | } 9 | 10 | type Issue { 11 | _id: ID! 12 | id: Int! 13 | title: String! 14 | status: StatusType! 15 | owner: String 16 | effort: Int 17 | created: GraphQLDate! 18 | due: GraphQLDate 19 | } 20 | 21 | "Toned down Issue, used as inputs, without server generated values." 22 | input IssueInputs { 23 | title: String! 24 | "Optional, if not supplied, will be set to 'New'" 25 | status: StatusType = New 26 | owner: String 27 | effort: Int 28 | due: GraphQLDate 29 | } 30 | 31 | ##### Top level declarations 32 | 33 | type Query { 34 | about: String! 35 | issueList: [Issue!]! 36 | } 37 | 38 | type Mutation { 39 | setAboutMessage(message: String!): String 40 | issueAdd(issue: IssueInputs!): Issue! 41 | } 42 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/scripts/init.mongo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Run using the mongo shell. For remote databases, ensure that the 3 | * connection string is supplied in the command line. For example: 4 | * localhost: 5 | * mongo issuetracker scripts/init.mongo.js 6 | * Atlas: 7 | * mongo mongodb+srv://user:pwd@xxx.mongodb.net/issuetracker scripts/init.mongo.js 8 | * MLab: 9 | * mongo mongodb://user:pwd@xxx.mlab.com:33533/issuetracker scripts/init.mongo.js 10 | */ 11 | 12 | /* global db print */ 13 | /* eslint no-restricted-globals: "off" */ 14 | 15 | db.issues.remove({}); 16 | 17 | const issuesDB = [ 18 | { 19 | id: 1, 20 | status: 'New', 21 | owner: 'Ravan', 22 | effort: 5, 23 | created: new Date('2019-01-15'), 24 | due: undefined, 25 | title: 'Error in console when clicking Add', 26 | }, 27 | { 28 | id: 2, 29 | status: 'Assigned', 30 | owner: 'Eddie', 31 | effort: 14, 32 | created: new Date('2019-01-16'), 33 | due: new Date('2019-02-01'), 34 | title: 'Missing bottom border on panel', 35 | }, 36 | ]; 37 | 38 | db.issues.insertMany(issuesDB); 39 | const count = db.issues.count(); 40 | print('Inserted', count, 'issues'); 41 | 42 | db.counters.remove({ _id: 'issues' }); 43 | db.counters.insert({ _id: 'issues', current: count }); 44 | 45 | db.issues.createIndex({ id: 1 }, { unique: true }); 46 | db.issues.createIndex({ status: 1 }); 47 | db.issues.createIndex({ owner: 1 }); 48 | db.issues.createIndex({ created: 1 }); 49 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/api/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const { connectToDb } = require('./db.js'); 4 | const { installHandler } = require('./api_handler.js'); 5 | 6 | const app = express(); 7 | 8 | installHandler(app); 9 | 10 | const port = process.env.API_SERVER_PORT || 3000; 11 | 12 | (async function start() { 13 | try { 14 | await connectToDb(); 15 | app.listen(port, () => { 16 | console.log(`API server started on port ${port}`); 17 | }); 18 | } catch (err) { 19 | console.log('ERROR:', err); 20 | } 21 | }()); 22 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/sample.env: -------------------------------------------------------------------------------- 1 | UI_SERVER_PORT=8000 2 | UI_API_ENDPOINT=http://localhost:3000/graphql 3 | # API_PROXY_TARGET=http://localhost:3000 4 | # ENABLE_HMR=true 5 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "import/extensions": [ "error", "always", { "ignorePackages": true } ], 8 | "react/prop-types": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/src/App.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import 'whatwg-fetch'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import IssueList from './IssueList.jsx'; 7 | 8 | const element = ; 9 | 10 | ReactDOM.render(element, document.getElementById('contents')); 11 | 12 | if (module.hot) { 13 | module.hot.accept(); 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/src/IssueAdd.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class IssueAdd extends React.Component { 5 | constructor() { 6 | super(); 7 | this.handleSubmit = this.handleSubmit.bind(this); 8 | } 9 | 10 | handleSubmit(e) { 11 | e.preventDefault(); 12 | const form = document.forms.issueAdd; 13 | const issue = { 14 | owner: form.owner.value, 15 | title: form.title.value, 16 | due: new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 10), 17 | }; 18 | const { createIssue } = this.props; 19 | createIssue(issue); 20 | form.owner.value = ''; form.title.value = ''; 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 27 | 28 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | IssueAdd.propTypes = { 35 | createIssue: PropTypes.func.isRequired, 36 | }; 37 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/src/IssueFilter.jsx: -------------------------------------------------------------------------------- 1 | /* eslint "react/prefer-stateless-function": "off" */ 2 | 3 | import React from 'react'; 4 | 5 | export default class IssueFilter extends React.Component { 6 | render() { 7 | return ( 8 |
This is a placeholder for the issue filter.
9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/src/IssueList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import IssueFilter from './IssueFilter.jsx'; 4 | import IssueTable from './IssueTable.jsx'; 5 | import IssueAdd from './IssueAdd.jsx'; 6 | import graphQLFetch from './graphQLFetch.js'; 7 | 8 | export default class IssueList extends React.Component { 9 | constructor() { 10 | super(); 11 | this.state = { issues: [] }; 12 | this.createIssue = this.createIssue.bind(this); 13 | } 14 | 15 | componentDidMount() { 16 | this.loadData(); 17 | } 18 | 19 | async loadData() { 20 | const query = `query { 21 | issueList { 22 | id title status owner 23 | created effort due 24 | } 25 | }`; 26 | 27 | const data = await graphQLFetch(query); 28 | if (data) { 29 | this.setState({ issues: data.issueList }); 30 | } 31 | } 32 | 33 | async createIssue(issue) { 34 | const query = `mutation issueAdd($issue: IssueInputs!) { 35 | issueAdd(issue: $issue) { 36 | id 37 | } 38 | }`; 39 | 40 | const data = await graphQLFetch(query, { issue }); 41 | if (data) { 42 | this.loadData(); 43 | } 44 | } 45 | 46 | render() { 47 | const { issues } = this.state; 48 | return ( 49 | 50 |

Issue Tracker

51 | 52 |
53 | 54 |
55 | 56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/src/IssueTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function IssueRow({ issue }) { 4 | return ( 5 | 6 | {issue.id} 7 | {issue.status} 8 | {issue.owner} 9 | {issue.created.toDateString()} 10 | {issue.effort} 11 | {issue.due ? issue.due.toDateString() : ''} 12 | {issue.title} 13 | 14 | ); 15 | } 16 | 17 | export default function IssueTable({ issues }) { 18 | const issueRows = issues.map(issue => ( 19 | 20 | )); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {issueRows} 37 | 38 |
IDStatusOwnerCreatedEffortDue DateTitle
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/src/graphQLFetch.js: -------------------------------------------------------------------------------- 1 | /* eslint "no-alert": "off" */ 2 | 3 | const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d'); 4 | 5 | function jsonDateReviver(key, value) { 6 | if (dateRegex.test(value)) return new Date(value); 7 | return value; 8 | } 9 | 10 | export default async function graphQLFetch(query, variables = {}) { 11 | try { 12 | const response = await fetch(window.ENV.UI_API_ENDPOINT, { 13 | method: 'POST', 14 | headers: { 'Content-Type': 'application/json' }, 15 | body: JSON.stringify({ query, variables }), 16 | }); 17 | const body = await response.text(); 18 | const result = JSON.parse(body, jsonDateReviver); 19 | 20 | if (result.errors) { 21 | const error = result.errors[0]; 22 | if (error.extensions.code === 'BAD_USER_INPUT') { 23 | const details = error.extensions.exception.errors.join('\n '); 24 | alert(`${error.message}:\n ${details}`); 25 | } else { 26 | alert(`${error.extensions.code}: ${error.message}`); 27 | } 28 | } 29 | return result.data; 30 | } catch (e) { 31 | alert(`Error in sending data to server: ${e.message}`); 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pro-mern-stack-2-08.06-debugging/ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { app: ['./src/App.jsx'] }, 6 | output: { 7 | filename: '[name].bundle.js', 8 | path: path.resolve(__dirname, 'public'), 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.jsx?$/, 14 | exclude: /node_modules/, 15 | use: 'babel-loader', 16 | }, 17 | ], 18 | }, 19 | optimization: { 20 | splitChunks: { 21 | name: 'vendor', 22 | chunks: 'all', 23 | }, 24 | }, 25 | devtool: 'source-map', 26 | }; 27 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | ui/public/*.js 4 | ui/public/*.map 5 | .env 6 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": "true" 5 | }, 6 | rules: { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/about.js: -------------------------------------------------------------------------------- 1 | let aboutMessage = 'Issue Tracker API v1.0'; 2 | 3 | function setMessage(_, { message }) { 4 | aboutMessage = message; 5 | return aboutMessage; 6 | } 7 | 8 | function getMessage() { 9 | return aboutMessage; 10 | } 11 | 12 | module.exports = { getMessage, setMessage }; 13 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/api_handler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | require('dotenv').config(); 3 | const { ApolloServer } = require('apollo-server-express'); 4 | 5 | const GraphQLDate = require('./graphql_date.js'); 6 | const about = require('./about.js'); 7 | const issue = require('./issue.js'); 8 | 9 | const resolvers = { 10 | Query: { 11 | about: about.getMessage, 12 | issueList: issue.list, 13 | issue: issue.get, 14 | }, 15 | Mutation: { 16 | setAboutMessage: about.setMessage, 17 | issueAdd: issue.add, 18 | }, 19 | GraphQLDate, 20 | }; 21 | 22 | const server = new ApolloServer({ 23 | typeDefs: fs.readFileSync('schema.graphql', 'utf-8'), 24 | resolvers, 25 | formatError: (error) => { 26 | console.log(error); 27 | return error; 28 | }, 29 | }); 30 | 31 | function installHandler(app) { 32 | const enableCors = (process.env.ENABLE_CORS || 'true') === 'true'; 33 | console.log('CORS setting:', enableCors); 34 | server.applyMiddleware({ app, path: '/graphql', cors: enableCors }); 35 | } 36 | 37 | module.exports = { installHandler }; 38 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { MongoClient } = require('mongodb'); 3 | 4 | let db; 5 | 6 | async function connectToDb() { 7 | const url = process.env.DB_URL || 'mongodb://localhost/issuetracker'; 8 | const client = new MongoClient(url, { useNewUrlParser: true }); 9 | await client.connect(); 10 | console.log('Connected to MongoDB at', url); 11 | db = client.db(); 12 | } 13 | 14 | async function getNextSequence(name) { 15 | const result = await db.collection('counters').findOneAndUpdate( 16 | { _id: name }, 17 | { $inc: { current: 1 } }, 18 | { returnOriginal: false }, 19 | ); 20 | return result.value.current; 21 | } 22 | 23 | function getDb() { 24 | return db; 25 | } 26 | 27 | module.exports = { connectToDb, getNextSequence, getDb }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/graphql_date.js: -------------------------------------------------------------------------------- 1 | const { GraphQLScalarType } = require('graphql'); 2 | const { Kind } = require('graphql/language'); 3 | 4 | const GraphQLDate = new GraphQLScalarType({ 5 | name: 'GraphQLDate', 6 | description: 'A Date() type in GraphQL as a scalar', 7 | serialize(value) { 8 | return value.toISOString(); 9 | }, 10 | parseValue(value) { 11 | const dateValue = new Date(value); 12 | return Number.isNaN(dateValue.getTime()) ? undefined : dateValue; 13 | }, 14 | parseLiteral(ast) { 15 | if (ast.kind === Kind.STRING) { 16 | const value = new Date(ast.value); 17 | return Number.isNaN(value.getTime()) ? undefined : value; 18 | } 19 | return undefined; 20 | }, 21 | }); 22 | 23 | module.exports = GraphQLDate; 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/issue.js: -------------------------------------------------------------------------------- 1 | const { UserInputError } = require('apollo-server-express'); 2 | const { getDb, getNextSequence } = require('./db.js'); 3 | 4 | async function get(_, { id }) { 5 | const db = getDb(); 6 | const issue = await db.collection('issues').findOne({ id }); 7 | return issue; 8 | } 9 | 10 | async function list(_, { status }) { 11 | const db = getDb(); 12 | const filter = {}; 13 | if (status) filter.status = status; 14 | const issues = await db.collection('issues').find(filter).toArray(); 15 | return issues; 16 | } 17 | 18 | function validate(issue) { 19 | const errors = []; 20 | if (issue.title.length < 3) { 21 | errors.push('Field "title" must be at least 3 characters long.'); 22 | } 23 | if (issue.status === 'Assigned' && !issue.owner) { 24 | errors.push('Field "owner" is required when status is "Assigned"'); 25 | } 26 | if (errors.length > 0) { 27 | throw new UserInputError('Invalid input(s)', { errors }); 28 | } 29 | } 30 | 31 | async function add(_, { issue }) { 32 | const db = getDb(); 33 | validate(issue); 34 | 35 | const newIssue = Object.assign({}, issue); 36 | newIssue.created = new Date(); 37 | newIssue.id = await getNextSequence('issues'); 38 | 39 | const result = await db.collection('issues').insertOne(newIssue); 40 | const savedIssue = await db.collection('issues') 41 | .findOne({ _id: result.insertedId }); 42 | return savedIssue; 43 | } 44 | 45 | module.exports = { list, add, get }; 46 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-api", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -e js,graphql -w . -w .env server.js", 8 | "lint": "eslint .", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 14 | }, 15 | "author": "vasan.promern@gmail.com", 16 | "license": "ISC", 17 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 18 | "dependencies": { 19 | "apollo-server-express": "^2.3.1", 20 | "dotenv": "^6.2.0", 21 | "express": "^4.16.4", 22 | "graphql": "^0.13.2", 23 | "mongodb": "^3.1.10", 24 | "nodemon": "^1.18.9" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^5.12.0", 28 | "eslint-config-airbnb-base": "^13.1.0", 29 | "eslint-plugin-import": "^2.14.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/sample.env: -------------------------------------------------------------------------------- 1 | ## DB 2 | # Local 3 | DB_URL=mongodb://localhost/issuetracker 4 | 5 | # Atlas - replace UUU: user, PPP: password, XXX: hostname 6 | # DB_URL=mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true 7 | 8 | # mLab - replace UUU: user, PPP: password, XXX: hostname, YYY: port 9 | # DB_URL=mongodb://UUU:PPP@XXX.mlab.com:YYY/issuetracker 10 | 11 | 12 | ## Server Port 13 | API_SERVER_PORT=3000 14 | 15 | ## Enable CORS (default: true) 16 | # ENABLE_CORS=false 17 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar GraphQLDate 2 | 3 | enum StatusType { 4 | New 5 | Assigned 6 | Fixed 7 | Closed 8 | } 9 | 10 | type Issue { 11 | _id: ID! 12 | id: Int! 13 | title: String! 14 | status: StatusType! 15 | owner: String 16 | effort: Int 17 | created: GraphQLDate! 18 | due: GraphQLDate 19 | description: String 20 | } 21 | 22 | "Toned down Issue, used as inputs, without server generated values." 23 | input IssueInputs { 24 | title: String! 25 | "Optional, if not supplied, will be set to 'New'" 26 | status: StatusType = New 27 | owner: String 28 | effort: Int 29 | due: GraphQLDate 30 | description: String 31 | } 32 | 33 | ##### Top level declarations 34 | 35 | type Query { 36 | about: String! 37 | issueList(status: StatusType): [Issue!]! 38 | issue(id: Int!): Issue! 39 | } 40 | 41 | type Mutation { 42 | setAboutMessage(message: String!): String 43 | issueAdd(issue: IssueInputs!): Issue! 44 | } 45 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/api/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const { connectToDb } = require('./db.js'); 4 | const { installHandler } = require('./api_handler.js'); 5 | 6 | const app = express(); 7 | 8 | installHandler(app); 9 | 10 | const port = process.env.API_SERVER_PORT || 3000; 11 | 12 | (async function start() { 13 | try { 14 | await connectToDb(); 15 | app.listen(port, () => { 16 | console.log(`API server started on port ${port}`); 17 | }); 18 | } catch (err) { 19 | console.log('ERROR:', err); 20 | } 21 | }()); 22 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/sample.env: -------------------------------------------------------------------------------- 1 | UI_SERVER_PORT=8000 2 | UI_API_ENDPOINT=http://localhost:3000/graphql 3 | # API_PROXY_TARGET=http://localhost:3000 4 | # ENABLE_HMR=true 5 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "import/extensions": [ "error", "always", { "ignorePackages": true } ], 8 | "react/prop-types": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/App.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import 'whatwg-fetch'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | 7 | import Page from './Page.jsx'; 8 | 9 | const element = ( 10 | 11 | 12 | 13 | ); 14 | 15 | ReactDOM.render(element, document.getElementById('contents')); 16 | 17 | if (module.hot) { 18 | module.hot.accept(); 19 | } 20 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/Contents.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | 4 | import IssueList from './IssueList.jsx'; 5 | import IssueReport from './IssueReport.jsx'; 6 | import IssueEdit from './IssueEdit.jsx'; 7 | 8 | const NotFound = () =>

Page Not Found

; 9 | 10 | export default function Contents() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/IssueAdd.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class IssueAdd extends React.Component { 5 | constructor() { 6 | super(); 7 | this.handleSubmit = this.handleSubmit.bind(this); 8 | } 9 | 10 | handleSubmit(e) { 11 | e.preventDefault(); 12 | const form = document.forms.issueAdd; 13 | const issue = { 14 | owner: form.owner.value, 15 | title: form.title.value, 16 | due: new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 10), 17 | }; 18 | const { createIssue } = this.props; 19 | createIssue(issue); 20 | form.owner.value = ''; form.title.value = ''; 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 27 | 28 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | IssueAdd.propTypes = { 35 | createIssue: PropTypes.func.isRequired, 36 | }; 37 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/IssueDetail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import graphQLFetch from './graphQLFetch.js'; 4 | 5 | export default class IssueDetail extends React.Component { 6 | constructor() { 7 | super(); 8 | this.state = { issue: {} }; 9 | } 10 | 11 | componentDidMount() { 12 | this.loadData(); 13 | } 14 | 15 | componentDidUpdate(prevProps) { 16 | const { match: { params: { id: prevId } } } = prevProps; 17 | const { match: { params: { id } } } = this.props; 18 | if (prevId !== id) { 19 | this.loadData(); 20 | } 21 | } 22 | 23 | async loadData() { 24 | const { match: { params: { id } } } = this.props; 25 | const query = `query issue($id: Int!) { 26 | issue (id: $id) { 27 | id description 28 | } 29 | }`; 30 | 31 | const data = await graphQLFetch(query, { id }); 32 | if (data) { 33 | this.setState({ issue: data.issue }); 34 | } else { 35 | this.setState({ issue: {} }); 36 | } 37 | } 38 | 39 | render() { 40 | const { issue: { description } } = this.state; 41 | return ( 42 |
43 |

Description

44 |
{description}
45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/IssueEdit.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IssueEdit({ match }) { 4 | const { id } = match.params; 5 | return ( 6 |

{`This is a placeholder for editing issue ${id}`}

7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/IssueFilter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | class IssueFilter extends React.Component { 5 | constructor() { 6 | super(); 7 | this.onChangeStatus = this.onChangeStatus.bind(this); 8 | } 9 | 10 | onChangeStatus(e) { 11 | const status = e.target.value; 12 | const { history } = this.props; 13 | history.push({ 14 | pathname: '/issues', 15 | search: status ? `?status=${status}` : '', 16 | }); 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 | Status: 23 | {' '} 24 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | export default withRouter(IssueFilter); 37 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/IssueReport.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IssueReport() { 4 | return ( 5 |
6 |

This is a placeholder for the Issue Report

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/IssueTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, NavLink, withRouter } from 'react-router-dom'; 3 | 4 | const IssueRow = withRouter(({ issue, location: { search } }) => { 5 | const selectLocation = { pathname: `/issues/${issue.id}`, search }; 6 | return ( 7 | 8 | {issue.id} 9 | {issue.status} 10 | {issue.owner} 11 | {issue.created.toDateString()} 12 | {issue.effort} 13 | {issue.due ? issue.due.toDateString() : ''} 14 | {issue.title} 15 | 16 | Edit 17 | {' | '} 18 | Select 19 | 20 | 21 | ); 22 | }); 23 | 24 | export default function IssueTable({ issues }) { 25 | const issueRows = issues.map(issue => ( 26 | 27 | )); 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {issueRows} 45 | 46 |
IDStatusOwnerCreatedEffortDue DateTitleAction
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import Contents from './Contents.jsx'; 5 | 6 | function NavBar() { 7 | return ( 8 | 15 | ); 16 | } 17 | 18 | export default function Page() { 19 | return ( 20 |
21 | 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/src/graphQLFetch.js: -------------------------------------------------------------------------------- 1 | /* eslint "no-alert": "off" */ 2 | 3 | const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d'); 4 | 5 | function jsonDateReviver(key, value) { 6 | if (dateRegex.test(value)) return new Date(value); 7 | return value; 8 | } 9 | 10 | export default async function graphQLFetch(query, variables = {}) { 11 | try { 12 | const response = await fetch(window.ENV.UI_API_ENDPOINT, { 13 | method: 'POST', 14 | headers: { 'Content-Type': 'application/json' }, 15 | body: JSON.stringify({ query, variables }), 16 | }); 17 | const body = await response.text(); 18 | const result = JSON.parse(body, jsonDateReviver); 19 | 20 | if (result.errors) { 21 | const error = result.errors[0]; 22 | if (error.extensions.code === 'BAD_USER_INPUT') { 23 | const details = error.extensions.exception.errors.join('\n '); 24 | alert(`${error.message}:\n ${details}`); 25 | } else { 26 | alert(`${error.extensions.code}: ${error.message}`); 27 | } 28 | } 29 | return result.data; 30 | } catch (e) { 31 | alert(`Error in sending data to server: ${e.message}`); 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pro-mern-stack-2-09.07-browser-history-router/ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { app: ['./src/App.jsx'] }, 6 | output: { 7 | filename: '[name].bundle.js', 8 | path: path.resolve(__dirname, 'public'), 9 | publicPath: '/', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | use: 'babel-loader', 17 | }, 18 | ], 19 | }, 20 | optimization: { 21 | splitChunks: { 22 | name: 'vendor', 23 | chunks: 'all', 24 | }, 25 | }, 26 | devtool: 'source-map', 27 | }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | ui/public/*.js 4 | ui/public/*.map 5 | .env 6 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": "true" 5 | }, 6 | rules: { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/api/about.js: -------------------------------------------------------------------------------- 1 | let aboutMessage = 'Issue Tracker API v1.0'; 2 | 3 | function setMessage(_, { message }) { 4 | aboutMessage = message; 5 | return aboutMessage; 6 | } 7 | 8 | function getMessage() { 9 | return aboutMessage; 10 | } 11 | 12 | module.exports = { getMessage, setMessage }; 13 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/api/api_handler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | require('dotenv').config(); 3 | const { ApolloServer } = require('apollo-server-express'); 4 | 5 | const GraphQLDate = require('./graphql_date.js'); 6 | const about = require('./about.js'); 7 | const issue = require('./issue.js'); 8 | 9 | const resolvers = { 10 | Query: { 11 | about: about.getMessage, 12 | issueList: issue.list, 13 | issue: issue.get, 14 | }, 15 | Mutation: { 16 | setAboutMessage: about.setMessage, 17 | issueAdd: issue.add, 18 | issueUpdate: issue.update, 19 | issueDelete: issue.delete, 20 | }, 21 | GraphQLDate, 22 | }; 23 | 24 | const server = new ApolloServer({ 25 | typeDefs: fs.readFileSync('schema.graphql', 'utf-8'), 26 | resolvers, 27 | formatError: (error) => { 28 | console.log(error); 29 | return error; 30 | }, 31 | }); 32 | 33 | function installHandler(app) { 34 | const enableCors = (process.env.ENABLE_CORS || 'true') === 'true'; 35 | console.log('CORS setting:', enableCors); 36 | server.applyMiddleware({ app, path: '/graphql', cors: enableCors }); 37 | } 38 | 39 | module.exports = { installHandler }; 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/api/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { MongoClient } = require('mongodb'); 3 | 4 | let db; 5 | 6 | async function connectToDb() { 7 | const url = process.env.DB_URL || 'mongodb://localhost/issuetracker'; 8 | const client = new MongoClient(url, { useNewUrlParser: true }); 9 | await client.connect(); 10 | console.log('Connected to MongoDB at', url); 11 | db = client.db(); 12 | } 13 | 14 | async function getNextSequence(name) { 15 | const result = await db.collection('counters').findOneAndUpdate( 16 | { _id: name }, 17 | { $inc: { current: 1 } }, 18 | { returnOriginal: false }, 19 | ); 20 | return result.value.current; 21 | } 22 | 23 | function getDb() { 24 | return db; 25 | } 26 | 27 | module.exports = { connectToDb, getNextSequence, getDb }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/api/graphql_date.js: -------------------------------------------------------------------------------- 1 | const { GraphQLScalarType } = require('graphql'); 2 | const { Kind } = require('graphql/language'); 3 | 4 | const GraphQLDate = new GraphQLScalarType({ 5 | name: 'GraphQLDate', 6 | description: 'A Date() type in GraphQL as a scalar', 7 | serialize(value) { 8 | return value.toISOString(); 9 | }, 10 | parseValue(value) { 11 | const dateValue = new Date(value); 12 | return Number.isNaN(dateValue.getTime()) ? undefined : dateValue; 13 | }, 14 | parseLiteral(ast) { 15 | if (ast.kind === Kind.STRING) { 16 | const value = new Date(ast.value); 17 | return Number.isNaN(value.getTime()) ? undefined : value; 18 | } 19 | return undefined; 20 | }, 21 | }); 22 | 23 | module.exports = GraphQLDate; 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-api", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -e js,graphql -w . -w .env server.js", 8 | "lint": "eslint .", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 14 | }, 15 | "author": "vasan.promern@gmail.com", 16 | "license": "ISC", 17 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 18 | "dependencies": { 19 | "apollo-server-express": "^2.3.1", 20 | "dotenv": "^6.2.0", 21 | "express": "^4.16.4", 22 | "graphql": "^0.13.2", 23 | "mongodb": "^3.1.10", 24 | "nodemon": "^1.18.9" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^5.12.0", 28 | "eslint-config-airbnb-base": "^13.1.0", 29 | "eslint-plugin-import": "^2.14.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/api/sample.env: -------------------------------------------------------------------------------- 1 | ## DB 2 | # Local 3 | DB_URL=mongodb://localhost/issuetracker 4 | 5 | # Atlas - replace UUU: user, PPP: password, XXX: hostname 6 | # DB_URL=mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true 7 | 8 | # mLab - replace UUU: user, PPP: password, XXX: hostname, YYY: port 9 | # DB_URL=mongodb://UUU:PPP@XXX.mlab.com:YYY/issuetracker 10 | 11 | 12 | ## Server Port 13 | API_SERVER_PORT=3000 14 | 15 | ## Enable CORS (default: true) 16 | # ENABLE_CORS=false 17 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/api/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar GraphQLDate 2 | 3 | enum StatusType { 4 | New 5 | Assigned 6 | Fixed 7 | Closed 8 | } 9 | 10 | type Issue { 11 | _id: ID! 12 | id: Int! 13 | title: String! 14 | status: StatusType! 15 | owner: String 16 | effort: Int 17 | created: GraphQLDate! 18 | due: GraphQLDate 19 | description: String 20 | } 21 | 22 | "Toned down Issue, used as inputs, without server generated values." 23 | input IssueInputs { 24 | title: String! 25 | "Optional, if not supplied, will be set to 'New'" 26 | status: StatusType = New 27 | owner: String 28 | effort: Int 29 | due: GraphQLDate 30 | description: String 31 | } 32 | 33 | """Inputs for issueUpdate: all are optional. Whichever is specified will 34 | be set to the given value, undefined fields will remain unmodified.""" 35 | input IssueUpdateInputs { 36 | title: String 37 | status: StatusType 38 | owner: String 39 | effort: Int 40 | due: GraphQLDate 41 | description: String 42 | } 43 | 44 | ##### Top level declarations 45 | 46 | type Query { 47 | about: String! 48 | issueList( 49 | status: StatusType 50 | effortMin: Int 51 | effortMax: Int 52 | ): [Issue!]! 53 | issue(id: Int!): Issue! 54 | } 55 | 56 | type Mutation { 57 | setAboutMessage(message: String!): String 58 | issueAdd(issue: IssueInputs!): Issue! 59 | issueUpdate(id: Int!, changes: IssueUpdateInputs!): Issue! 60 | issueDelete(id: Int!): Boolean! 61 | } 62 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/api/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const { connectToDb } = require('./db.js'); 4 | const { installHandler } = require('./api_handler.js'); 5 | 6 | const app = express(); 7 | 8 | installHandler(app); 9 | 10 | const port = process.env.API_SERVER_PORT || 3000; 11 | 12 | (async function start() { 13 | try { 14 | await connectToDb(); 15 | app.listen(port, () => { 16 | console.log(`API server started on port ${port}`); 17 | }); 18 | } catch (err) { 19 | console.log('ERROR:', err); 20 | } 21 | }()); 22 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/sample.env: -------------------------------------------------------------------------------- 1 | UI_SERVER_PORT=8000 2 | UI_API_ENDPOINT=http://localhost:3000/graphql 3 | # API_PROXY_TARGET=http://localhost:3000 4 | # ENABLE_HMR=true 5 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "import/extensions": [ "error", "always", { "ignorePackages": true } ], 8 | "react/prop-types": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/App.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import 'whatwg-fetch'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | 7 | import Page from './Page.jsx'; 8 | 9 | const element = ( 10 | 11 | 12 | 13 | ); 14 | 15 | ReactDOM.render(element, document.getElementById('contents')); 16 | 17 | if (module.hot) { 18 | module.hot.accept(); 19 | } 20 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/Contents.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | 4 | import IssueList from './IssueList.jsx'; 5 | import IssueReport from './IssueReport.jsx'; 6 | import IssueEdit from './IssueEdit.jsx'; 7 | 8 | const NotFound = () =>

Page Not Found

; 9 | 10 | export default function Contents() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/IssueAdd.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class IssueAdd extends React.Component { 5 | constructor() { 6 | super(); 7 | this.handleSubmit = this.handleSubmit.bind(this); 8 | } 9 | 10 | handleSubmit(e) { 11 | e.preventDefault(); 12 | const form = document.forms.issueAdd; 13 | const issue = { 14 | owner: form.owner.value, 15 | title: form.title.value, 16 | due: new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 10), 17 | }; 18 | const { createIssue } = this.props; 19 | createIssue(issue); 20 | form.owner.value = ''; form.title.value = ''; 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 27 | 28 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | IssueAdd.propTypes = { 35 | createIssue: PropTypes.func.isRequired, 36 | }; 37 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/IssueDetail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import graphQLFetch from './graphQLFetch.js'; 4 | 5 | export default class IssueDetail extends React.Component { 6 | constructor() { 7 | super(); 8 | this.state = { issue: {} }; 9 | } 10 | 11 | componentDidMount() { 12 | this.loadData(); 13 | } 14 | 15 | componentDidUpdate(prevProps) { 16 | const { match: { params: { id: prevId } } } = prevProps; 17 | const { match: { params: { id } } } = this.props; 18 | if (prevId !== id) { 19 | this.loadData(); 20 | } 21 | } 22 | 23 | async loadData() { 24 | const { match: { params: { id } } } = this.props; 25 | const query = `query issue($id: Int!) { 26 | issue (id: $id) { 27 | id description 28 | } 29 | }`; 30 | 31 | const data = await graphQLFetch(query, { id }); 32 | if (data) { 33 | this.setState({ issue: data.issue }); 34 | } else { 35 | this.setState({ issue: {} }); 36 | } 37 | } 38 | 39 | render() { 40 | const { issue: { description } } = this.state; 41 | return ( 42 |
43 |

Description

44 |
{description}
45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/IssueReport.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IssueReport() { 4 | return ( 5 |
6 |

This is a placeholder for the Issue Report

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/NumInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(num) { 4 | return num != null ? num.toString() : ''; 5 | } 6 | 7 | function unformat(str) { 8 | const val = parseInt(str, 10); 9 | return Number.isNaN(val) ? null : val; 10 | } 11 | 12 | export default class NumInput extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { value: format(props.value) }; 16 | this.onBlur = this.onBlur.bind(this); 17 | this.onChange = this.onChange.bind(this); 18 | } 19 | 20 | onChange(e) { 21 | if (e.target.value.match(/^\d*$/)) { 22 | this.setState({ value: e.target.value }); 23 | } 24 | } 25 | 26 | onBlur(e) { 27 | const { onChange } = this.props; 28 | const { value } = this.state; 29 | onChange(e, unformat(value)); 30 | } 31 | 32 | render() { 33 | const { value } = this.state; 34 | return ( 35 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import Contents from './Contents.jsx'; 5 | 6 | function NavBar() { 7 | return ( 8 | 15 | ); 16 | } 17 | 18 | export default function Page() { 19 | return ( 20 |
21 | 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(text) { 4 | return text != null ? text : ''; 5 | } 6 | 7 | function unformat(text) { 8 | return text.trim().length === 0 ? null : text; 9 | } 10 | 11 | export default class TextInput extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { value: format(props.value) }; 15 | this.onBlur = this.onBlur.bind(this); 16 | this.onChange = this.onChange.bind(this); 17 | } 18 | 19 | onChange(e) { 20 | this.setState({ value: e.target.value }); 21 | } 22 | 23 | onBlur(e) { 24 | const { onChange } = this.props; 25 | const { value } = this.state; 26 | onChange(e, unformat(value)); 27 | } 28 | 29 | render() { 30 | const { value } = this.state; 31 | const { tag = 'input', ...props } = this.props; 32 | return React.createElement(tag, { 33 | ...props, 34 | value, 35 | onBlur: this.onBlur, 36 | onChange: this.onChange, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/src/graphQLFetch.js: -------------------------------------------------------------------------------- 1 | /* eslint "no-alert": "off" */ 2 | 3 | const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d'); 4 | 5 | function jsonDateReviver(key, value) { 6 | if (dateRegex.test(value)) return new Date(value); 7 | return value; 8 | } 9 | 10 | export default async function graphQLFetch(query, variables = {}) { 11 | try { 12 | const response = await fetch(window.ENV.UI_API_ENDPOINT, { 13 | method: 'POST', 14 | headers: { 'Content-Type': 'application/json' }, 15 | body: JSON.stringify({ query, variables }), 16 | }); 17 | const body = await response.text(); 18 | const result = JSON.parse(body, jsonDateReviver); 19 | 20 | if (result.errors) { 21 | const error = result.errors[0]; 22 | if (error.extensions.code === 'BAD_USER_INPUT') { 23 | const details = error.extensions.exception.errors.join('\n '); 24 | alert(`${error.message}:\n ${details}`); 25 | } else { 26 | alert(`${error.extensions.code}: ${error.message}`); 27 | } 28 | } 29 | return result.data; 30 | } catch (e) { 31 | alert(`Error in sending data to server: ${e.message}`); 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pro-mern-stack-2-10.13-deleting-an-issue/ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { app: ['./src/App.jsx'] }, 6 | output: { 7 | filename: '[name].bundle.js', 8 | path: path.resolve(__dirname, 'public'), 9 | publicPath: '/', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | use: 'babel-loader', 17 | }, 18 | ], 19 | }, 20 | optimization: { 21 | splitChunks: { 22 | name: 'vendor', 23 | chunks: 'all', 24 | }, 25 | }, 26 | devtool: 'source-map', 27 | }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | ui/public/*.js 4 | ui/public/*.map 5 | .env 6 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": "true" 5 | }, 6 | rules: { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/api/about.js: -------------------------------------------------------------------------------- 1 | let aboutMessage = 'Issue Tracker API v1.0'; 2 | 3 | function setMessage(_, { message }) { 4 | aboutMessage = message; 5 | return aboutMessage; 6 | } 7 | 8 | function getMessage() { 9 | return aboutMessage; 10 | } 11 | 12 | module.exports = { getMessage, setMessage }; 13 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/api/api_handler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | require('dotenv').config(); 3 | const { ApolloServer } = require('apollo-server-express'); 4 | 5 | const GraphQLDate = require('./graphql_date.js'); 6 | const about = require('./about.js'); 7 | const issue = require('./issue.js'); 8 | 9 | const resolvers = { 10 | Query: { 11 | about: about.getMessage, 12 | issueList: issue.list, 13 | issue: issue.get, 14 | }, 15 | Mutation: { 16 | setAboutMessage: about.setMessage, 17 | issueAdd: issue.add, 18 | issueUpdate: issue.update, 19 | issueDelete: issue.delete, 20 | }, 21 | GraphQLDate, 22 | }; 23 | 24 | const server = new ApolloServer({ 25 | typeDefs: fs.readFileSync('schema.graphql', 'utf-8'), 26 | resolvers, 27 | formatError: (error) => { 28 | console.log(error); 29 | return error; 30 | }, 31 | }); 32 | 33 | function installHandler(app) { 34 | const enableCors = (process.env.ENABLE_CORS || 'true') === 'true'; 35 | console.log('CORS setting:', enableCors); 36 | server.applyMiddleware({ app, path: '/graphql', cors: enableCors }); 37 | } 38 | 39 | module.exports = { installHandler }; 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/api/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { MongoClient } = require('mongodb'); 3 | 4 | let db; 5 | 6 | async function connectToDb() { 7 | const url = process.env.DB_URL || 'mongodb://localhost/issuetracker'; 8 | const client = new MongoClient(url, { useNewUrlParser: true }); 9 | await client.connect(); 10 | console.log('Connected to MongoDB at', url); 11 | db = client.db(); 12 | } 13 | 14 | async function getNextSequence(name) { 15 | const result = await db.collection('counters').findOneAndUpdate( 16 | { _id: name }, 17 | { $inc: { current: 1 } }, 18 | { returnOriginal: false }, 19 | ); 20 | return result.value.current; 21 | } 22 | 23 | function getDb() { 24 | return db; 25 | } 26 | 27 | module.exports = { connectToDb, getNextSequence, getDb }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/api/graphql_date.js: -------------------------------------------------------------------------------- 1 | const { GraphQLScalarType } = require('graphql'); 2 | const { Kind } = require('graphql/language'); 3 | 4 | const GraphQLDate = new GraphQLScalarType({ 5 | name: 'GraphQLDate', 6 | description: 'A Date() type in GraphQL as a scalar', 7 | serialize(value) { 8 | return value.toISOString(); 9 | }, 10 | parseValue(value) { 11 | const dateValue = new Date(value); 12 | return Number.isNaN(dateValue.getTime()) ? undefined : dateValue; 13 | }, 14 | parseLiteral(ast) { 15 | if (ast.kind === Kind.STRING) { 16 | const value = new Date(ast.value); 17 | return Number.isNaN(value.getTime()) ? undefined : value; 18 | } 19 | return undefined; 20 | }, 21 | }); 22 | 23 | module.exports = GraphQLDate; 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-api", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -e js,graphql -w . -w .env server.js", 8 | "lint": "eslint .", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 14 | }, 15 | "author": "vasan.promern@gmail.com", 16 | "license": "ISC", 17 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 18 | "dependencies": { 19 | "apollo-server-express": "^2.3.1", 20 | "dotenv": "^6.2.0", 21 | "express": "^4.16.4", 22 | "graphql": "^0.13.2", 23 | "mongodb": "^3.1.10", 24 | "nodemon": "^1.18.9" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^5.12.0", 28 | "eslint-config-airbnb-base": "^13.1.0", 29 | "eslint-plugin-import": "^2.14.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/api/sample.env: -------------------------------------------------------------------------------- 1 | ## DB 2 | # Local 3 | DB_URL=mongodb://localhost/issuetracker 4 | 5 | # Atlas - replace UUU: user, PPP: password, XXX: hostname 6 | # DB_URL=mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true 7 | 8 | # mLab - replace UUU: user, PPP: password, XXX: hostname, YYY: port 9 | # DB_URL=mongodb://UUU:PPP@XXX.mlab.com:YYY/issuetracker 10 | 11 | 12 | ## Server Port 13 | API_SERVER_PORT=3000 14 | 15 | ## Enable CORS (default: true) 16 | # ENABLE_CORS=false 17 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/api/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar GraphQLDate 2 | 3 | enum StatusType { 4 | New 5 | Assigned 6 | Fixed 7 | Closed 8 | } 9 | 10 | type Issue { 11 | _id: ID! 12 | id: Int! 13 | title: String! 14 | status: StatusType! 15 | owner: String 16 | effort: Int 17 | created: GraphQLDate! 18 | due: GraphQLDate 19 | description: String 20 | } 21 | 22 | "Toned down Issue, used as inputs, without server generated values." 23 | input IssueInputs { 24 | title: String! 25 | "Optional, if not supplied, will be set to 'New'" 26 | status: StatusType = New 27 | owner: String 28 | effort: Int 29 | due: GraphQLDate 30 | description: String 31 | } 32 | 33 | """Inputs for issueUpdate: all are optional. Whichever is specified will 34 | be set to the given value, undefined fields will remain unmodified.""" 35 | input IssueUpdateInputs { 36 | title: String 37 | status: StatusType 38 | owner: String 39 | effort: Int 40 | due: GraphQLDate 41 | description: String 42 | } 43 | 44 | ##### Top level declarations 45 | 46 | type Query { 47 | about: String! 48 | issueList( 49 | status: StatusType 50 | effortMin: Int 51 | effortMax: Int 52 | ): [Issue!]! 53 | issue(id: Int!): Issue! 54 | } 55 | 56 | type Mutation { 57 | setAboutMessage(message: String!): String 58 | issueAdd(issue: IssueInputs!): Issue! 59 | issueUpdate(id: Int!, changes: IssueUpdateInputs!): Issue! 60 | issueDelete(id: Int!): Boolean! 61 | } 62 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/api/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const { connectToDb } = require('./db.js'); 4 | const { installHandler } = require('./api_handler.js'); 5 | 6 | const app = express(); 7 | 8 | installHandler(app); 9 | 10 | const port = process.env.API_SERVER_PORT || 3000; 11 | 12 | (async function start() { 13 | try { 14 | await connectToDb(); 15 | app.listen(port, () => { 16 | console.log(`API server started on port ${port}`); 17 | }); 18 | } catch (err) { 19 | console.log('ERROR:', err); 20 | } 21 | }()); 22 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/public/bootstrap: -------------------------------------------------------------------------------- 1 | ../node_modules/bootstrap/dist -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pro MERN Stack 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/sample.env: -------------------------------------------------------------------------------- 1 | UI_SERVER_PORT=8000 2 | UI_API_ENDPOINT=http://localhost:3000/graphql 3 | # API_PROXY_TARGET=http://localhost:3000 4 | # ENABLE_HMR=true 5 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "ie": "11", 6 | "edge": "15", 7 | "safari": "10", 8 | "firefox": "50", 9 | "chrome": "49" 10 | } 11 | }], 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "import/extensions": [ "error", "always", { "ignorePackages": true } ], 8 | "react/prop-types": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/src/App.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import 'whatwg-fetch'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | 7 | import Page from './Page.jsx'; 8 | 9 | const element = ( 10 | 11 | 12 | 13 | ); 14 | 15 | ReactDOM.render(element, document.getElementById('contents')); 16 | 17 | if (module.hot) { 18 | module.hot.accept(); 19 | } 20 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/src/Contents.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | 4 | import IssueList from './IssueList.jsx'; 5 | import IssueReport from './IssueReport.jsx'; 6 | import IssueEdit from './IssueEdit.jsx'; 7 | 8 | const NotFound = () =>

Page Not Found

; 9 | 10 | export default function Contents() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/src/IssueReport.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IssueReport() { 4 | return ( 5 |
6 |

This is a placeholder for the Issue Report

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/src/NumInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(num) { 4 | return num != null ? num.toString() : ''; 5 | } 6 | 7 | function unformat(str) { 8 | const val = parseInt(str, 10); 9 | return Number.isNaN(val) ? null : val; 10 | } 11 | 12 | export default class NumInput extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { value: format(props.value) }; 16 | this.onBlur = this.onBlur.bind(this); 17 | this.onChange = this.onChange.bind(this); 18 | } 19 | 20 | onChange(e) { 21 | if (e.target.value.match(/^\d*$/)) { 22 | this.setState({ value: e.target.value }); 23 | } 24 | } 25 | 26 | onBlur(e) { 27 | const { onChange } = this.props; 28 | const { value } = this.state; 29 | onChange(e, unformat(value)); 30 | } 31 | 32 | render() { 33 | const { value } = this.state; 34 | return ( 35 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/src/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(text) { 4 | return text != null ? text : ''; 5 | } 6 | 7 | function unformat(text) { 8 | return text.trim().length === 0 ? null : text; 9 | } 10 | 11 | export default class TextInput extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { value: format(props.value) }; 15 | this.onBlur = this.onBlur.bind(this); 16 | this.onChange = this.onChange.bind(this); 17 | } 18 | 19 | onChange(e) { 20 | this.setState({ value: e.target.value }); 21 | } 22 | 23 | onBlur(e) { 24 | const { onChange } = this.props; 25 | const { value } = this.state; 26 | onChange(e, unformat(value)); 27 | } 28 | 29 | render() { 30 | const { value } = this.state; 31 | const { tag = 'input', ...props } = this.props; 32 | return React.createElement(tag, { 33 | ...props, 34 | value, 35 | onBlur: this.onBlur, 36 | onChange: this.onChange, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/src/Toast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, Collapse } from 'react-bootstrap'; 3 | 4 | export default class Toast extends React.Component { 5 | componentDidUpdate() { 6 | const { showing, onDismiss } = this.props; 7 | if (showing) { 8 | clearTimeout(this.dismissTimer); 9 | this.dismissTimer = setTimeout(onDismiss, 5000); 10 | } 11 | } 12 | 13 | componentWillUnmount() { 14 | clearTimeout(this.dismissTimer); 15 | } 16 | 17 | render() { 18 | const { 19 | showing, bsStyle, onDismiss, children, 20 | } = this.props; 21 | return ( 22 | 23 |
24 | 25 | {children} 26 | 27 |
28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/src/graphQLFetch.js: -------------------------------------------------------------------------------- 1 | const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d'); 2 | 3 | function jsonDateReviver(key, value) { 4 | if (dateRegex.test(value)) return new Date(value); 5 | return value; 6 | } 7 | 8 | export default async function 9 | graphQLFetch(query, variables = {}, showError = null) { 10 | try { 11 | const response = await fetch(window.ENV.UI_API_ENDPOINT, { 12 | method: 'POST', 13 | headers: { 'Content-Type': 'application/json' }, 14 | body: JSON.stringify({ query, variables }), 15 | }); 16 | const body = await response.text(); 17 | const result = JSON.parse(body, jsonDateReviver); 18 | 19 | if (result.errors) { 20 | const error = result.errors[0]; 21 | if (error.extensions.code === 'BAD_USER_INPUT') { 22 | const details = error.extensions.exception.errors.join('\n '); 23 | if (showError) showError(`${error.message}:\n ${details}`); 24 | } else if (showError) { 25 | showError(`${error.extensions.code}: ${error.message}`); 26 | } 27 | } 28 | return result.data; 29 | } catch (e) { 30 | if (showError) showError(`Error in sending data to server: ${e.message}`); 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pro-mern-stack-2-11.12-modals/ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { app: ['./src/App.jsx'] }, 6 | output: { 7 | filename: '[name].bundle.js', 8 | path: path.resolve(__dirname, 'public'), 9 | publicPath: '/', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | use: 'babel-loader', 17 | }, 18 | ], 19 | }, 20 | optimization: { 21 | splitChunks: { 22 | name: 'vendor', 23 | chunks: 'all', 24 | }, 25 | }, 26 | devtool: 'source-map', 27 | }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | ui/public/*.js 4 | ui/public/*.map 5 | .env 6 | ui/dist 7 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": "true" 5 | }, 6 | rules: { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/api/about.js: -------------------------------------------------------------------------------- 1 | let aboutMessage = 'Issue Tracker API v1.0'; 2 | 3 | function setMessage(_, { message }) { 4 | aboutMessage = message; 5 | return aboutMessage; 6 | } 7 | 8 | function getMessage() { 9 | return aboutMessage; 10 | } 11 | 12 | module.exports = { getMessage, setMessage }; 13 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/api/api_handler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | require('dotenv').config(); 3 | const { ApolloServer } = require('apollo-server-express'); 4 | 5 | const GraphQLDate = require('./graphql_date.js'); 6 | const about = require('./about.js'); 7 | const issue = require('./issue.js'); 8 | 9 | const resolvers = { 10 | Query: { 11 | about: about.getMessage, 12 | issueList: issue.list, 13 | issue: issue.get, 14 | }, 15 | Mutation: { 16 | setAboutMessage: about.setMessage, 17 | issueAdd: issue.add, 18 | issueUpdate: issue.update, 19 | issueDelete: issue.delete, 20 | }, 21 | GraphQLDate, 22 | }; 23 | 24 | const server = new ApolloServer({ 25 | typeDefs: fs.readFileSync('schema.graphql', 'utf-8'), 26 | resolvers, 27 | formatError: (error) => { 28 | console.log(error); 29 | return error; 30 | }, 31 | }); 32 | 33 | function installHandler(app) { 34 | const enableCors = (process.env.ENABLE_CORS || 'true') === 'true'; 35 | console.log('CORS setting:', enableCors); 36 | server.applyMiddleware({ app, path: '/graphql', cors: enableCors }); 37 | } 38 | 39 | module.exports = { installHandler }; 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/api/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { MongoClient } = require('mongodb'); 3 | 4 | let db; 5 | 6 | async function connectToDb() { 7 | const url = process.env.DB_URL || 'mongodb://localhost/issuetracker'; 8 | const client = new MongoClient(url, { useNewUrlParser: true }); 9 | await client.connect(); 10 | console.log('Connected to MongoDB at', url); 11 | db = client.db(); 12 | } 13 | 14 | async function getNextSequence(name) { 15 | const result = await db.collection('counters').findOneAndUpdate( 16 | { _id: name }, 17 | { $inc: { current: 1 } }, 18 | { returnOriginal: false }, 19 | ); 20 | return result.value.current; 21 | } 22 | 23 | function getDb() { 24 | return db; 25 | } 26 | 27 | module.exports = { connectToDb, getNextSequence, getDb }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/api/graphql_date.js: -------------------------------------------------------------------------------- 1 | const { GraphQLScalarType } = require('graphql'); 2 | const { Kind } = require('graphql/language'); 3 | 4 | const GraphQLDate = new GraphQLScalarType({ 5 | name: 'GraphQLDate', 6 | description: 'A Date() type in GraphQL as a scalar', 7 | serialize(value) { 8 | return value.toISOString(); 9 | }, 10 | parseValue(value) { 11 | const dateValue = new Date(value); 12 | return Number.isNaN(dateValue.getTime()) ? undefined : dateValue; 13 | }, 14 | parseLiteral(ast) { 15 | if (ast.kind === Kind.STRING) { 16 | const value = new Date(ast.value); 17 | return Number.isNaN(value.getTime()) ? undefined : value; 18 | } 19 | return undefined; 20 | }, 21 | }); 22 | 23 | module.exports = GraphQLDate; 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-api", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -e js,graphql -w . -w .env server.js", 8 | "lint": "eslint .", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 14 | }, 15 | "author": "vasan.promern@gmail.com", 16 | "license": "ISC", 17 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 18 | "dependencies": { 19 | "apollo-server-express": "^2.3.1", 20 | "dotenv": "^6.2.0", 21 | "express": "^4.16.4", 22 | "graphql": "^0.13.2", 23 | "mongodb": "^3.1.10", 24 | "nodemon": "^1.18.9" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^5.12.0", 28 | "eslint-config-airbnb-base": "^13.1.0", 29 | "eslint-plugin-import": "^2.14.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/api/sample.env: -------------------------------------------------------------------------------- 1 | ## DB 2 | # Local 3 | DB_URL=mongodb://localhost/issuetracker 4 | 5 | # Atlas - replace UUU: user, PPP: password, XXX: hostname 6 | # DB_URL=mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true 7 | 8 | # mLab - replace UUU: user, PPP: password, XXX: hostname, YYY: port 9 | # DB_URL=mongodb://UUU:PPP@XXX.mlab.com:YYY/issuetracker 10 | 11 | 12 | ## Server Port 13 | API_SERVER_PORT=3000 14 | 15 | ## Enable CORS (default: true) 16 | # ENABLE_CORS=false 17 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/api/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar GraphQLDate 2 | 3 | enum StatusType { 4 | New 5 | Assigned 6 | Fixed 7 | Closed 8 | } 9 | 10 | type Issue { 11 | _id: ID! 12 | id: Int! 13 | title: String! 14 | status: StatusType! 15 | owner: String 16 | effort: Int 17 | created: GraphQLDate! 18 | due: GraphQLDate 19 | description: String 20 | } 21 | 22 | "Toned down Issue, used as inputs, without server generated values." 23 | input IssueInputs { 24 | title: String! 25 | "Optional, if not supplied, will be set to 'New'" 26 | status: StatusType = New 27 | owner: String 28 | effort: Int 29 | due: GraphQLDate 30 | description: String 31 | } 32 | 33 | """Inputs for issueUpdate: all are optional. Whichever is specified will 34 | be set to the given value, undefined fields will remain unmodified.""" 35 | input IssueUpdateInputs { 36 | title: String 37 | status: StatusType 38 | owner: String 39 | effort: Int 40 | due: GraphQLDate 41 | description: String 42 | } 43 | 44 | ##### Top level declarations 45 | 46 | type Query { 47 | about: String! 48 | issueList( 49 | status: StatusType 50 | effortMin: Int 51 | effortMax: Int 52 | ): [Issue!]! 53 | issue(id: Int!): Issue! 54 | } 55 | 56 | type Mutation { 57 | setAboutMessage(message: String!): String 58 | issueAdd(issue: IssueInputs!): Issue! 59 | issueUpdate(id: Int!, changes: IssueUpdateInputs!): Issue! 60 | issueDelete(id: Int!): Boolean! 61 | } 62 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/api/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const { connectToDb } = require('./db.js'); 4 | const { installHandler } = require('./api_handler.js'); 5 | 6 | const app = express(); 7 | 8 | installHandler(app); 9 | 10 | const port = process.env.API_SERVER_PORT || 3000; 11 | 12 | (async function start() { 13 | try { 14 | await connectToDb(); 15 | app.listen(port, () => { 16 | console.log(`API server started on port ${port}`); 17 | }); 18 | } catch (err) { 19 | console.log('ERROR:', err); 20 | } 21 | }()); 22 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | } 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/browser/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | rules: { 6 | "import/extensions": [ 'error', 'always', { ignorePackages: true } ], 7 | "react/prop-types": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/browser/App.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | 6 | import Page from '../src/Page.jsx'; 7 | import store from '../src/store.js'; 8 | 9 | // eslint-disable-next-line no-underscore-dangle 10 | store.initialData = window.__INITIAL_DATA__; 11 | 12 | const element = ( 13 | 14 | 15 | 16 | ); 17 | 18 | ReactDOM.hydrate(element, document.getElementById('contents')); 19 | 20 | if (module.hot) { 21 | module.hot.accept(); 22 | } 23 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/public/bootstrap: -------------------------------------------------------------------------------- 1 | ../node_modules/bootstrap/dist -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/sample.env: -------------------------------------------------------------------------------- 1 | UI_SERVER_PORT=8000 2 | UI_API_ENDPOINT=http://localhost:3000/graphql 3 | # UI_SERVER_API_ENDPOINT=http://localhost:3000/graphql 4 | # API_PROXY_TARGET=http://localhost:3000 5 | # ENABLE_HMR=true 6 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "no-console": "off", 7 | "import/extensions": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/server/render.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { StaticRouter, matchPath } from 'react-router-dom'; 4 | 5 | import Page from '../src/Page.jsx'; 6 | import template from './template.js'; 7 | import store from '../src/store.js'; 8 | import routes from '../src/routes.js'; 9 | 10 | async function render(req, res) { 11 | const activeRoute = routes.find( 12 | route => matchPath(req.path, route), 13 | ); 14 | 15 | let initialData; 16 | if (activeRoute && activeRoute.component.fetchData) { 17 | const match = matchPath(req.path, activeRoute); 18 | const index = req.url.indexOf('?'); 19 | const search = index !== -1 ? req.url.substr(index) : null; 20 | initialData = await activeRoute.component.fetchData(match, search); 21 | } 22 | 23 | store.initialData = initialData; 24 | const context = {}; 25 | const element = ( 26 | 27 | 28 | 29 | ); 30 | const body = ReactDOMServer.renderToString(element); 31 | 32 | if (context.url) { 33 | res.redirect(301, context.url); 34 | } else { 35 | res.send(template(body, initialData)); 36 | } 37 | } 38 | 39 | export default render; 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/server/template.js: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | 3 | export default function template(body, data) { 4 | return ` 5 | 6 | 7 | 8 | 9 | Pro MERN Stack 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 |
${body}
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | `; 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "rules": { 7 | "import/extensions": [ "error", "always", { "ignorePackages": true } ], 8 | "react/prop-types": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import store from './store.js'; 3 | import graphQLFetch from './graphQLFetch.js'; 4 | 5 | export default class About extends React.Component { 6 | static async fetchData() { 7 | const data = await graphQLFetch('query {about}'); 8 | return data; 9 | } 10 | 11 | constructor(props) { 12 | super(props); 13 | const apiAbout = store.initialData ? store.initialData.about : null; 14 | delete store.initialData; 15 | this.state = { apiAbout }; 16 | } 17 | 18 | async componentDidMount() { 19 | const { apiAbout } = this.state; 20 | if (apiAbout == null) { 21 | const data = await About.fetchData(); 22 | this.setState({ apiAbout: data.about }); 23 | } 24 | } 25 | 26 | render() { 27 | const { apiAbout } = this.state; 28 | return ( 29 |
30 |

Issue Tracker version 0.9

31 |

32 | {apiAbout} 33 |

34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/Contents.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | 4 | import routes from './routes.js'; 5 | 6 | export default function Contents() { 7 | return ( 8 | 9 | 10 | {routes.map(attrs => )} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/IssueDetail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IssueDetail({ issue }) { 4 | if (issue) { 5 | return ( 6 |
7 |

Description

8 |
{issue.description}
9 |
10 | ); 11 | } 12 | return null; 13 | } 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/IssueReport.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IssueReport() { 4 | return ( 5 |
6 |

This is a placeholder for the Issue Report

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function NotFound() { 4 | return

Page Not Found

; 5 | } 6 | 7 | export default NotFound; 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/NumInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(num) { 4 | return num != null ? num.toString() : ''; 5 | } 6 | 7 | function unformat(str) { 8 | const val = parseInt(str, 10); 9 | return Number.isNaN(val) ? null : val; 10 | } 11 | 12 | export default class NumInput extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { value: format(props.value) }; 16 | this.onBlur = this.onBlur.bind(this); 17 | this.onChange = this.onChange.bind(this); 18 | } 19 | 20 | onChange(e) { 21 | if (e.target.value.match(/^\d*$/)) { 22 | this.setState({ value: e.target.value }); 23 | } 24 | } 25 | 26 | onBlur(e) { 27 | const { onChange } = this.props; 28 | const { value } = this.state; 29 | onChange(e, unformat(value)); 30 | } 31 | 32 | render() { 33 | const { value } = this.state; 34 | return ( 35 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(text) { 4 | return text != null ? text : ''; 5 | } 6 | 7 | function unformat(text) { 8 | return text.trim().length === 0 ? null : text; 9 | } 10 | 11 | export default class TextInput extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { value: format(props.value) }; 15 | this.onBlur = this.onBlur.bind(this); 16 | this.onChange = this.onChange.bind(this); 17 | } 18 | 19 | onChange(e) { 20 | this.setState({ value: e.target.value }); 21 | } 22 | 23 | onBlur(e) { 24 | const { onChange } = this.props; 25 | const { value } = this.state; 26 | onChange(e, unformat(value)); 27 | } 28 | 29 | render() { 30 | const { value } = this.state; 31 | const { tag = 'input', ...props } = this.props; 32 | return React.createElement(tag, { 33 | ...props, 34 | value, 35 | onBlur: this.onBlur, 36 | onChange: this.onChange, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/Toast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, Collapse } from 'react-bootstrap'; 3 | 4 | export default class Toast extends React.Component { 5 | componentDidUpdate() { 6 | const { showing, onDismiss } = this.props; 7 | if (showing) { 8 | clearTimeout(this.dismissTimer); 9 | this.dismissTimer = setTimeout(onDismiss, 5000); 10 | } 11 | } 12 | 13 | componentWillUnmount() { 14 | clearTimeout(this.dismissTimer); 15 | } 16 | 17 | render() { 18 | const { 19 | showing, bsStyle, onDismiss, children, 20 | } = this.props; 21 | return ( 22 | 23 |
24 | 25 | {children} 26 | 27 |
28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/graphQLFetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d'); 4 | 5 | function jsonDateReviver(key, value) { 6 | if (dateRegex.test(value)) return new Date(value); 7 | return value; 8 | } 9 | 10 | export default async function 11 | graphQLFetch(query, variables = {}, showError = null) { 12 | const apiEndpoint = (__isBrowser__) // eslint-disable-line no-undef 13 | ? window.ENV.UI_API_ENDPOINT 14 | : process.env.UI_SERVER_API_ENDPOINT; 15 | try { 16 | const response = await fetch(apiEndpoint, { 17 | method: 'POST', 18 | headers: { 'Content-Type': 'application/json' }, 19 | body: JSON.stringify({ query, variables }), 20 | }); 21 | const body = await response.text(); 22 | const result = JSON.parse(body, jsonDateReviver); 23 | 24 | if (result.errors) { 25 | const error = result.errors[0]; 26 | if (error.extensions.code === 'BAD_USER_INPUT') { 27 | const details = error.extensions.exception.errors.join('\n '); 28 | if (showError) showError(`${error.message}:\n ${details}`); 29 | } else if (showError) { 30 | showError(`${error.extensions.code}: ${error.message}`); 31 | } 32 | } 33 | return result.data; 34 | } catch (e) { 35 | if (showError) showError(`Error in sending data to server: ${e.message}`); 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/routes.js: -------------------------------------------------------------------------------- 1 | import IssueList from './IssueList.jsx'; 2 | import IssueReport from './IssueReport.jsx'; 3 | import IssueEdit from './IssueEdit.jsx'; 4 | import About from './About.jsx'; 5 | import NotFound from './NotFound.jsx'; 6 | 7 | const routes = [ 8 | { path: '/issues/:id?', component: IssueList }, 9 | { path: '/edit/:id', component: IssueEdit }, 10 | { path: '/report', component: IssueReport }, 11 | { path: '/about', component: About }, 12 | { path: '*', component: NotFound }, 13 | ]; 14 | 15 | export default routes; 16 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/src/store.js: -------------------------------------------------------------------------------- 1 | const store = {}; 2 | 3 | export default store; 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-12.14-redirects/ui/webpack.serverHMR.js: -------------------------------------------------------------------------------- 1 | /* 2 | eslint-disable import/no-extraneous-dependencies 3 | */ 4 | const webpack = require('webpack'); 5 | const merge = require('webpack-merge'); 6 | const serverConfig = require('./webpack.config.js')[1]; 7 | 8 | module.exports = merge(serverConfig, { 9 | entry: { server: ['./node_modules/webpack/hot/poll?1000'] }, 10 | plugins: [ 11 | new webpack.HotModuleReplacementPlugin(), 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | ui/public/*.js 4 | ui/public/*.map 5 | .env 6 | ui/dist 7 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": "true" 5 | }, 6 | rules: { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/api/about.js: -------------------------------------------------------------------------------- 1 | let aboutMessage = 'Issue Tracker API v1.0'; 2 | 3 | function setMessage(_, { message }) { 4 | aboutMessage = message; 5 | return aboutMessage; 6 | } 7 | 8 | function getMessage() { 9 | return aboutMessage; 10 | } 11 | 12 | module.exports = { getMessage, setMessage }; 13 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/api/api_handler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | require('dotenv').config(); 3 | const { ApolloServer } = require('apollo-server-express'); 4 | 5 | const GraphQLDate = require('./graphql_date.js'); 6 | const about = require('./about.js'); 7 | const issue = require('./issue.js'); 8 | 9 | const resolvers = { 10 | Query: { 11 | about: about.getMessage, 12 | issueList: issue.list, 13 | issue: issue.get, 14 | issueCounts: issue.counts, 15 | }, 16 | Mutation: { 17 | setAboutMessage: about.setMessage, 18 | issueAdd: issue.add, 19 | issueUpdate: issue.update, 20 | issueDelete: issue.delete, 21 | issueRestore: issue.restore, 22 | }, 23 | GraphQLDate, 24 | }; 25 | 26 | const server = new ApolloServer({ 27 | typeDefs: fs.readFileSync('schema.graphql', 'utf-8'), 28 | resolvers, 29 | formatError: (error) => { 30 | console.log(error); 31 | return error; 32 | }, 33 | }); 34 | 35 | function installHandler(app) { 36 | const enableCors = (process.env.ENABLE_CORS || 'true') === 'true'; 37 | console.log('CORS setting:', enableCors); 38 | server.applyMiddleware({ app, path: '/graphql', cors: enableCors }); 39 | } 40 | 41 | module.exports = { installHandler }; 42 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/api/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { MongoClient } = require('mongodb'); 3 | 4 | let db; 5 | 6 | async function connectToDb() { 7 | const url = process.env.DB_URL || 'mongodb://localhost/issuetracker'; 8 | const client = new MongoClient(url, { useNewUrlParser: true }); 9 | await client.connect(); 10 | console.log('Connected to MongoDB at', url); 11 | db = client.db(); 12 | } 13 | 14 | async function getNextSequence(name) { 15 | const result = await db.collection('counters').findOneAndUpdate( 16 | { _id: name }, 17 | { $inc: { current: 1 } }, 18 | { returnOriginal: false }, 19 | ); 20 | return result.value.current; 21 | } 22 | 23 | function getDb() { 24 | return db; 25 | } 26 | 27 | module.exports = { connectToDb, getNextSequence, getDb }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/api/graphql_date.js: -------------------------------------------------------------------------------- 1 | const { GraphQLScalarType } = require('graphql'); 2 | const { Kind } = require('graphql/language'); 3 | 4 | const GraphQLDate = new GraphQLScalarType({ 5 | name: 'GraphQLDate', 6 | description: 'A Date() type in GraphQL as a scalar', 7 | serialize(value) { 8 | return value.toISOString(); 9 | }, 10 | parseValue(value) { 11 | const dateValue = new Date(value); 12 | return Number.isNaN(dateValue.getTime()) ? undefined : dateValue; 13 | }, 14 | parseLiteral(ast) { 15 | if (ast.kind === Kind.STRING) { 16 | const value = new Date(ast.value); 17 | return Number.isNaN(value.getTime()) ? undefined : value; 18 | } 19 | return undefined; 20 | }, 21 | }); 22 | 23 | module.exports = GraphQLDate; 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-api", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -e js,graphql -w . -w .env server.js", 8 | "lint": "eslint .", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 14 | }, 15 | "author": "vasan.promern@gmail.com", 16 | "license": "ISC", 17 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 18 | "dependencies": { 19 | "apollo-server-express": "^2.3.1", 20 | "dotenv": "^6.2.0", 21 | "express": "^4.16.4", 22 | "graphql": "^0.13.2", 23 | "mongodb": "^3.1.10", 24 | "nodemon": "^1.18.9" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^5.12.0", 28 | "eslint-config-airbnb-base": "^13.1.0", 29 | "eslint-plugin-import": "^2.14.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/api/sample.env: -------------------------------------------------------------------------------- 1 | ## DB 2 | # Local 3 | DB_URL=mongodb://localhost/issuetracker 4 | 5 | # Atlas - replace UUU: user, PPP: password, XXX: hostname 6 | # DB_URL=mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true 7 | 8 | # mLab - replace UUU: user, PPP: password, XXX: hostname, YYY: port 9 | # DB_URL=mongodb://UUU:PPP@XXX.mlab.com:YYY/issuetracker 10 | 11 | 12 | ## Server Port 13 | API_SERVER_PORT=3000 14 | 15 | ## Enable CORS (default: true) 16 | # ENABLE_CORS=false 17 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/api/scripts/generate_data.mongo.js: -------------------------------------------------------------------------------- 1 | /* global db print */ 2 | /* eslint no-restricted-globals: "off" */ 3 | 4 | const owners = ['Ravan', 'Eddie', 'Pieta', 'Parvati', 'Victor']; 5 | const statuses = ['New', 'Assigned', 'Fixed', 'Closed']; 6 | 7 | const initialCount = db.issues.count(); 8 | 9 | for (let i = 0; i < 100; i += 1) { 10 | const randomCreatedDate = (new Date()) 11 | - Math.floor(Math.random() * 60) * 1000 * 60 * 60 * 24; 12 | const created = new Date(randomCreatedDate); 13 | const randomDueDate = (new Date()) 14 | - Math.floor(Math.random() * 60) * 1000 * 60 * 60 * 24; 15 | const due = new Date(randomDueDate); 16 | 17 | const owner = owners[Math.floor(Math.random() * 5)]; 18 | const status = statuses[Math.floor(Math.random() * 4)]; 19 | const effort = Math.ceil(Math.random() * 20); 20 | const title = `Lorem ipsum dolor sit amet, ${i}`; 21 | const id = initialCount + i + 1; 22 | 23 | const issue = { 24 | id, title, created, due, owner, status, effort, 25 | }; 26 | 27 | db.issues.insertOne(issue); 28 | } 29 | 30 | const count = db.issues.count(); 31 | db.counters.update({ _id: 'issues' }, { $set: { current: count } }); 32 | 33 | print('New issue count:', count); 34 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/api/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const { connectToDb } = require('./db.js'); 4 | const { installHandler } = require('./api_handler.js'); 5 | 6 | const app = express(); 7 | 8 | installHandler(app); 9 | 10 | const port = process.env.API_SERVER_PORT || 3000; 11 | 12 | (async function start() { 13 | try { 14 | await connectToDb(); 15 | app.listen(port, () => { 16 | console.log(`API server started on port ${port}`); 17 | }); 18 | } catch (err) { 19 | console.log('ERROR:', err); 20 | } 21 | }()); 22 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | } 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/browser/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | rules: { 6 | "import/extensions": [ 'error', 'always', { ignorePackages: true } ], 7 | "react/prop-types": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/browser/App.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | 6 | import Page from '../src/Page.jsx'; 7 | import store from '../src/store.js'; 8 | 9 | // eslint-disable-next-line no-underscore-dangle 10 | store.initialData = window.__INITIAL_DATA__; 11 | 12 | const element = ( 13 | 14 | 15 | 16 | ); 17 | 18 | ReactDOM.hydrate(element, document.getElementById('contents')); 19 | 20 | if (module.hot) { 21 | module.hot.accept(); 22 | } 23 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/public/bootstrap: -------------------------------------------------------------------------------- 1 | ../node_modules/bootstrap/dist -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/sample.env: -------------------------------------------------------------------------------- 1 | UI_SERVER_PORT=8000 2 | UI_API_ENDPOINT=http://localhost:3000/graphql 3 | # UI_SERVER_API_ENDPOINT=http://localhost:3000/graphql 4 | # API_PROXY_TARGET=http://localhost:3000 5 | # ENABLE_HMR=true 6 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "no-console": "off", 7 | "import/extensions": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/server/render.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { StaticRouter, matchPath } from 'react-router-dom'; 4 | 5 | import Page from '../src/Page.jsx'; 6 | import template from './template.js'; 7 | import store from '../src/store.js'; 8 | import routes from '../src/routes.js'; 9 | 10 | async function render(req, res) { 11 | const activeRoute = routes.find( 12 | route => matchPath(req.path, route), 13 | ); 14 | 15 | let initialData; 16 | if (activeRoute && activeRoute.component.fetchData) { 17 | const match = matchPath(req.path, activeRoute); 18 | const index = req.url.indexOf('?'); 19 | const search = index !== -1 ? req.url.substr(index) : null; 20 | initialData = await activeRoute.component.fetchData(match, search); 21 | } 22 | 23 | store.initialData = initialData; 24 | const context = {}; 25 | const element = ( 26 | 27 | 28 | 29 | ); 30 | const body = ReactDOMServer.renderToString(element); 31 | 32 | if (context.url) { 33 | res.redirect(301, context.url); 34 | } else { 35 | res.send(template(body, initialData)); 36 | } 37 | } 38 | 39 | export default render; 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/server/template.js: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | 3 | export default function template(body, data) { 4 | return ` 5 | 6 | 7 | 8 | 9 | Pro MERN Stack 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 |
${body}
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | `; 31 | } 32 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "rules": { 7 | "import/extensions": [ "error", "always", { "ignorePackages": true } ], 8 | "react/prop-types": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import store from './store.js'; 3 | import graphQLFetch from './graphQLFetch.js'; 4 | 5 | export default class About extends React.Component { 6 | static async fetchData() { 7 | const data = await graphQLFetch('query {about}'); 8 | return data; 9 | } 10 | 11 | constructor(props) { 12 | super(props); 13 | const apiAbout = store.initialData ? store.initialData.about : null; 14 | delete store.initialData; 15 | this.state = { apiAbout }; 16 | } 17 | 18 | async componentDidMount() { 19 | const { apiAbout } = this.state; 20 | if (apiAbout == null) { 21 | const data = await About.fetchData(); 22 | this.setState({ apiAbout: data.about }); 23 | } 24 | } 25 | 26 | render() { 27 | const { apiAbout } = this.state; 28 | return ( 29 |
30 |

Issue Tracker version 0.9

31 |

32 | {apiAbout} 33 |

34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/Contents.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | 4 | import routes from './routes.js'; 5 | 6 | export default function Contents() { 7 | return ( 8 | 9 | 10 | {routes.map(attrs => )} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/IssueDetail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IssueDetail({ issue }) { 4 | if (issue) { 5 | return ( 6 |
7 |

Description

8 |
{issue.description}
9 |
10 | ); 11 | } 12 | return null; 13 | } 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function NotFound() { 4 | return

Page Not Found

; 5 | } 6 | 7 | export default NotFound; 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/NumInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(num) { 4 | return num != null ? num.toString() : ''; 5 | } 6 | 7 | function unformat(str) { 8 | const val = parseInt(str, 10); 9 | return Number.isNaN(val) ? null : val; 10 | } 11 | 12 | export default class NumInput extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { value: format(props.value) }; 16 | this.onBlur = this.onBlur.bind(this); 17 | this.onChange = this.onChange.bind(this); 18 | } 19 | 20 | onChange(e) { 21 | if (e.target.value.match(/^\d*$/)) { 22 | this.setState({ value: e.target.value }); 23 | } 24 | } 25 | 26 | onBlur(e) { 27 | const { onChange } = this.props; 28 | const { value } = this.state; 29 | onChange(e, unformat(value)); 30 | } 31 | 32 | render() { 33 | const { value } = this.state; 34 | return ( 35 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SelectAsync from 'react-select/lib/Async'; // eslint-disable-line 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | import graphQLFetch from './graphQLFetch.js'; 6 | import withToast from './withToast.jsx'; 7 | 8 | class Search extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.onChangeSelection = this.onChangeSelection.bind(this); 13 | this.loadOptions = this.loadOptions.bind(this); 14 | } 15 | 16 | onChangeSelection({ value }) { 17 | const { history } = this.props; 18 | history.push(`/edit/${value}`); 19 | } 20 | 21 | async loadOptions(term) { 22 | if (term.length < 3) return []; 23 | const query = `query issueList($search: String) { 24 | issueList(search: $search) { 25 | issues {id title} 26 | } 27 | }`; 28 | 29 | const { showError } = this.props; 30 | const data = await graphQLFetch(query, { search: term }, showError); 31 | return data.issueList.issues.map(issue => ({ 32 | label: `#${issue.id}: ${issue.title}`, value: issue.id, 33 | })); 34 | } 35 | 36 | render() { 37 | return ( 38 | true} 43 | onChange={this.onChangeSelection} 44 | components={{ DropdownIndicator: null }} 45 | /> 46 | ); 47 | } 48 | } 49 | 50 | export default withRouter(withToast(Search)); 51 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(text) { 4 | return text != null ? text : ''; 5 | } 6 | 7 | function unformat(text) { 8 | return text.trim().length === 0 ? null : text; 9 | } 10 | 11 | export default class TextInput extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { value: format(props.value) }; 15 | this.onBlur = this.onBlur.bind(this); 16 | this.onChange = this.onChange.bind(this); 17 | } 18 | 19 | onChange(e) { 20 | this.setState({ value: e.target.value }); 21 | } 22 | 23 | onBlur(e) { 24 | const { onChange } = this.props; 25 | const { value } = this.state; 26 | onChange(e, unformat(value)); 27 | } 28 | 29 | render() { 30 | const { value } = this.state; 31 | const { tag = 'input', ...props } = this.props; 32 | return React.createElement(tag, { 33 | ...props, 34 | value, 35 | onBlur: this.onBlur, 36 | onChange: this.onChange, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/Toast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, Collapse } from 'react-bootstrap'; 3 | 4 | export default class Toast extends React.Component { 5 | componentDidUpdate() { 6 | const { showing, onDismiss } = this.props; 7 | if (showing) { 8 | clearTimeout(this.dismissTimer); 9 | this.dismissTimer = setTimeout(onDismiss, 5000); 10 | } 11 | } 12 | 13 | componentWillUnmount() { 14 | clearTimeout(this.dismissTimer); 15 | } 16 | 17 | render() { 18 | const { 19 | showing, bsStyle, onDismiss, children, 20 | } = this.props; 21 | return ( 22 | 23 |
27 | 28 | {children} 29 | 30 |
31 |
32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/graphQLFetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d'); 4 | 5 | function jsonDateReviver(key, value) { 6 | if (dateRegex.test(value)) return new Date(value); 7 | return value; 8 | } 9 | 10 | export default async function 11 | graphQLFetch(query, variables = {}, showError = null) { 12 | const apiEndpoint = (__isBrowser__) // eslint-disable-line no-undef 13 | ? window.ENV.UI_API_ENDPOINT 14 | : process.env.UI_SERVER_API_ENDPOINT; 15 | try { 16 | const response = await fetch(apiEndpoint, { 17 | method: 'POST', 18 | headers: { 'Content-Type': 'application/json' }, 19 | body: JSON.stringify({ query, variables }), 20 | }); 21 | const body = await response.text(); 22 | const result = JSON.parse(body, jsonDateReviver); 23 | 24 | if (result.errors) { 25 | const error = result.errors[0]; 26 | if (error.extensions.code === 'BAD_USER_INPUT') { 27 | const details = error.extensions.exception.errors.join('\n '); 28 | if (showError) showError(`${error.message}:\n ${details}`); 29 | } else if (showError) { 30 | showError(`${error.extensions.code}: ${error.message}`); 31 | } 32 | } 33 | return result.data; 34 | } catch (e) { 35 | if (showError) showError(`Error in sending data to server: ${e.message}`); 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/routes.js: -------------------------------------------------------------------------------- 1 | import IssueList from './IssueList.jsx'; 2 | import IssueReport from './IssueReport.jsx'; 3 | import IssueEdit from './IssueEdit.jsx'; 4 | import About from './About.jsx'; 5 | import NotFound from './NotFound.jsx'; 6 | 7 | const routes = [ 8 | { path: '/issues/:id?', component: IssueList }, 9 | { path: '/edit/:id', component: IssueEdit }, 10 | { path: '/report', component: IssueReport }, 11 | { path: '/about', component: About }, 12 | { path: '*', component: NotFound }, 13 | ]; 14 | 15 | export default routes; 16 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/store.js: -------------------------------------------------------------------------------- 1 | const store = {}; 2 | 3 | export default store; 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/src/withToast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Toast from './Toast.jsx'; 3 | 4 | export default function withToast(OriginalComponent) { 5 | return class ToastWrapper extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | toastVisible: false, toastMessage: '', toastType: 'success', 10 | }; 11 | this.showSuccess = this.showSuccess.bind(this); 12 | this.showError = this.showError.bind(this); 13 | this.dismissToast = this.dismissToast.bind(this); 14 | } 15 | 16 | showSuccess(message) { 17 | this.setState({ toastVisible: true, toastMessage: message, toastType: 'success' }); 18 | } 19 | 20 | showError(message) { 21 | this.setState({ toastVisible: true, toastMessage: message, toastType: 'danger' }); 22 | } 23 | 24 | dismissToast() { 25 | this.setState({ toastVisible: false }); 26 | } 27 | 28 | render() { 29 | const { toastType, toastVisible, toastMessage } = this.state; 30 | return ( 31 | 32 | 38 | 43 | {toastMessage} 44 | 45 | 46 | ); 47 | } 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /pro-mern-stack-2-13.10-search-bar/ui/webpack.serverHMR.js: -------------------------------------------------------------------------------- 1 | /* 2 | eslint-disable import/no-extraneous-dependencies 3 | */ 4 | const webpack = require('webpack'); 5 | const merge = require('webpack-merge'); 6 | const serverConfig = require('./webpack.config.js')[1]; 7 | 8 | module.exports = merge(serverConfig, { 9 | entry: { server: ['./node_modules/webpack/hot/poll?1000'] }, 10 | plugins: [ 11 | new webpack.HotModuleReplacementPlugin(), 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | ui/public/*.js 4 | ui/public/*.map 5 | .env 6 | ui/dist 7 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": "true" 5 | }, 6 | rules: { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/api/about.js: -------------------------------------------------------------------------------- 1 | const { mustBeSignedIn } = require('./auth.js'); 2 | 3 | let aboutMessage = 'Issue Tracker API v1.0'; 4 | 5 | function setMessage(_, { message }) { 6 | aboutMessage = message; 7 | return aboutMessage; 8 | } 9 | 10 | function getMessage() { 11 | return aboutMessage; 12 | } 13 | 14 | module.exports = { getMessage, setMessage: mustBeSignedIn(setMessage) }; 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/api/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { MongoClient } = require('mongodb'); 3 | 4 | let db; 5 | 6 | async function connectToDb() { 7 | const url = process.env.DB_URL || 'mongodb://localhost/issuetracker'; 8 | const client = new MongoClient(url, { useNewUrlParser: true }); 9 | await client.connect(); 10 | console.log('Connected to MongoDB at', url); 11 | db = client.db(); 12 | } 13 | 14 | async function getNextSequence(name) { 15 | const result = await db.collection('counters').findOneAndUpdate( 16 | { _id: name }, 17 | { $inc: { current: 1 } }, 18 | { returnOriginal: false }, 19 | ); 20 | return result.value.current; 21 | } 22 | 23 | function getDb() { 24 | return db; 25 | } 26 | 27 | module.exports = { connectToDb, getNextSequence, getDb }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/api/graphql_date.js: -------------------------------------------------------------------------------- 1 | const { GraphQLScalarType } = require('graphql'); 2 | const { Kind } = require('graphql/language'); 3 | 4 | const GraphQLDate = new GraphQLScalarType({ 5 | name: 'GraphQLDate', 6 | description: 'A Date() type in GraphQL as a scalar', 7 | serialize(value) { 8 | return value.toISOString(); 9 | }, 10 | parseValue(value) { 11 | const dateValue = new Date(value); 12 | return Number.isNaN(dateValue.getTime()) ? undefined : dateValue; 13 | }, 14 | parseLiteral(ast) { 15 | if (ast.kind === Kind.STRING) { 16 | const value = new Date(ast.value); 17 | return Number.isNaN(value.getTime()) ? undefined : value; 18 | } 19 | return undefined; 20 | }, 21 | }); 22 | 23 | module.exports = GraphQLDate; 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-api", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon -e js,graphql -w . -w .env server.js", 8 | "lint": "eslint .", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 14 | }, 15 | "author": "vasan.promern@gmail.com", 16 | "license": "ISC", 17 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 18 | "dependencies": { 19 | "apollo-server-express": "^2.3.1", 20 | "body-parser": "^1.18.3", 21 | "cookie-parser": "^1.4.3", 22 | "cors": "^2.8.5", 23 | "dotenv": "^6.2.0", 24 | "express": "^4.16.4", 25 | "google-auth-library": "^2.0.2", 26 | "graphql": "^0.13.2", 27 | "jsonwebtoken": "^8.4.0", 28 | "mongodb": "^3.1.10", 29 | "nodemon": "^1.18.9" 30 | }, 31 | "devDependencies": { 32 | "eslint": "^5.12.0", 33 | "eslint-config-airbnb-base": "^13.1.0", 34 | "eslint-plugin-import": "^2.14.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/api/sample.env: -------------------------------------------------------------------------------- 1 | ## DB 2 | # Local 3 | DB_URL=mongodb://localhost/issuetracker 4 | 5 | # Atlas - replace UUU: user, PPP: password, XXX: hostname 6 | # DB_URL=mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true 7 | 8 | # mLab - replace UUU: user, PPP: password, XXX: hostname, YYY: port 9 | # DB_URL=mongodb://UUU:PPP@XXX.mlab.com:YYY/issuetracker 10 | 11 | 12 | ## Server Port 13 | API_SERVER_PORT=3000 14 | 15 | ## Enable CORS (default: true) 16 | # ENABLE_CORS=false 17 | 18 | UI_SERVER_ORIGIN=http://ui.promernstack.com:8000 19 | 20 | COOKIE_DOMAIN=promernstack.com 21 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/api/scripts/generate_data.mongo.js: -------------------------------------------------------------------------------- 1 | /* global db print */ 2 | /* eslint no-restricted-globals: "off" */ 3 | 4 | const owners = ['Ravan', 'Eddie', 'Pieta', 'Parvati', 'Victor']; 5 | const statuses = ['New', 'Assigned', 'Fixed', 'Closed']; 6 | 7 | const initialCount = db.issues.count(); 8 | 9 | for (let i = 0; i < 100; i += 1) { 10 | const randomCreatedDate = (new Date()) 11 | - Math.floor(Math.random() * 60) * 1000 * 60 * 60 * 24; 12 | const created = new Date(randomCreatedDate); 13 | const randomDueDate = (new Date()) 14 | - Math.floor(Math.random() * 60) * 1000 * 60 * 60 * 24; 15 | const due = new Date(randomDueDate); 16 | 17 | const owner = owners[Math.floor(Math.random() * 5)]; 18 | const status = statuses[Math.floor(Math.random() * 4)]; 19 | const effort = Math.ceil(Math.random() * 20); 20 | const title = `Lorem ipsum dolor sit amet, ${i}`; 21 | const id = initialCount + i + 1; 22 | 23 | const issue = { 24 | id, title, created, due, owner, status, effort, 25 | }; 26 | 27 | db.issues.insertOne(issue); 28 | } 29 | 30 | const count = db.issues.count(); 31 | db.counters.update({ _id: 'issues' }, { $set: { current: count } }); 32 | 33 | print('New issue count:', count); 34 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/api/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const cookieParser = require('cookie-parser'); 4 | 5 | const { connectToDb } = require('./db.js'); 6 | const { installHandler } = require('./api_handler.js'); 7 | const auth = require('./auth.js'); 8 | 9 | const app = express(); 10 | 11 | app.use(cookieParser()); 12 | app.use('/auth', auth.routes); 13 | 14 | installHandler(app); 15 | 16 | const port = process.env.API_SERVER_PORT || 3000; 17 | 18 | (async function start() { 19 | try { 20 | await connectToDb(); 21 | app.listen(port, () => { 22 | console.log(`API server started on port ${port}`); 23 | }); 24 | } catch (err) { 25 | console.log('ERROR:', err); 26 | } 27 | }()); 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | } 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/browser/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | rules: { 6 | "import/extensions": [ 'error', 'always', { ignorePackages: true } ], 7 | "react/prop-types": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/browser/App.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | 6 | import Page from '../src/Page.jsx'; 7 | import store from '../src/store.js'; 8 | 9 | /* eslint-disable no-underscore-dangle */ 10 | store.initialData = window.__INITIAL_DATA__; 11 | store.userData = window.__USER_DATA__; 12 | 13 | const element = ( 14 | 15 | 16 | 17 | ); 18 | 19 | ReactDOM.hydrate(element, document.getElementById('contents')); 20 | 21 | if (module.hot) { 22 | module.hot.accept(); 23 | } 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/public/bootstrap: -------------------------------------------------------------------------------- 1 | ../node_modules/bootstrap/dist -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/sample.env: -------------------------------------------------------------------------------- 1 | UI_SERVER_PORT=8000 2 | # ENABLE_HMR=true 3 | GOOGLE_CLIENT_ID=YOUR_CLIENT_ID.apps.googleusercontent.com 4 | 5 | # Regular config 6 | # UI_API_ENDPOINT=http://localhost:3000/graphql 7 | # UI_AUTH_ENDPOINT=http://localhost:3000/auth 8 | 9 | # Regular config with domains 10 | UI_API_ENDPOINT=http://api.promernstack.com:3000/graphql 11 | UI_AUTH_ENDPOINT=http://api.promernstack.com:3000/auth 12 | 13 | # Proxy Config 14 | # UI_API_ENDPOINT=http://localhost:8000/graphql 15 | # UI_AUTH_ENDPOINT=http://localhost:8000/auth 16 | # API_PROXY_TARGET=http://localhost:3000 17 | # UI_SERVER_API_ENDPOINT=http://localhost:3000/graphql 18 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "no-console": "off", 7 | "import/extensions": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/server/render.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { StaticRouter, matchPath } from 'react-router-dom'; 4 | 5 | import Page from '../src/Page.jsx'; 6 | import template from './template.js'; 7 | import store from '../src/store.js'; 8 | import routes from '../src/routes.js'; 9 | 10 | async function render(req, res) { 11 | const activeRoute = routes.find( 12 | route => matchPath(req.path, route), 13 | ); 14 | 15 | let initialData; 16 | if (activeRoute && activeRoute.component.fetchData) { 17 | const match = matchPath(req.path, activeRoute); 18 | const index = req.url.indexOf('?'); 19 | const search = index !== -1 ? req.url.substr(index) : null; 20 | initialData = await activeRoute.component 21 | .fetchData(match, search, req.headers.cookie); 22 | } 23 | 24 | const userData = await Page.fetchData(req.headers.cookie); 25 | 26 | store.initialData = initialData; 27 | store.userData = userData; 28 | 29 | const context = {}; 30 | const element = ( 31 | 32 | 33 | 34 | ); 35 | const body = ReactDOMServer.renderToString(element); 36 | 37 | if (context.url) { 38 | res.redirect(301, context.url); 39 | } else { 40 | res.send(template(body, initialData, userData)); 41 | } 42 | } 43 | 44 | export default render; 45 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/server/template.js: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | 3 | export default function template(body, initialData, userData) { 4 | return ` 5 | 6 | 7 | 8 | 9 | Pro MERN Stack 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 |
${body}
24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | `; 36 | } 37 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "rules": { 7 | "import/extensions": [ "error", "always", { "ignorePackages": true } ], 8 | "react/prop-types": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import store from './store.js'; 3 | import graphQLFetch from './graphQLFetch.js'; 4 | 5 | export default class About extends React.Component { 6 | static async fetchData() { 7 | const data = await graphQLFetch('query {about}'); 8 | return data; 9 | } 10 | 11 | constructor(props) { 12 | super(props); 13 | const apiAbout = store.initialData ? store.initialData.about : null; 14 | delete store.initialData; 15 | this.state = { apiAbout }; 16 | } 17 | 18 | async componentDidMount() { 19 | const { apiAbout } = this.state; 20 | if (apiAbout == null) { 21 | const data = await About.fetchData(); 22 | this.setState({ apiAbout: data.about }); 23 | } 24 | } 25 | 26 | render() { 27 | const { apiAbout } = this.state; 28 | return ( 29 |
30 |

Issue Tracker version 0.9

31 |

32 | {apiAbout} 33 |

34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/Contents.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | 4 | import routes from './routes.js'; 5 | 6 | export default function Contents() { 7 | return ( 8 | 9 | 10 | {routes.map(attrs => )} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/IssueDetail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IssueDetail({ issue }) { 4 | if (issue) { 5 | return ( 6 |
7 |

Description

8 |
{issue.description}
9 |
10 | ); 11 | } 12 | return null; 13 | } 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function NotFound() { 4 | return

Page Not Found

; 5 | } 6 | 7 | export default NotFound; 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/NumInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(num) { 4 | return num != null ? num.toString() : ''; 5 | } 6 | 7 | function unformat(str) { 8 | const val = parseInt(str, 10); 9 | return Number.isNaN(val) ? null : val; 10 | } 11 | 12 | export default class NumInput extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { value: format(props.value) }; 16 | this.onBlur = this.onBlur.bind(this); 17 | this.onChange = this.onChange.bind(this); 18 | } 19 | 20 | onChange(e) { 21 | if (e.target.value.match(/^\d*$/)) { 22 | this.setState({ value: e.target.value }); 23 | } 24 | } 25 | 26 | onBlur(e) { 27 | const { onChange } = this.props; 28 | const { value } = this.state; 29 | onChange(e, unformat(value)); 30 | } 31 | 32 | render() { 33 | const { value } = this.state; 34 | return ( 35 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SelectAsync from 'react-select/lib/Async'; // eslint-disable-line 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | import graphQLFetch from './graphQLFetch.js'; 6 | import withToast from './withToast.jsx'; 7 | 8 | class Search extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.onChangeSelection = this.onChangeSelection.bind(this); 13 | this.loadOptions = this.loadOptions.bind(this); 14 | } 15 | 16 | onChangeSelection({ value }) { 17 | const { history } = this.props; 18 | history.push(`/edit/${value}`); 19 | } 20 | 21 | async loadOptions(term) { 22 | if (term.length < 3) return []; 23 | const query = `query issueList($search: String) { 24 | issueList(search: $search) { 25 | issues {id title} 26 | } 27 | }`; 28 | 29 | const { showError } = this.props; 30 | const data = await graphQLFetch(query, { search: term }, showError); 31 | return data.issueList.issues.map(issue => ({ 32 | label: `#${issue.id}: ${issue.title}`, value: issue.id, 33 | })); 34 | } 35 | 36 | render() { 37 | return ( 38 | true} 43 | onChange={this.onChangeSelection} 44 | components={{ DropdownIndicator: null }} 45 | /> 46 | ); 47 | } 48 | } 49 | 50 | export default withRouter(withToast(Search)); 51 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(text) { 4 | return text != null ? text : ''; 5 | } 6 | 7 | function unformat(text) { 8 | return text.trim().length === 0 ? null : text; 9 | } 10 | 11 | export default class TextInput extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { value: format(props.value) }; 15 | this.onBlur = this.onBlur.bind(this); 16 | this.onChange = this.onChange.bind(this); 17 | } 18 | 19 | onChange(e) { 20 | this.setState({ value: e.target.value }); 21 | } 22 | 23 | onBlur(e) { 24 | const { onChange } = this.props; 25 | const { value } = this.state; 26 | onChange(e, unformat(value)); 27 | } 28 | 29 | render() { 30 | const { value } = this.state; 31 | const { tag = 'input', ...props } = this.props; 32 | return React.createElement(tag, { 33 | ...props, 34 | value, 35 | onBlur: this.onBlur, 36 | onChange: this.onChange, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/Toast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, Collapse } from 'react-bootstrap'; 3 | 4 | export default class Toast extends React.Component { 5 | componentDidUpdate() { 6 | const { showing, onDismiss } = this.props; 7 | if (showing) { 8 | clearTimeout(this.dismissTimer); 9 | this.dismissTimer = setTimeout(onDismiss, 5000); 10 | } 11 | } 12 | 13 | componentWillUnmount() { 14 | clearTimeout(this.dismissTimer); 15 | } 16 | 17 | render() { 18 | const { 19 | showing, bsStyle, onDismiss, children, 20 | } = this.props; 21 | return ( 22 | 23 |
27 | 28 | {children} 29 | 30 |
31 |
32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/UserContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UserContext = React.createContext({ 4 | signedIn: false, 5 | }); 6 | 7 | export default UserContext; 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/graphQLFetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d'); 4 | 5 | function jsonDateReviver(key, value) { 6 | if (dateRegex.test(value)) return new Date(value); 7 | return value; 8 | } 9 | 10 | export default async function 11 | graphQLFetch(query, variables = {}, showError = null, cookie = null) { 12 | const apiEndpoint = (__isBrowser__) // eslint-disable-line no-undef 13 | ? window.ENV.UI_API_ENDPOINT 14 | : process.env.UI_SERVER_API_ENDPOINT; 15 | try { 16 | const headers = { 'Content-Type': 'application/json' }; 17 | if (cookie) headers.Cookie = cookie; 18 | const response = await fetch(apiEndpoint, { 19 | method: 'POST', 20 | credentials: 'include', 21 | headers, 22 | body: JSON.stringify({ query, variables }), 23 | }); 24 | const body = await response.text(); 25 | const result = JSON.parse(body, jsonDateReviver); 26 | 27 | if (result.errors) { 28 | const error = result.errors[0]; 29 | if (error.extensions.code === 'BAD_USER_INPUT') { 30 | const details = error.extensions.exception.errors.join('\n '); 31 | if (showError) showError(`${error.message}:\n ${details}`); 32 | } else if (showError) { 33 | showError(`${error.extensions.code}: ${error.message}`); 34 | } 35 | } 36 | return result.data; 37 | } catch (e) { 38 | if (showError) showError(`Error in sending data to server: ${e.message}`); 39 | return null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/routes.js: -------------------------------------------------------------------------------- 1 | import IssueList from './IssueList.jsx'; 2 | import IssueReport from './IssueReport.jsx'; 3 | import IssueEdit from './IssueEdit.jsx'; 4 | import About from './About.jsx'; 5 | import NotFound from './NotFound.jsx'; 6 | 7 | const routes = [ 8 | { path: '/issues/:id?', component: IssueList }, 9 | { path: '/edit/:id', component: IssueEdit }, 10 | { path: '/report', component: IssueReport }, 11 | { path: '/about', component: About }, 12 | { path: '*', component: NotFound }, 13 | ]; 14 | 15 | export default routes; 16 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/store.js: -------------------------------------------------------------------------------- 1 | const store = {}; 2 | 3 | export default store; 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/src/withToast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Toast from './Toast.jsx'; 3 | 4 | export default function withToast(OriginalComponent) { 5 | return class ToastWrapper extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | toastVisible: false, toastMessage: '', toastType: 'success', 10 | }; 11 | this.showSuccess = this.showSuccess.bind(this); 12 | this.showError = this.showError.bind(this); 13 | this.dismissToast = this.dismissToast.bind(this); 14 | } 15 | 16 | showSuccess(message) { 17 | this.setState({ toastVisible: true, toastMessage: message, toastType: 'success' }); 18 | } 19 | 20 | showError(message) { 21 | this.setState({ toastVisible: true, toastMessage: message, toastType: 'danger' }); 22 | } 23 | 24 | dismissToast() { 25 | this.setState({ toastVisible: false }); 26 | } 27 | 28 | render() { 29 | const { toastType, toastVisible, toastMessage } = this.state; 30 | return ( 31 | 32 | 38 | 43 | {toastMessage} 44 | 45 | 46 | ); 47 | } 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /pro-mern-stack-2-14.11-cookie-domain/ui/webpack.serverHMR.js: -------------------------------------------------------------------------------- 1 | /* 2 | eslint-disable import/no-extraneous-dependencies 3 | */ 4 | const webpack = require('webpack'); 5 | const merge = require('webpack-merge'); 6 | const serverConfig = require('./webpack.config.js')[1]; 7 | 8 | module.exports = merge(serverConfig, { 9 | entry: { server: ['./node_modules/webpack/hot/poll?1000'] }, 10 | plugins: [ 11 | new webpack.HotModuleReplacementPlugin(), 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | ui/public/*.js 4 | ui/public/*.map 5 | .env 6 | ui/dist 7 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/README.md: -------------------------------------------------------------------------------- 1 | # Pro MERN Stack - 2nd Edition 2 | 3 | You are browsing the source code at the end of one of the sections in the book. 4 | 5 | The project's README which contains the list of all chapters, sections 6 | their sources and other useful information can be found in the 7 | [master branch](https://github.com/vasansr/pro-mern-stack-2). 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": "true" 5 | }, 6 | rules: { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/api/about.js: -------------------------------------------------------------------------------- 1 | const { mustBeSignedIn } = require('./auth.js'); 2 | 3 | let aboutMessage = 'Issue Tracker API v1.0'; 4 | 5 | function setMessage(_, { message }) { 6 | aboutMessage = message; 7 | return aboutMessage; 8 | } 9 | 10 | function getMessage() { 11 | return aboutMessage; 12 | } 13 | 14 | module.exports = { getMessage, setMessage: mustBeSignedIn(setMessage) }; 15 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/api/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { MongoClient } = require('mongodb'); 3 | 4 | let db; 5 | 6 | async function connectToDb() { 7 | const url = process.env.DB_URL || 'mongodb://localhost/issuetracker'; 8 | const client = new MongoClient(url, { useNewUrlParser: true }); 9 | await client.connect(); 10 | console.log('Connected to MongoDB at', url); 11 | db = client.db(); 12 | } 13 | 14 | async function getNextSequence(name) { 15 | const result = await db.collection('counters').findOneAndUpdate( 16 | { _id: name }, 17 | { $inc: { current: 1 } }, 18 | { returnOriginal: false }, 19 | ); 20 | return result.value.current; 21 | } 22 | 23 | function getDb() { 24 | return db; 25 | } 26 | 27 | module.exports = { connectToDb, getNextSequence, getDb }; 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/api/graphql_date.js: -------------------------------------------------------------------------------- 1 | const { GraphQLScalarType } = require('graphql'); 2 | const { Kind } = require('graphql/language'); 3 | 4 | const GraphQLDate = new GraphQLScalarType({ 5 | name: 'GraphQLDate', 6 | description: 'A Date() type in GraphQL as a scalar', 7 | serialize(value) { 8 | return value.toISOString(); 9 | }, 10 | parseValue(value) { 11 | const dateValue = new Date(value); 12 | return Number.isNaN(dateValue.getTime()) ? undefined : dateValue; 13 | }, 14 | parseLiteral(ast) { 15 | if (ast.kind === Kind.STRING) { 16 | const value = new Date(ast.value); 17 | return Number.isNaN(value.getTime()) ? undefined : value; 18 | } 19 | return undefined; 20 | }, 21 | }); 22 | 23 | module.exports = GraphQLDate; 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-mern-stack-2-api", 3 | "version": "1.0.0", 4 | "description": "Pro MERN Stack (2nd Edition) API", 5 | "main": "index.js", 6 | "engines": { 7 | "node": "10.x", 8 | "npm": "6.x" 9 | }, 10 | "scripts": { 11 | "start": "nodemon -e js,graphql -w . -w .env server.js", 12 | "lint": "eslint .", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/vasansr/pro-mern-stack-2.git" 18 | }, 19 | "author": "vasan.promern@gmail.com", 20 | "license": "ISC", 21 | "homepage": "https://github.com/vasansr/pro-mern-stack-2", 22 | "dependencies": { 23 | "apollo-server-express": "^2.3.1", 24 | "body-parser": "^1.18.3", 25 | "cookie-parser": "^1.4.3", 26 | "cors": "^2.8.5", 27 | "dotenv": "^6.2.0", 28 | "express": "^4.16.4", 29 | "google-auth-library": "^2.0.2", 30 | "graphql": "^0.13.2", 31 | "jsonwebtoken": "^8.4.0", 32 | "mongodb": "^3.1.10", 33 | "nodemon": "^1.18.9" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^5.12.0", 37 | "eslint-config-airbnb-base": "^13.1.0", 38 | "eslint-plugin-import": "^2.14.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/api/sample.env: -------------------------------------------------------------------------------- 1 | ## DB 2 | # Local 3 | DB_URL=mongodb://localhost/issuetracker 4 | 5 | # Atlas - replace UUU: user, PPP: password, XXX: hostname 6 | # DB_URL=mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true 7 | 8 | # mLab - replace UUU: user, PPP: password, XXX: hostname, YYY: port 9 | # DB_URL=mongodb://UUU:PPP@XXX.mlab.com:YYY/issuetracker 10 | 11 | 12 | ## Server Port 13 | PORT=3000 14 | 15 | ## Enable CORS (default: true) 16 | # ENABLE_CORS=false 17 | 18 | UI_SERVER_ORIGIN=http://ui.promernstack.com:8000 19 | 20 | COOKIE_DOMAIN=promernstack.com 21 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/api/scripts/generate_data.mongo.js: -------------------------------------------------------------------------------- 1 | /* global db print */ 2 | /* eslint no-restricted-globals: "off" */ 3 | 4 | const owners = ['Ravan', 'Eddie', 'Pieta', 'Parvati', 'Victor']; 5 | const statuses = ['New', 'Assigned', 'Fixed', 'Closed']; 6 | 7 | const initialCount = db.issues.count(); 8 | 9 | for (let i = 0; i < 100; i += 1) { 10 | const randomCreatedDate = (new Date()) 11 | - Math.floor(Math.random() * 60) * 1000 * 60 * 60 * 24; 12 | const created = new Date(randomCreatedDate); 13 | const randomDueDate = (new Date()) 14 | - Math.floor(Math.random() * 60) * 1000 * 60 * 60 * 24; 15 | const due = new Date(randomDueDate); 16 | 17 | const owner = owners[Math.floor(Math.random() * 5)]; 18 | const status = statuses[Math.floor(Math.random() * 4)]; 19 | const effort = Math.ceil(Math.random() * 20); 20 | const title = `Lorem ipsum dolor sit amet, ${i}`; 21 | const id = initialCount + i + 1; 22 | 23 | const issue = { 24 | id, title, created, due, owner, status, effort, 25 | }; 26 | 27 | db.issues.insertOne(issue); 28 | } 29 | 30 | const count = db.issues.count(); 31 | db.counters.update({ _id: 'issues' }, { $set: { current: count } }); 32 | 33 | print('New issue count:', count); 34 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/api/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const cookieParser = require('cookie-parser'); 4 | 5 | const { connectToDb } = require('./db.js'); 6 | const { installHandler } = require('./api_handler.js'); 7 | const auth = require('./auth.js'); 8 | 9 | const app = express(); 10 | 11 | app.use(cookieParser()); 12 | app.use('/auth', auth.routes); 13 | 14 | installHandler(app); 15 | 16 | const port = process.env.PORT || 3000; 17 | 18 | (async function start() { 19 | try { 20 | await connectToDb(); 21 | app.listen(port, () => { 22 | console.log(`API server started on port ${port}`); 23 | }); 24 | } catch (err) { 25 | console.log('ERROR:', err); 26 | } 27 | }()); 28 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | } 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .env 4 | public/*.js 5 | public/*.js.map 6 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/browser/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | rules: { 6 | "import/extensions": [ 'error', 'always', { ignorePackages: true } ], 7 | "react/prop-types": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/browser/App.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | 6 | import Page from '../src/Page.jsx'; 7 | import store from '../src/store.js'; 8 | 9 | /* eslint-disable no-underscore-dangle */ 10 | store.initialData = window.__INITIAL_DATA__; 11 | store.userData = window.__USER_DATA__; 12 | 13 | const element = ( 14 | 15 | 16 | 17 | ); 18 | 19 | ReactDOM.hydrate(element, document.getElementById('contents')); 20 | 21 | if (module.hot) { 22 | module.hot.accept(); 23 | } 24 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/public/bootstrap: -------------------------------------------------------------------------------- 1 | ../node_modules/bootstrap/dist -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/sample.env: -------------------------------------------------------------------------------- 1 | PORT=8000 2 | # ENABLE_HMR=true 3 | GOOGLE_CLIENT_ID=YOUR_CLIENT_ID.apps.googleusercontent.com 4 | 5 | # Regular config 6 | # UI_API_ENDPOINT=http://localhost:3000/graphql 7 | # UI_AUTH_ENDPOINT=http://localhost:3000/auth 8 | 9 | # Regular config with domains 10 | UI_API_ENDPOINT=http://api.promernstack.com:3000/graphql 11 | UI_AUTH_ENDPOINT=http://api.promernstack.com:3000/auth 12 | 13 | # Proxy Config 14 | # UI_API_ENDPOINT=http://localhost:8000/graphql 15 | # UI_AUTH_ENDPOINT=http://localhost:8000/auth 16 | # API_PROXY_TARGET=http://localhost:3000 17 | # UI_SERVER_API_ENDPOINT=http://localhost:3000/graphql 18 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "no-console": "off", 7 | "import/extensions": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/server/render.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { StaticRouter, matchPath } from 'react-router-dom'; 4 | 5 | import Page from '../src/Page.jsx'; 6 | import template from './template.js'; 7 | import store from '../src/store.js'; 8 | import routes from '../src/routes.js'; 9 | 10 | async function render(req, res) { 11 | const activeRoute = routes.find( 12 | route => matchPath(req.path, route), 13 | ); 14 | 15 | let initialData; 16 | if (activeRoute && activeRoute.component.fetchData) { 17 | const match = matchPath(req.path, activeRoute); 18 | const index = req.url.indexOf('?'); 19 | const search = index !== -1 ? req.url.substr(index) : null; 20 | initialData = await activeRoute.component 21 | .fetchData(match, search, req.headers.cookie); 22 | } 23 | 24 | const userData = await Page.fetchData(req.headers.cookie); 25 | 26 | store.initialData = initialData; 27 | store.userData = userData; 28 | 29 | const context = {}; 30 | const element = ( 31 | 32 | 33 | 34 | ); 35 | const body = ReactDOMServer.renderToString(element); 36 | 37 | if (context.url) { 38 | res.redirect(301, context.url); 39 | } else { 40 | res.send(template(body, initialData, userData)); 41 | } 42 | } 43 | 44 | export default render; 45 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/server/template.js: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | 3 | export default function template(body, initialData, userData) { 4 | return ` 5 | 6 | 7 | 8 | 9 | Pro MERN Stack 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 |
${body}
24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | `; 36 | } 37 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "rules": { 7 | "import/extensions": [ "error", "always", { "ignorePackages": true } ], 8 | "react/prop-types": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import store from './store.js'; 3 | import graphQLFetch from './graphQLFetch.js'; 4 | 5 | export default class About extends React.Component { 6 | static async fetchData() { 7 | const data = await graphQLFetch('query {about}'); 8 | return data; 9 | } 10 | 11 | constructor(props) { 12 | super(props); 13 | const apiAbout = store.initialData ? store.initialData.about : null; 14 | delete store.initialData; 15 | this.state = { apiAbout }; 16 | } 17 | 18 | async componentDidMount() { 19 | const { apiAbout } = this.state; 20 | if (apiAbout == null) { 21 | const data = await About.fetchData(); 22 | this.setState({ apiAbout: data.about }); 23 | } 24 | } 25 | 26 | render() { 27 | const { apiAbout } = this.state; 28 | return ( 29 |
30 |

Issue Tracker version 0.9

31 |

32 | {apiAbout} 33 |

34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/Contents.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | 4 | import routes from './routes.js'; 5 | 6 | export default function Contents() { 7 | return ( 8 | 9 | 10 | {routes.map(attrs => )} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/IssueDetail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IssueDetail({ issue }) { 4 | if (issue) { 5 | return ( 6 |
7 |

Description

8 |
{issue.description}
9 |
10 | ); 11 | } 12 | return null; 13 | } 14 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function NotFound() { 4 | return

Page Not Found

; 5 | } 6 | 7 | export default NotFound; 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/NumInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(num) { 4 | return num != null ? num.toString() : ''; 5 | } 6 | 7 | function unformat(str) { 8 | const val = parseInt(str, 10); 9 | return Number.isNaN(val) ? null : val; 10 | } 11 | 12 | export default class NumInput extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { value: format(props.value) }; 16 | this.onBlur = this.onBlur.bind(this); 17 | this.onChange = this.onChange.bind(this); 18 | } 19 | 20 | onChange(e) { 21 | if (e.target.value.match(/^\d*$/)) { 22 | this.setState({ value: e.target.value }); 23 | } 24 | } 25 | 26 | onBlur(e) { 27 | const { onChange } = this.props; 28 | const { value } = this.state; 29 | onChange(e, unformat(value)); 30 | } 31 | 32 | render() { 33 | const { value } = this.state; 34 | return ( 35 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SelectAsync from 'react-select/lib/Async'; // eslint-disable-line 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | import graphQLFetch from './graphQLFetch.js'; 6 | import withToast from './withToast.jsx'; 7 | 8 | class Search extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.onChangeSelection = this.onChangeSelection.bind(this); 13 | this.loadOptions = this.loadOptions.bind(this); 14 | } 15 | 16 | onChangeSelection({ value }) { 17 | const { history } = this.props; 18 | history.push(`/edit/${value}`); 19 | } 20 | 21 | async loadOptions(term) { 22 | if (term.length < 3) return []; 23 | const query = `query issueList($search: String) { 24 | issueList(search: $search) { 25 | issues {id title} 26 | } 27 | }`; 28 | 29 | const { showError } = this.props; 30 | const data = await graphQLFetch(query, { search: term }, showError); 31 | return data.issueList.issues.map(issue => ({ 32 | label: `#${issue.id}: ${issue.title}`, value: issue.id, 33 | })); 34 | } 35 | 36 | render() { 37 | return ( 38 | true} 43 | onChange={this.onChangeSelection} 44 | components={{ DropdownIndicator: null }} 45 | /> 46 | ); 47 | } 48 | } 49 | 50 | export default withRouter(withToast(Search)); 51 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function format(text) { 4 | return text != null ? text : ''; 5 | } 6 | 7 | function unformat(text) { 8 | return text.trim().length === 0 ? null : text; 9 | } 10 | 11 | export default class TextInput extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { value: format(props.value) }; 15 | this.onBlur = this.onBlur.bind(this); 16 | this.onChange = this.onChange.bind(this); 17 | } 18 | 19 | onChange(e) { 20 | this.setState({ value: e.target.value }); 21 | } 22 | 23 | onBlur(e) { 24 | const { onChange } = this.props; 25 | const { value } = this.state; 26 | onChange(e, unformat(value)); 27 | } 28 | 29 | render() { 30 | const { value } = this.state; 31 | const { tag = 'input', ...props } = this.props; 32 | return React.createElement(tag, { 33 | ...props, 34 | value, 35 | onBlur: this.onBlur, 36 | onChange: this.onChange, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/Toast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, Collapse } from 'react-bootstrap'; 3 | 4 | export default class Toast extends React.Component { 5 | componentDidUpdate() { 6 | const { showing, onDismiss } = this.props; 7 | if (showing) { 8 | clearTimeout(this.dismissTimer); 9 | this.dismissTimer = setTimeout(onDismiss, 5000); 10 | } 11 | } 12 | 13 | componentWillUnmount() { 14 | clearTimeout(this.dismissTimer); 15 | } 16 | 17 | render() { 18 | const { 19 | showing, bsStyle, onDismiss, children, 20 | } = this.props; 21 | return ( 22 | 23 |
27 | 28 | {children} 29 | 30 |
31 |
32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/UserContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UserContext = React.createContext({ 4 | signedIn: false, 5 | }); 6 | 7 | export default UserContext; 8 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/graphQLFetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d'); 4 | 5 | function jsonDateReviver(key, value) { 6 | if (dateRegex.test(value)) return new Date(value); 7 | return value; 8 | } 9 | 10 | export default async function 11 | graphQLFetch(query, variables = {}, showError = null, cookie = null) { 12 | const apiEndpoint = (__isBrowser__) // eslint-disable-line no-undef 13 | ? window.ENV.UI_API_ENDPOINT 14 | : process.env.UI_SERVER_API_ENDPOINT; 15 | try { 16 | const headers = { 'Content-Type': 'application/json' }; 17 | if (cookie) headers.Cookie = cookie; 18 | const response = await fetch(apiEndpoint, { 19 | method: 'POST', 20 | credentials: 'include', 21 | headers, 22 | body: JSON.stringify({ query, variables }), 23 | }); 24 | const body = await response.text(); 25 | const result = JSON.parse(body, jsonDateReviver); 26 | 27 | if (result.errors) { 28 | const error = result.errors[0]; 29 | if (error.extensions.code === 'BAD_USER_INPUT') { 30 | const details = error.extensions.exception.errors.join('\n '); 31 | if (showError) showError(`${error.message}:\n ${details}`); 32 | } else if (showError) { 33 | showError(`${error.extensions.code}: ${error.message}`); 34 | } 35 | } 36 | return result.data; 37 | } catch (e) { 38 | if (showError) showError(`Error in sending data to server: ${e.message}`); 39 | return null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/routes.js: -------------------------------------------------------------------------------- 1 | import IssueList from './IssueList.jsx'; 2 | import IssueReport from './IssueReport.jsx'; 3 | import IssueEdit from './IssueEdit.jsx'; 4 | import About from './About.jsx'; 5 | import NotFound from './NotFound.jsx'; 6 | 7 | const routes = [ 8 | { path: '/issues/:id?', component: IssueList }, 9 | { path: '/edit/:id', component: IssueEdit }, 10 | { path: '/report', component: IssueReport }, 11 | { path: '/about', component: About }, 12 | { path: '*', component: NotFound }, 13 | ]; 14 | 15 | export default routes; 16 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/store.js: -------------------------------------------------------------------------------- 1 | const store = {}; 2 | 3 | export default store; 4 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/src/withToast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Toast from './Toast.jsx'; 3 | 4 | export default function withToast(OriginalComponent) { 5 | return class ToastWrapper extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | toastVisible: false, toastMessage: '', toastType: 'success', 10 | }; 11 | this.showSuccess = this.showSuccess.bind(this); 12 | this.showError = this.showError.bind(this); 13 | this.dismissToast = this.dismissToast.bind(this); 14 | } 15 | 16 | showSuccess(message) { 17 | this.setState({ toastVisible: true, toastMessage: message, toastType: 'success' }); 18 | } 19 | 20 | showError(message) { 21 | this.setState({ toastVisible: true, toastMessage: message, toastType: 'danger' }); 22 | } 23 | 24 | dismissToast() { 25 | this.setState({ toastVisible: false }); 26 | } 27 | 28 | render() { 29 | const { toastType, toastVisible, toastMessage } = this.state; 30 | return ( 31 | 32 | 38 | 43 | {toastMessage} 44 | 45 | 46 | ); 47 | } 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /pro-mern-stack-2-15.07-non-proxy-mode/ui/webpack.serverHMR.js: -------------------------------------------------------------------------------- 1 | /* 2 | eslint-disable import/no-extraneous-dependencies 3 | */ 4 | const webpack = require('webpack'); 5 | const merge = require('webpack-merge'); 6 | const serverConfig = require('./webpack.config.js')[1]; 7 | 8 | module.exports = merge(serverConfig, { 9 | entry: { server: ['./node_modules/webpack/hot/poll?1000'] }, 10 | plugins: [ 11 | new webpack.HotModuleReplacementPlugin(), 12 | ], 13 | }); 14 | --------------------------------------------------------------------------------