diff --git a/.babelrc b/.babelrc index e70f5bb..68bdd9d 100644 --- a/.babelrc +++ b/.babelrc @@ -21,18 +21,12 @@ "env": { "test": { "plugins": [ - "transform-es2015-modules-commonjs", - "transform-object-rest-spread" + "transform-es2015-modules-commonjs" ] }, "server": { - "plugins": [ - "transform-es2015-modules-commonjs", - "transform-object-rest-spread" - ] + "plugins": + "transform-es2015-modules-commonjs" } - // "production": { - // "plugins": ["transform-es2015-modules-commonjs"] - // } } } \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 112fee6..6f68a43 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,12 +2,17 @@ "parser": "babel-eslint", "extends": [ "standard", - "standard-react" + "standard-react", + "plugin:jest/recommended" ], "rules": { "object-curly-spacing": [ "error", "always" ] + }, + "plugins": ["jest"], + "env": { + "jest/globals": true } -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index 700ac07..17a5309 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ data/* config.json public/* !public/.gitkeep -.vscode/* \ No newline at end of file +.vscode +coverage \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 70c0d6b..dc8ec7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "6" + - "8" env: - CXX=g++-4.8 addons: diff --git a/CHANGELOG.md b/CHANGELOG.md index cbc1480..30a1369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 2.0.0 (1/31/2017) :tada: + +Many of the core libraries and tools have had major releases, as they should, so this major release is to account for any breaking changes they've introduced. Most of the breakage occured with the Bootstrap upgrade from alpha to beta 3, but Bootstrap 4 should remain much more stable from beta to release. The rest of the updates were relatively painless. Hello React 16! + +Major changes: + +- React 15 -> React 16 +- Webpack 2 -> Webpack 3 +- Bootstrap 4 alpha -> Bootstrap 4 beta +- Implements Jest more fully with new lint rules and supporting dependencies +- Upgrade TravisCI's Node environment from 6 to 8 +- All other dependencies upgraded, some including major version bumps +- various refactors and cleanup + ## 1.0.0 (05/19/2017) This marks the first release of the boilerplate! :tada: diff --git a/README.md b/README.md index b03b9ee..a0ddd31 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ ### [Live Demo](https://react-ssr-boilerplate.matttrifilo.com/) -A minimalistic boiler plate with everything you need to get a server side rendered React application up and running FAST. +A minimalistic boilerplate with everything you need to get a server side rendered React application up and running FAST. -This is meant to be an alternative starter project for Free Code Camp full stack application projects. It's not officially affiliated with Free Code Camp. +This was originally meant to be an alternative starter project for Free Code Camp full stack application projects. It's not officially affiliated with Free Code Camp. Many of the implementations here are opinionated, so feel free to refactor them, or gut them, in anyway you see fit to work for your way of thinking and your use case. ## Batteries Included :battery: -You get everything you need to build out a full stack application with: +You get everything you need to build out a full stack application with: - React - React Router 4 - Redux @@ -22,22 +22,22 @@ You get everything you need to build out a full stack application with: - Mongoose - Passport -Out of the box, the boilerplate comes with a few views, and working local and OAuth authentication to get you going. +Out of the box, the boilerplate comes with a few views, and working local and OAuth authentication to get you going. -The React application itself is what is called a universal (formerly "isomorphic") JavaScript application. This means that on the server, React will render its component tree into HTML markup as a big string, and pass it down to the browser. Instead of the browser having to wait for a JavaScript bundle with the React application, it can simply show the HTML it's getting right away while the JavaScript bundles download. Once the React application code finishes downloading to the client, the JavaScript will be able to handle user interactions without needing to re-render the markup that already arrived pre-rendered from the server. This has huge performance wins for users with slow connections and is becoming a common way serve production React applications, in addition to other frameworks like Angular 2 and 4. +The React application itself is what is called a universal (formerly "isomorphic") JavaScript application. This means that on the server, React will render its component tree into HTML markup as a big string, and pass it down to the browser. Instead of the browser having to wait for a JavaScript bundle with the React application, it can simply show the HTML it's getting right away while the JavaScript bundles download. Once the React application code finishes downloading to the client, the JavaScript will be able to handle user interactions without needing to re-render the markup that already arrived pre-rendered from the server. This has huge performance wins for users with slow connections and is becoming a common way serve production React applications, in addition to other frameworks like Angular and Vue. ### Why another boilerplate? :dancers: Free Code Camp encourages students to use a boilerplate collection called `Clementine.js`, which is very powerful and easy to set up. It has great documentation and tutorials so it's the way to go if you're new to building full stack applications. -This boilerplate is meant to be an alternative for students who want to build their full stack application projects using modern tools from the React ecosystem, with a project structure that is tailored to using React and Redux. +This boilerplate is meant to be an alternative for anyone who wants to build full stack applications using modern tools from the React ecosystem, with a project structure that is tailored to using React and Redux. -There are many mature React boilerplates out their with dozens (or hundreds!) of smart, experienced developers contributing to them all the time, but a lot of them are overly complex for just building a basic full stack application in my opinion. This boilerplate isn't meant to address every use case, or be optimally polished for production ready apps. It's meant to be easy to understand and extend, while encouraging good habits for a good user and developer experience. +There are many mature React boilerplates out their with dozens (even hundreds!) of smart, experienced developers contributing to them all the time, but a lot of them are overly complex for just building a basic full stack application in my opinion. This boilerplate isn't meant to address every use case, or to be fully production ready. It's meant to be easy to understand and extend, while encouraging good habits for a good user and developer experience. -I encourage you to learn these technologies deeply before using this for a project, as it is fairly complex. I'll be working on documenting and commenting every aspect of this boilerplate to make it as clear and extendable as possible, but you'll get much more out of it if you have a solid understanding of how the underlying technologies like React, Redux, React Router 4, and Express all work and work together. Server side rendering has gotten easier over time, but that's another level of complexity that needs to be accounted for when adding features. +I recommend that you learn these technologies deeply before using this for a project, as it is fairly complex. I'll be working on documenting and commenting every aspect of this boilerplate to make it as clear and extendable as possible, but you'll get much more out of it if you have a solid understanding of how the underlying technologies like React, Redux, React Router 4, Mongo, and Express all work and work together. Server side rendering has gotten easier, but it's another level of complexity that needs to be accounted for when adding features. + +That said, I think it's beneficial to challenge yourself with modern tools you'll likely find at companies using React every day. If you can master these technologies, you'll be in great shape build whatever you can dream up. -That said, I think it's beneficial to challenge yourself with modern tools you'll likely find at companies using React every day. - ### Roadmap :milky_way: * Thorough documentation @@ -75,14 +75,14 @@ Here are the constants it contains: // The full domain for Passport to use in the GitHub strategy. // For actual DNS names, the port will not be necessary "domain": "http://localhost:4000", - // If you're running MongoDB locally, this address will work fine with + // If you're running MongoDB locally, this address will work fine with // the mongo NPM script during development. "mongoUriDev": "mongodb://localhost:27017/react-ssr-boiler", // You can set up a free MongoDB instance at mlab.com "mongoUriProduction": "ENTER_YOUR_PRODUCTION_MONGO_URI_FROM_MLAB_OR_ELSEWHERE", // A complex string to be used for creating and verifying JSON Web Tokens "jwtSecret": "PickAComplexString1337", - // Your app's GitHub credentials. + // Your app's GitHub credentials. // Go to github.com, log in, go to settings, then scroll down to "developer settings" // and you'll find "OAuth Applications". Go there to register your new application. // The callback URL will be `${domain}/api/login/github/callback` diff --git a/client/auth/__test__/setTokenHeaders.test.js b/client/auth/__test__/setTokenHeaders.test.js new file mode 100644 index 0000000..0d5a06e --- /dev/null +++ b/client/auth/__test__/setTokenHeaders.test.js @@ -0,0 +1,20 @@ +const axios = require('axios') + +const setTokenToHeaders = require('../../auth/setTokenToHeaders') + +afterEach(() => { + if (axios.defaults.headers.common['Authorization']) { + delete axios.defaults.headers.common['Authorization'] + } +}) + +test('with token: should set Authorization header with the token', () => { + setTokenToHeaders('testToken') + expect(axios.defaults.headers.common['Authorization']).toBe('Bearer testToken') +}) + +test('without token: should delete token if present', () => { + axios.defaults.headers.common['Authorization'] = 'testToken' + setTokenToHeaders() + expect(axios.defaults.headers.common['Authorization']).toBeUndefined() +}) diff --git a/client/auth/setTokenToHeaders.js b/client/auth/setTokenToHeaders.js index 2ac9040..5b59a0c 100644 --- a/client/auth/setTokenToHeaders.js +++ b/client/auth/setTokenToHeaders.js @@ -3,7 +3,7 @@ const axios = require('axios') function setTokenToHeaders (token) { if (token) { axios.defaults.headers.common['Authorization'] = `Bearer ${token}` - } else { + } else if (axios.defaults.headers.common['Authorization']) { delete axios.defaults.headers.common['Authorization'] } } diff --git a/client/components/Account/AccountSettings.js b/client/components/Account/AccountSettings.js index 9c0aca6..c80c3d0 100644 --- a/client/components/Account/AccountSettings.js +++ b/client/components/Account/AccountSettings.js @@ -45,18 +45,24 @@ class AccountSettings extends Component { onChangeHandler = evt => { this.setState({ [evt.target.name]: evt.target.value }) - }; + } onBlurHandler = evt => { if (evt.target.name === 'newUsername') { this.setValidationError(validateNewUsername(this.state.newUsername)) - if (this.state.newUsername !== this.props.username && this.state.newUsername !== '') { + if ( + this.state.newUsername !== this.props.username && + this.state.newUsername !== '' + ) { this.props.dispatchCheckUsernameUniqueness(this.state.newUsername) } } if (evt.target.name === 'newEmail') { this.setValidationError(validateNewEmail(this.state.newEmail)) - if (this.state.newEmail !== this.props.email && this.state.newEmail !== '') { + if ( + this.state.newEmail !== this.props.email && + this.state.newEmail !== '' + ) { this.props.dispatchCheckEmailUniqueness(this.state.newEmail) } } @@ -76,7 +82,7 @@ class AccountSettings extends Component { ) ) } - }; + } setValidationError = validationResult => { // set the validtion result to state @@ -88,7 +94,7 @@ class AccountSettings extends Component { this.setState({ validationErrors: newValidationErrors }, () => { this.checkFormValidity() }) - }; + } checkFormValidity = () => { const { @@ -106,7 +112,9 @@ class AccountSettings extends Component { identifiersValid = false } if ( - currentPassword !== '' || newPassword !== '' || confirmNewPassword !== '' + currentPassword !== '' || + newPassword !== '' || + confirmNewPassword !== '' ) { passwordValid = false } @@ -117,12 +125,12 @@ class AccountSettings extends Component { password: passwordValid } }) - }; + } onClickDeleteAccount = evt => { evt.preventDefault() this.props.dispatchDeleteUserAccount() - }; + } componentDidMount () { this.props.dispatchGetCurrentUser() @@ -154,19 +162,20 @@ class AccountSettings extends Component { return (

Account Settings

- {this.props.gitHubToken - ? - : + ) : ( + } + /> + )}
) diff --git a/client/components/Account/GitHubAccountSettings.js b/client/components/Account/GitHubAccountSettings.js index ef1e6d4..bf6b5de 100644 --- a/client/components/Account/GitHubAccountSettings.js +++ b/client/components/Account/GitHubAccountSettings.js @@ -18,7 +18,7 @@ class GitHubAccountSettings extends Component { this.props.dispatchChangeGitHubUsername({ newUsername: this.props.newUsername }) - }; + } render () { return ( diff --git a/client/components/Account/LocalAccountSettings/ChangeIdentifierForm.js b/client/components/Account/LocalAccountSettings/ChangeIdentifierForm.js index fb492c4..27d7839 100644 --- a/client/components/Account/LocalAccountSettings/ChangeIdentifierForm.js +++ b/client/components/Account/LocalAccountSettings/ChangeIdentifierForm.js @@ -26,16 +26,13 @@ class ChangeIdentifierForm extends Component { if (!isEmpty(userDataChanges)) { console.log('userDataChanges', userDataChanges) - this.props.dispatchChangeUserIdentifiers( - userDataChanges, - this.props.user - ) + this.props.dispatchChangeUserIdentifiers(userDataChanges, this.props.user) } else { console.log('no changes to submit') // dispatch flashMessage to inform the user that there // are no changes to submit } - }; + } render () { return ( diff --git a/client/components/Account/LocalAccountSettings/ChangePasswordForm.js b/client/components/Account/LocalAccountSettings/ChangePasswordForm.js index eecae92..df31e0d 100644 --- a/client/components/Account/LocalAccountSettings/ChangePasswordForm.js +++ b/client/components/Account/LocalAccountSettings/ChangePasswordForm.js @@ -17,7 +17,7 @@ class ChangePasswordForm extends Component { return this.props.setValidationError(validation.validationErrors) } this.props.dispatchChangeUserPassword(passwordData) - }; + } render () { return ( diff --git a/client/components/BrowserEntry.js b/client/components/BrowserEntry.js index dba184c..a958f9d 100644 --- a/client/components/BrowserEntry.js +++ b/client/components/BrowserEntry.js @@ -5,4 +5,6 @@ import '../../node_modules/bootstrap/dist/js/bootstrap.js' import '../../node_modules/bootstrap/dist/css/bootstrap.css' import './main.css' -ReactDOM.render(, document.getElementById('app')) +window.addEventListener('DOMContentLoaded', () => { + ReactDOM.hydrate(, document.getElementById('app')) +}) diff --git a/client/components/Common/FlashMessage.js b/client/components/Common/FlashMessage.js index 64c54d7..29ab3c3 100644 --- a/client/components/Common/FlashMessage.js +++ b/client/components/Common/FlashMessage.js @@ -17,13 +17,13 @@ class FlashMessage extends Component { this.setState({ display: false }) }, 1500) } - }; + } componentDidMount = () => { setTimeout(() => { this.setState({ display: true }) }, 50) - }; + } render () { this.showMessage() diff --git a/client/components/Common/Input.js b/client/components/Common/Input.js index f512d8e..d6ac917 100644 --- a/client/components/Common/Input.js +++ b/client/components/Common/Input.js @@ -22,10 +22,10 @@ const Input = ({ name={name} onChange={onChange} onBlur={onBlur} - className='form-control' + className={classNames('form-control', { 'is-invalid': validationError })} /> {validationError && -
+
{validationError}
}
diff --git a/client/components/Common/ShowIfLoggedOut.js b/client/components/Common/ShowIfLoggedOut.js new file mode 100644 index 0000000..201d7a4 --- /dev/null +++ b/client/components/Common/ShowIfLoggedOut.js @@ -0,0 +1,31 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { Redirect } from 'react-router' +import { array, bool } from 'prop-types' + +class ShowIfLoggedOut extends Component { + render () { + if (this.props.isAuthenticated) { + return + } + + if (this.props.isAuthenticated !== false) { + return null + } + + return
{this.props.children}
+ } +} + +ShowIfLoggedOut.propTypes = { + children: array.isRequired, + isAuthenticated: bool.isRequired +} + +const mapStateToProps = state => { + return { + isAuthenticated: state.user.isAuthenticated + } +} + +export default connect(mapStateToProps)(ShowIfLoggedOut) diff --git a/client/components/Login/Login.js b/client/components/Login/Login.js index 6651db3..369a2f4 100644 --- a/client/components/Login/Login.js +++ b/client/components/Login/Login.js @@ -1,13 +1,23 @@ import React from 'react' +// import { connect } from 'react-redux' +// import { Redirect } from 'react-router' +// import { bool, object } from 'prop-types' + import LoginForm from './LoginForm' import LoginGithub from './LoginGithub' +import ShowIfLoggedOut from '../Common/ShowIfLoggedOut' -const Login = props => ( -
-

Login

- - -
-) +const Login = props => { + console.log('props:', props) + return ( +
+ +

Login

+ + +
+
+ ) +} export default Login diff --git a/client/components/Login/LoginForm.js b/client/components/Login/LoginForm.js index 2e8095a..545f1d0 100644 --- a/client/components/Login/LoginForm.js +++ b/client/components/Login/LoginForm.js @@ -1,5 +1,4 @@ import React, { Component } from 'react' -import { Redirect } from 'react-router' import { connect } from 'react-redux' import { loginRequest } from '../../redux/modules/loginLocal' import Input from '../Common/Input' @@ -8,11 +7,11 @@ import { validateEmail, validatePassword } from '../../../server/validation/loginFormValidation' -import { func, object, bool } from 'prop-types' +import { func, object } from 'prop-types' class LoginForm extends Component { - constructor () { - super() + constructor (props) { + super(props) this.state = { email: '', password: '', @@ -25,7 +24,7 @@ class LoginForm extends Component { onChangeHandler = evt => { this.setState({ [evt.target.name]: evt.target.value }) - }; + } onBlurHandler = evt => { if (evt.target.name === 'email') { @@ -34,7 +33,7 @@ class LoginForm extends Component { if (evt.target.name === 'password') { this.setValidationError(validatePassword(this.state.password)) } - }; + } setValidationError = validationResult => { // Set validation result to state @@ -44,7 +43,7 @@ class LoginForm extends Component { validationResult ) this.setState({ validationErrors: newValidationErrors }) - }; + } onSubmitHandler = evt => { evt.preventDefault() @@ -56,13 +55,9 @@ class LoginForm extends Component { } else { return this.setValidationError(validation.validationErrors) } - }; + } render () { - if (this.props.isAuthenticated) { - return - } - return (
{ return { - loginLoading: state.loginLocal.loginLoading, - isAuthenticated: state.user.isAuthenticated + loginLoading: state.loginLocal.loginLoading } } diff --git a/client/components/Login/LoginGithub.js b/client/components/Login/LoginGithub.js index 71b4f15..850aba9 100644 --- a/client/components/Login/LoginGithub.js +++ b/client/components/Login/LoginGithub.js @@ -13,9 +13,7 @@ const LoginGithub = () => { strokeLinejoin='round' strokeMiterlimit='1.414' > - + diff --git a/client/components/NavBar/NavBar.js b/client/components/NavBar/NavBar.js index 06b3724..601d40d 100644 --- a/client/components/NavBar/NavBar.js +++ b/client/components/NavBar/NavBar.js @@ -20,7 +20,7 @@ class NavBar extends Component { } else { this.setState({ mounted: true }) } - }; + } componentWillReceiveProps = nextProps => { if (nextProps.isAuthenticated) { @@ -28,7 +28,7 @@ class NavBar extends Component { } else { this.setState({ showAuthenticatedLinks: false }) } - }; + } render () { let displayLinks @@ -41,7 +41,7 @@ class NavBar extends Component { } return ( -