diff --git a/README.md b/README.md index 5433baf..139e313 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@

-A starting point for building universal/isomorphic React applications with ASP.NET Core 1, leveraging existing front-end approaches. Uses the [JavaScriptViewEngine](https://github.com/pauldotknopf/javascriptviewengine). +A starting point for building universal/isomorphic React applications with ASP.NET Core 2, leveraging existing front-end approaches. Uses the [JavaScriptViewEngine](https://github.com/pauldotknopf/javascriptviewengine). ## Goals 1. **Minimize .NET's usage** - It's only usage should be for building REST endpoints (WebApi) and providing the initial state (pure POCO). No razor syntax *anywhere*. 2. **Isomorphic/universal rendering** -3. **Client and server should render using the same source files (javascript)** +3. **Client and server should render using the same source files (TypeScript)** 4. **Out-of-the-box login/register/manage functionality** - Use the branch ```empty-template``` if you wish to have a vanilla React application. This approach is great for front-end developers because it gives them complete control to build their app as they like. No .NET crutches (bundling/razor). No opinions. No gotchas. Just another typical React client-side application, but with the initial state provided by ASP.NET for each URL. @@ -40,7 +40,7 @@ After you have your new project generated, let's run that app! ```bash cd src/ReactBoilerplate npm install -gulp +gulp build dotnet restore # The following two lines are only for the 'master' branch, which has a database backend (user management). # They are not needed when using 'empty-template'. @@ -49,6 +49,21 @@ dotnet ef database update dotnet run ``` +Additionally, other gulp actions are available to aid in development. + +Build bundles and server-side code in production mode: +```bash +gulp prod build +``` +Build and watch both the bundles and the server-side code (in development mode), and run a browsersync proxy to serve both: +```bash +gulp start +``` + +Build both the bundles and the server-side code (in production mode), and execute "dotnet run" to serve both: +```bash +gulp prod start +``` Some of the branches in this repo that are maintained: * [```master```](https://github.com/pauldotknopf/react-aspnet-boilerplate/tree/master) - This is the main branch. It has all the stuff required to get you started, including membership, external logins (OAuth) and account management. This is the default branch used with the Yeoman generator. @@ -56,8 +71,8 @@ Some of the branches in this repo that are maintained: ## The interesting parts -- [client.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/client.js) and [server.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/server.js) - The entry point for the client-side/server-side applications. -- [Html.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/helpers/Html.js) and [App.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/containers/App/App.js) - These files essentially represent the "React" version of MVC Razor's "_Layout.cshtml". +- [client.tsx](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/client.tsx) and [server.tsx](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/server.tsx) - The entry point for the client-side/server-side applications. +- [Html.tsx](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/helpers/Html.tsx) and [App.tsx](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/containers/App/App.tsx) - These files essentially represent the "React" version of MVC Razor's "_Layout.cshtml". - [Controllers](https://github.com/pauldotknopf/react-aspnet-boilerplate/tree/master/src/ReactBoilerplate/Controllers) - The endpoints for a each initial GET request, and each client-side network request. ## What is next? diff --git a/src/ReactBoilerplate/.babelrc b/src/ReactBoilerplate/.babelrc deleted file mode 100644 index 2fe3a45..0000000 --- a/src/ReactBoilerplate/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["react", "es2015", "stage-0"], - "plugins": [] -} \ No newline at end of file diff --git a/src/ReactBoilerplate/.bootstraprc b/src/ReactBoilerplate/.bootstraprc index 962b9d5..592d2dc 100644 --- a/src/ReactBoilerplate/.bootstraprc +++ b/src/ReactBoilerplate/.bootstraprc @@ -1,11 +1,18 @@ -{ - "bootstrapVersion": 3, +bootstrapVersion: 3 - "useFlexbox": true, - "extractStyles": true, - "styleLoaders": ["style", "css", "sass"], +env: + development: + extractStyles: false + styleLoaders: + - style-loader + - css-loader + - sass-loader + production: + extractStyles: true + styleLoaders: + - css-loader + - sass-loader - "styles": true, +styles: true - "scripts": false -} \ No newline at end of file +scripts: false \ No newline at end of file diff --git a/src/ReactBoilerplate/.editorconfig b/src/ReactBoilerplate/.editorconfig index bb1f190..097e288 100644 --- a/src/ReactBoilerplate/.editorconfig +++ b/src/ReactBoilerplate/.editorconfig @@ -5,5 +5,9 @@ root = true indent_style = space indent_size = 2 +[*.cs] +indent_style = space +indent_size = 4 + [*.md] trim_trailing_whitespace = false diff --git a/src/ReactBoilerplate/.eslintrc b/src/ReactBoilerplate/.eslintrc index fd71550..fdebf4b 100644 --- a/src/ReactBoilerplate/.eslintrc +++ b/src/ReactBoilerplate/.eslintrc @@ -4,35 +4,76 @@ "browser": true, "node": true }, - "parser": "babel-eslint", + "parser": "typescript-eslint-parser", "plugins": [ "react", - "import" + "import", + "typescript" ], "rules": { - "comma-dangle": 0, // not sure why airbnb turned this on. gross! - "indent": [2, 2, {"SwitchCase": 1}], - "react/prefer-stateless-function": 0, - "react/prop-types": 0, - "react/jsx-closing-bracket-location": 0, - "no-console": 0, - "prefer-template": 0, - "max-len": 0, - "no-underscore-dangle": [2, {"allow": ["__data"]}], - "global-require": 0, - "no-restricted-syntax": 0, - "linebreak-style": 0, - "react/jsx-filename-extension": 0, - "import/imports-first": 0, - "no-class-assign": 0 + "comma-dangle": "off", // Not sure why airbnb turned this on. Gross! + "indent": ["error", 2, {"SwitchCase": 1}], + "react/prefer-stateless-function": "off", + "react/prop-types": "off", + "react/jsx-closing-bracket-location": "off", + "no-console": "off", + "prefer-template": "off", + "max-len": "off", + "no-underscore-dangle": ["error", {"allow": ["__data"]}], + "global-require": "off", + "no-restricted-syntax": "off", + "linebreak-style": "off", + "react/jsx-filename-extension": "off", + "arrow-parens": ["error", "always"], + "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], + "jsx-a11y/anchor-is-valid": ["error", { + "specialLink": ["to"] // Allow "to" property in react-router-dom Link element + }], + "no-undef": "off", // TypeScript lint parser produces false positves; the compiler will error anyway + "no-unused-vars": "off", // TypeScript lint parser produces false positves; the compiler is set to error instead + "import/extensions": ["error", "ignorePackages", { + "js": "never", + "jsx": "never", + "ts": "never", + "tsx": "never" + }], + "react/sort-comp": ["error", { // Add type-annotations to the top + "order": [ + "static-methods", + "type-annotations", + "instance-variables", + "lifecycle", + "/^on.+$/", + "getters", + "setters", + "/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/", + "instance-methods", + "everything-else", + "rendering" + ] + }], + "typescript/adjacent-overload-signatures": ["error"], + "typescript/class-name-casing": ["error"], + "typescript/explicit-member-accessibility": ["error"], + "typescript/interface-name-prefix": ["error", "never"], + "typescript/member-delimiter-style": ["error"], + "typescript/no-angle-bracket-type-assertion": ["error"], + "typescript/no-array-constructor": ["error"], + "typescript/no-empty-interface": ["error"], + "typescript/no-inferrable-types": ["error"], + "typescript/no-unused-vars": ["error"], + "typescript/no-use-before-define": ["error"], + "typescript/type-annotation-spacing": ["error"] }, "settings": { - "import/parser": "babel-eslint", - "import/resolver": { + "import/parser": "typescript-eslint-parser", + "import/resolver": { "node": { - "moduleDirectory": ["node_modules", "Scripts"] + "moduleDirectory": ["node_modules", "Scripts"], + "extensions": [".js", ".jsx", ".ts", ".tsx"] } }, + "import/extensions": [".js", ".jsx", ".ts", ".tsx"], "no-underscore-dangle": { "allow": ["__data"] } diff --git a/src/ReactBoilerplate/.stylelintrc b/src/ReactBoilerplate/.stylelintrc new file mode 100644 index 0000000..77bc086 --- /dev/null +++ b/src/ReactBoilerplate/.stylelintrc @@ -0,0 +1,29 @@ +{ + "extends": [ + "stylelint-config-standard" + ], + "rules": { + 'selector-pseudo-class-no-unknown': [ true, { + ignorePseudoClasses: [ + 'export', + 'import', + 'global', + 'local' + ], + }], + 'property-no-unknown': [ true, { + ignoreProperties: [ + 'composes', + 'compose-with' + ], + }], + 'at-rule-no-unknown': [ true, { + ignoreAtRules: [ + 'each', + 'extend', + 'include', + 'mixin', + ], + }] + } +} diff --git a/src/ReactBoilerplate/Scripts/client.js b/src/ReactBoilerplate/Scripts/client.js deleted file mode 100644 index 704bc92..0000000 --- a/src/ReactBoilerplate/Scripts/client.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Router, browserHistory } from 'react-router'; -import getRoutes from './routes'; -import { Provider } from 'react-redux'; -import configureStore from './redux/configureStore'; -import { syncHistoryWithStore } from 'react-router-redux'; -import ApiClient from './helpers/ApiClient'; - -const client = new ApiClient(); -const store = configureStore(window.__data, browserHistory, client); -const history = syncHistoryWithStore(browserHistory, store); - -ReactDOM.render( - - - {getRoutes(store)} - - , - document.getElementById('content') -); diff --git a/src/ReactBoilerplate/Scripts/client.tsx b/src/ReactBoilerplate/Scripts/client.tsx new file mode 100644 index 0000000..0217c3e --- /dev/null +++ b/src/ReactBoilerplate/Scripts/client.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import createHistory from 'history/createBrowserHistory'; +import { ConnectedRouter } from 'react-router-redux'; +import { AppContainer } from 'react-hot-loader'; +import configureStore from './redux/configureStore'; +import ApiClient from './helpers/ApiClient'; + +// Need to import App directly from its file so that this module is dependent on it directly for HMR +import App from './containers/App/App'; + +const client = new ApiClient(); +const history = createHistory(); +const store = configureStore((window as any).__data, history, client); + +const render = () => { + ReactDOM.render( + + + + + + + , + document.getElementById('content') + ); +}; + +// Render the App inside the store and HMR AppContainer +render(); + +// Register to accept changes to the App +if (module.hot) { + module.hot.accept('./containers/App/App', () => { render(); }); +} diff --git a/src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.js b/src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.tsx similarity index 66% rename from src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.js rename to src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.tsx index 24281da..77c0e83 100644 --- a/src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.js +++ b/src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.tsx @@ -1,19 +1,20 @@ -import React from 'react'; -import Form from 'components/Form'; +import * as React from 'react'; import { reduxForm } from 'redux-form'; -import { Input } from 'components'; -import { changeEmail } from 'redux/modules/manage'; +import { RootState } from '../../redux/reducer'; +import Form from '../Form'; +import { Input } from '../../components'; +import { changeEmail } from '../../redux/modules/manage/email'; -class ChangeEmailForm extends Form { - constructor(props) { +class ChangeEmailForm extends Form<{}, { success: boolean }> { + public constructor(props: any) { super(props); this.success = this.success.bind(this); this.state = { success: false }; } - success() { + public success() { this.setState({ success: true }); } - render() { + public render() { const { fields: { currentPassword, email, emailConfirm } } = this.props; @@ -27,7 +28,7 @@ class ChangeEmailForm extends Form { An email has been sent to your email to confirm the change.

} - {!success && + {!success && currentPassword && email && emailConfirm &&
@@ -47,12 +48,11 @@ class ChangeEmailForm extends Form { } } -ChangeEmailForm = reduxForm({ - form: 'changeEmail', - fields: ['currentPassword', 'email', 'emailConfirm'] -}, -(state) => state, -{ } +export default reduxForm( + { + form: 'changeEmail', + fields: ['currentPassword', 'email', 'emailConfirm'] + }, + (state: RootState) => state, + { } )(ChangeEmailForm); - -export default ChangeEmailForm; diff --git a/src/ReactBoilerplate/Scripts/components/ChangePasswordForm/ChangePasswordForm.js b/src/ReactBoilerplate/Scripts/components/ChangePasswordForm/ChangePasswordForm.tsx similarity index 65% rename from src/ReactBoilerplate/Scripts/components/ChangePasswordForm/ChangePasswordForm.js rename to src/ReactBoilerplate/Scripts/components/ChangePasswordForm/ChangePasswordForm.tsx index 069fccf..ef836a0 100644 --- a/src/ReactBoilerplate/Scripts/components/ChangePasswordForm/ChangePasswordForm.js +++ b/src/ReactBoilerplate/Scripts/components/ChangePasswordForm/ChangePasswordForm.tsx @@ -1,19 +1,20 @@ -import React from 'react'; -import Form from 'components/Form'; +import * as React from 'react'; import { reduxForm } from 'redux-form'; -import { Input } from 'components'; -import { changePassword } from 'redux/modules/manage'; +import Form from '../../components/Form'; +import { Input } from '../../components'; +import { changePassword } from '../../redux/modules/manage/changePassword'; +import { RootState } from '../../redux/reducer'; -class ChangePasswordForm extends Form { - constructor(props) { +class ChangePasswordForm extends Form<{}, { success: boolean }> { + public constructor(props: any) { super(props); this.success = this.success.bind(this); this.state = { success: false }; } - success() { + public success() { this.setState({ success: true }); } - render() { + public render() { const { fields: { oldPassword, newPassword, newPasswordConfirm } } = this.props; @@ -27,7 +28,7 @@ class ChangePasswordForm extends Form { Your password has been changed.

} - {!success && + {!success && oldPassword && newPassword && newPasswordConfirm && @@ -47,12 +48,11 @@ class ChangePasswordForm extends Form { } } -ChangePasswordForm = reduxForm({ - form: 'changePassword', - fields: ['oldPassword', 'newPassword', 'newPasswordConfirm'] -}, -(state) => state, -{ } +export default reduxForm( + { + form: 'changePassword', + fields: ['oldPassword', 'newPassword', 'newPasswordConfirm'] + }, + (state: RootState) => state, + { } )(ChangePasswordForm); - -export default ChangePasswordForm; diff --git a/src/ReactBoilerplate/Scripts/components/ErrorList.js b/src/ReactBoilerplate/Scripts/components/ErrorList.js deleted file mode 100644 index 83c7b54..0000000 --- a/src/ReactBoilerplate/Scripts/components/ErrorList.js +++ /dev/null @@ -1,29 +0,0 @@ -import React, { Component } from 'react'; -import { Glyphicon } from 'react-bootstrap'; - -class ErrorList extends Component { - render() { - const { - errors - } = this.props; - if (!errors) return null; - if (Array.isArray(errors)) { - if (errors.length === 0) return null; - return ( -
- {errors.map((err, i) => - ( -

- - {' '} - {err} -

- ))} -
- ); - } - return null; - } -} - -export default ErrorList; diff --git a/src/ReactBoilerplate/Scripts/components/ErrorList.tsx b/src/ReactBoilerplate/Scripts/components/ErrorList.tsx new file mode 100644 index 0000000..cfa3173 --- /dev/null +++ b/src/ReactBoilerplate/Scripts/components/ErrorList.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { Glyphicon } from 'react-bootstrap'; + +export interface ErrorListProps { + errors?: string[]; +} + +class ErrorList extends React.Component { + public render() { + const { + errors + } = this.props; + if (!errors) return null; + if (Array.isArray(errors)) { + if (errors.length === 0) return null; + return ( +
+ {errors.map((err) => + ( +

+ + {' '} + {err} +

+ )) + } +
+ ); + } + return null; + } +} + +export default ErrorList; diff --git a/src/ReactBoilerplate/Scripts/components/ExternalLogin/ExternalLogin.js b/src/ReactBoilerplate/Scripts/components/ExternalLogin/ExternalLogin.js deleted file mode 100644 index bdc1e5e..0000000 --- a/src/ReactBoilerplate/Scripts/components/ExternalLogin/ExternalLogin.js +++ /dev/null @@ -1,41 +0,0 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { authenticate } from 'redux/modules/externalLogin'; -import { ExternalLoginButton } from 'components'; - -class ExternalLogin extends Component { - constructor(props) { - super(props); - this.loginClick = this.loginClick.bind(this); - } - loginClick(scheme) { - return (event) => { - event.preventDefault(); - this.props.authenticate(scheme); - }; - } - render() { - const { - loginProviders - } = this.props; - return ( -

- {loginProviders.map((loginProvider, i) => - ( - - - {' '} - - ))} -

- ); - } -} - -export default connect( -(state) => ({ loginProviders: state.externalLogin.loginProviders, location: state.routing.locationBeforeTransitions }), -{ authenticate } -)(ExternalLogin); diff --git a/src/ReactBoilerplate/Scripts/components/ExternalLogin/ExternalLogin.tsx b/src/ReactBoilerplate/Scripts/components/ExternalLogin/ExternalLogin.tsx new file mode 100644 index 0000000..d9983ce --- /dev/null +++ b/src/ReactBoilerplate/Scripts/components/ExternalLogin/ExternalLogin.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { authenticate, ExternalLoginProvider } from '../../redux/modules/externalLogin'; +import { ExternalLoginButton } from '../../components'; +import { RootState } from '../../redux/reducer'; + +export interface ExternalLoginProps { + authenticate: (scheme: string) => any; + loginProviders: ExternalLoginProvider[]; + leadingText?: string; +} + +class ExternalLogin extends React.Component { + public constructor(props: ExternalLoginProps) { + super(props); + this.loginClick = this.loginClick.bind(this); + } + public loginClick(scheme: string) { + return (event: any) => { + event.preventDefault(); + this.props.authenticate(scheme); + }; + } + public render() { + const { + loginProviders + } = this.props; + return ( +

+ {loginProviders.map((loginProvider) => + ( + + + {' '} + + )) + } +

+ ); + } +} + +export default connect( + (state: RootState) => ({ loginProviders: state.externalLogin.loginProviders }), + { authenticate } +)(ExternalLogin); diff --git a/src/ReactBoilerplate/Scripts/components/ExternalLoginButton.js b/src/ReactBoilerplate/Scripts/components/ExternalLoginButton.js deleted file mode 100644 index 6a23a23..0000000 --- a/src/ReactBoilerplate/Scripts/components/ExternalLoginButton.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { Button } from 'react-bootstrap'; - -const bootstrapSocial = require('bootstrap-social'); -const fontAwesome = require('font-awesome/scss/font-awesome.scss'); - -export default (props) => -( - -); diff --git a/src/ReactBoilerplate/Scripts/components/ExternalLoginButton.tsx b/src/ReactBoilerplate/Scripts/components/ExternalLoginButton.tsx new file mode 100644 index 0000000..de763e2 --- /dev/null +++ b/src/ReactBoilerplate/Scripts/components/ExternalLoginButton.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { Button } from 'react-bootstrap'; + +const bootstrapSocial = require('bootstrap-social'); +const fontAwesome = require('font-awesome/scss/font-awesome.scss'); + +export default (props: any) => + ( + + ); diff --git a/src/ReactBoilerplate/Scripts/components/ForgotPasswordForm/ForgotPasswordForm.js b/src/ReactBoilerplate/Scripts/components/ForgotPasswordForm/ForgotPasswordForm.tsx similarity index 64% rename from src/ReactBoilerplate/Scripts/components/ForgotPasswordForm/ForgotPasswordForm.js rename to src/ReactBoilerplate/Scripts/components/ForgotPasswordForm/ForgotPasswordForm.tsx index 281b9f7..75fc965 100644 --- a/src/ReactBoilerplate/Scripts/components/ForgotPasswordForm/ForgotPasswordForm.js +++ b/src/ReactBoilerplate/Scripts/components/ForgotPasswordForm/ForgotPasswordForm.tsx @@ -1,19 +1,20 @@ -import React from 'react'; -import Form from 'components/Form'; +import * as React from 'react'; import { reduxForm } from 'redux-form'; -import { Input } from 'components'; -import { forgotPassword } from 'redux/modules/account'; +import Form from '../../components/Form'; +import { Input } from '../../components'; +import { forgotPassword } from '../../redux/modules/account'; +import { RootState } from '../../redux/reducer'; -class ForgotPasswordForm extends Form { - constructor(props) { +class ForgotPasswordForm extends Form<{}, { success: boolean }> { + public constructor(props: any) { super(props); this.success = this.success.bind(this); this.state = { success: false }; } - success() { + public success() { this.setState({ success: true }); } - render() { + public render() { const { fields: { email } } = this.props; @@ -27,7 +28,7 @@ class ForgotPasswordForm extends Form { Please check your email to reset your password.

} - {!success && + {!success && email && @@ -45,12 +46,11 @@ class ForgotPasswordForm extends Form { } } -ForgotPasswordForm = reduxForm({ - form: 'forgotPassword', - fields: ['email'] -}, -(state) => state, -{ } +export default reduxForm( + { + form: 'forgotPassword', + fields: ['email'] + }, + (state: RootState) => state, + { } )(ForgotPasswordForm); - -export default ForgotPasswordForm; diff --git a/src/ReactBoilerplate/Scripts/components/Form.js b/src/ReactBoilerplate/Scripts/components/Form.js deleted file mode 100644 index 1995adb..0000000 --- a/src/ReactBoilerplate/Scripts/components/Form.js +++ /dev/null @@ -1,53 +0,0 @@ -import React, { Component } from 'react'; -import { modelStateErrorToFormFields } from '../utils/modelState'; -import { ErrorList } from 'components'; - -class Form extends Component { - modifyValues(values) { - return values; - } - handleApiSubmit(action, success, error) { - const { - handleSubmit - } = this.props; - return handleSubmit((values, dispatch) => - new Promise((resolve, reject) => { - dispatch(action(this.modifyValues(values))) - .then( - (result) => { - if (result.success) { - resolve(); - if (success) { - success(result); - } - } else { - reject(modelStateErrorToFormFields(result.errors)); - if (error) { - error(result); - } - } - }, - (result) => { - reject(modelStateErrorToFormFields(result.errors)); - if (error) { - error(result); - } - }); - }) - ); - } - renderGlobalErrorList() { - const { - error - } = this.props; - if (!error) { - return null; - } - if (!error.errors) { - return null; - } - return (); - } -} - -export default Form; diff --git a/src/ReactBoilerplate/Scripts/components/Form.tsx b/src/ReactBoilerplate/Scripts/components/Form.tsx new file mode 100644 index 0000000..e19a8e6 --- /dev/null +++ b/src/ReactBoilerplate/Scripts/components/Form.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { modelStateErrorToFormFields } from '../utils/modelState'; +import { Field } from './Input'; +import ErrorList from './ErrorList'; + +interface FormProps { + error: { + errors: string[]; + }; + handleSubmit: (handler: (values: any, dispatch: (action: any) => any) => Promise) => (evt: any) => void; + fields: { [key: string]: Field | undefined; }; +} + +class Form

extends React.Component

{ + // eslint-disable-next-line class-methods-use-this + protected modifyValues(values: any) { + return values; + } + protected handleApiSubmit(action: (values: any) => any, success?: (result: any) => void, error?: (result: any) => void) { + const { + handleSubmit + } = this.props; + return handleSubmit((values, dispatch) => + new Promise((resolve, reject) => { + dispatch(action(this.modifyValues(values))) + .then( + (result: any) => { + if (result.success) { + resolve(); + if (success) { + success(result); + } + } else { + reject(modelStateErrorToFormFields(result.errors)); + if (error) { + error(result); + } + } + }, + (result: any) => { + reject(modelStateErrorToFormFields(result.errors)); + if (error) { + error(result); + } + } + ); + })); + } + protected renderGlobalErrorList() { + const { + error + } = this.props; + if (!error) { + return null; + } + if (!error.errors) { + return null; + } + return (); + } +} + +export default Form; diff --git a/src/ReactBoilerplate/Scripts/components/Input.js b/src/ReactBoilerplate/Scripts/components/Input.tsx similarity index 55% rename from src/ReactBoilerplate/Scripts/components/Input.js rename to src/ReactBoilerplate/Scripts/components/Input.tsx index b63b723..7a85846 100644 --- a/src/ReactBoilerplate/Scripts/components/Input.js +++ b/src/ReactBoilerplate/Scripts/components/Input.tsx @@ -1,18 +1,39 @@ -import React, { Component, PropTypes } from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import { Glyphicon } from 'react-bootstrap'; -class Input extends Component { - static propTypes = { - field: PropTypes.object.isRequired, - type: React.PropTypes.oneOf([ - 'password', - 'text', - 'option', - 'checkbox' - ]), +export interface Field { + defaultChecked?: boolean; + defaultValue?: any; + name: string; // eslint-disable-line no-restricted-globals + onBlur?: (eventOrValue: any) => void; + onChange?: (eventOrValue: any) => void; + onDragStart?: (eventOrValue: any) => void; + onDrop?: (eventOrValue: any) => void; + onFocus?: (eventOrValue: any) => void; + touched: boolean; + invalid: boolean; + error: { + errors: string[]; }; - buildFieldProps() { +} + +export interface InputProps { + field: Field; + label?: string; + type?: 'password' | 'text' | 'option' | 'checkbox'; + options?: Array<{ + value: string; + display: string; + }>; +} + +class Input extends React.Component { + public static defaultProps = { + type: 'text' + } + + private buildFieldProps() { const { defaultChecked, defaultValue, @@ -34,26 +55,29 @@ class Input extends Component { onFocus }; } - renderErrorList(errors) { + + // eslint-disable-next-line class-methods-use-this + private renderErrorList(errors?: string[] | null) { if (!errors) { return null; } return (

- {errors.map((err, i) => - ( -

- - {' '} - {err} -

- ))} + {errors.map((err) => + ( +

+ + {' '} + {err} +

+ )) + }
); } - renderInput() { + private renderInput() { return ( ); } - renderOption() { + private renderOption() { const { options } = this.props; return ( ); } - renderCheckBox() { + private renderCheckBox() { return (
- - +
); } - render() { + public render() { let hasError = false; let errors; if (this.props.field.touched && this.props.field.invalid) { hasError = true; - errors = this.props.field.error.errors; + errors = this.props.field.error.errors; // eslint-disable-line prefer-destructuring if (!Array.isArray(errors)) { console.error('The errors object does not seem to be an array of errors.'); // eslint-disable-line max-len errors = null; } - if (errors.length === 0) { + if (!errors || errors.length === 0) { console.error('The errors array is empty. If it is empty, no array should be provided, the field is valid.'); // eslint-disable-line max-len } } @@ -125,7 +151,7 @@ class Input extends Component { } return (
- {(this.props.type !== 'checkbox') && + {(this.props.type !== 'checkbox') && // eslint-disable-next-line jsx-a11y/label-has-for }
@@ -137,8 +163,4 @@ class Input extends Component { } } -Input.defaultProps = { - type: 'text' -}; - export default Input; diff --git a/src/ReactBoilerplate/Scripts/components/LoginForm/LoginForm.js b/src/ReactBoilerplate/Scripts/components/LoginForm/LoginForm.tsx similarity index 59% rename from src/ReactBoilerplate/Scripts/components/LoginForm/LoginForm.js rename to src/ReactBoilerplate/Scripts/components/LoginForm/LoginForm.tsx index 68ce65a..c312930 100644 --- a/src/ReactBoilerplate/Scripts/components/LoginForm/LoginForm.js +++ b/src/ReactBoilerplate/Scripts/components/LoginForm/LoginForm.tsx @@ -1,17 +1,22 @@ -import React from 'react'; -import Form from 'components/Form'; +import * as React from 'react'; import { reduxForm } from 'redux-form'; -import { Input, ExternalLogin } from 'components'; -import { login } from 'redux/modules/account'; import { Row, Col } from 'react-bootstrap'; +import Form from '../../components/Form'; +import { Input, ExternalLogin } from '../../components'; +import { login } from '../../redux/modules/account'; +import { RootState } from '../../redux/reducer'; -class LoginForm extends Form { - render() { +export interface LoginFormProps { + loginProviders: string[]; +} + +class LoginForm extends Form { + public render() { const { fields: { userName, password }, loginProviders } = this.props; - return ( + return userName && password && ( {this.renderGlobalErrorList()} {(loginProviders.length > 0) && @@ -35,12 +40,11 @@ class LoginForm extends Form { } } -LoginForm = reduxForm({ - form: 'login', - fields: ['userName', 'password', 'rememberMe'] -}, -(state) => ({ loginProviders: state.externalLogin.loginProviders }), -{ } +export default reduxForm( + { + form: 'login', + fields: ['userName', 'password', 'rememberMe'] + }, + (state: RootState) => ({ loginProviders: state.externalLogin.loginProviders }), + { } )(LoginForm); - -export default LoginForm; diff --git a/src/ReactBoilerplate/Scripts/components/RegisterForm/RegisterForm.js b/src/ReactBoilerplate/Scripts/components/RegisterForm/RegisterForm.tsx similarity index 51% rename from src/ReactBoilerplate/Scripts/components/RegisterForm/RegisterForm.js rename to src/ReactBoilerplate/Scripts/components/RegisterForm/RegisterForm.tsx index 393518e..abc86d5 100644 --- a/src/ReactBoilerplate/Scripts/components/RegisterForm/RegisterForm.js +++ b/src/ReactBoilerplate/Scripts/components/RegisterForm/RegisterForm.tsx @@ -1,40 +1,57 @@ -import React from 'react'; -import Form from 'components/Form'; +import * as React from 'react'; import { reduxForm } from 'redux-form'; -import { Input, ExternalLoginButton, ExternalLogin } from 'components'; -import { register } from 'redux/modules/account'; -import { clearAuthentication as clearExternalAuthentication } from 'redux/modules/externalLogin'; import { Button, Row, Col } from 'react-bootstrap'; +import Form from '../../components/Form'; +import { Input, ExternalLoginButton, ExternalLogin } from '../../components'; +import { register } from '../../redux/modules/account'; +import { clearAuthentication as clearExternalAuthentication, State as ExternalLoginState } from '../../redux/modules/externalLogin'; +import { RootState } from '../../redux/reducer'; -class RegisterForm extends Form { - modifyValues(values) { +export interface RegisterFormProps { + externalLogin: ExternalLoginState; + clearExternalAuthentication: () => any; +} + +class RegisterForm extends Form { + protected modifyValues(values: any) { return { ...values, linkExternalLogin: this.props.externalLogin.externalAuthenticated }; } - onRemoveExternalAuthClick(action) { - return (event) => { + + // eslint-disable-next-line class-methods-use-this + public onRemoveExternalAuthClick(action: () => any) { + return (event: any) => { event.preventDefault(); action(); }; } - render() { + public render() { const { - fields: { userName, email, password, passwordConfirm }, - externalLogin: { externalAuthenticated, externalAuthenticatedProvider, loginProviders } + fields: { + userName, + email, + password, + passwordConfirm + }, + externalLogin: { + externalAuthenticated, + externalAuthenticatedProvider, + loginProviders + } } = this.props; - return ( + return userName && email && password && passwordConfirm && ( {this.renderGlobalErrorList()} - {externalAuthenticated && + {externalAuthenticated && externalAuthenticatedProvider &&
+ /> {' '} -
-
- - ); - } -} - -SendCodeForm = reduxForm({ - form: 'sendCode', - fields: ['provider'] -}, -(state) => ({ - userFactors: state.account.userFactors, - initialValues: { provider: (state.account.userFactors.length > 0 ? state.account.userFactors[0] : '') } -}), -{ } -)(SendCodeForm); - -export default SendCodeForm; diff --git a/src/ReactBoilerplate/Scripts/components/TwoFactor/SendCodeForm.tsx b/src/ReactBoilerplate/Scripts/components/TwoFactor/SendCodeForm.tsx new file mode 100644 index 0000000..ccbc1f2 --- /dev/null +++ b/src/ReactBoilerplate/Scripts/components/TwoFactor/SendCodeForm.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { reduxForm } from 'redux-form'; +import Form from '../Form'; +import { Input } from '../../components'; +import { sendCode } from '../../redux/modules/account'; +import { RootState } from '../../redux/reducer'; + +export interface SendCodeFormProps { + userFactors?: string[]; +} + +class SendCodeForm extends Form { + public render() { + const { + fields: { provider }, + userFactors + } = this.props; + return provider && userFactors && ( +
+ {this.renderGlobalErrorList()} + ({ value: userFactor, display: userFactor }))} /> +
+
+ +
+
+
+ ); + } +} + +export default reduxForm( + { + form: 'sendCode', + fields: ['provider'] + }, + (state: RootState) => ({ + userFactors: state.account.userFactors, + initialValues: { provider: (state.account.userFactors && state.account.userFactors.length > 0 ? state.account.userFactors[0] : '') } + }), + { } +)(SendCodeForm); diff --git a/src/ReactBoilerplate/Scripts/components/TwoFactor/TwoFactor.js b/src/ReactBoilerplate/Scripts/components/TwoFactor/TwoFactor.tsx similarity index 55% rename from src/ReactBoilerplate/Scripts/components/TwoFactor/TwoFactor.js rename to src/ReactBoilerplate/Scripts/components/TwoFactor/TwoFactor.tsx index 0260fbf..adc4b82 100644 --- a/src/ReactBoilerplate/Scripts/components/TwoFactor/TwoFactor.js +++ b/src/ReactBoilerplate/Scripts/components/TwoFactor/TwoFactor.tsx @@ -1,14 +1,20 @@ -import React, { Component } from 'react'; +import * as React from 'react'; import { connect } from 'react-redux'; +import { resetLoginState, State as AccountState } from '../../redux/modules/account'; import SendCodeForm from './SendCodeForm'; import VerifyCodeForm from './VerifyCodeForm'; -import { resetLoginState } from 'redux/modules/account'; +import { RootState } from '../../redux/reducer'; -class TwoFactor extends Component { - componentWillUnmount() { +export interface TwoFactorProps { + account: AccountState; + resetLoginState: () => any; +} + +class TwoFactor extends React.Component { + public componentWillUnmount() { this.props.resetLoginState(); } - render() { + public render() { const { account: { requiresTwoFactor, userFactors, sentCode } } = this.props; @@ -27,6 +33,6 @@ class TwoFactor extends Component { } export default connect( - (state) => ({ account: state.account }), + (state: RootState) => ({ account: state.account }), { resetLoginState } )(TwoFactor); diff --git a/src/ReactBoilerplate/Scripts/components/TwoFactor/VerifyCodeForm.js b/src/ReactBoilerplate/Scripts/components/TwoFactor/VerifyCodeForm.tsx similarity index 50% rename from src/ReactBoilerplate/Scripts/components/TwoFactor/VerifyCodeForm.js rename to src/ReactBoilerplate/Scripts/components/TwoFactor/VerifyCodeForm.tsx index 63a6b5d..2e69a87 100644 --- a/src/ReactBoilerplate/Scripts/components/TwoFactor/VerifyCodeForm.js +++ b/src/ReactBoilerplate/Scripts/components/TwoFactor/VerifyCodeForm.tsx @@ -1,21 +1,26 @@ -import React from 'react'; -import Form from 'components/Form'; +import * as React from 'react'; import { reduxForm } from 'redux-form'; -import { Input } from 'components'; -import { verifyCode } from 'redux/modules/account'; +import Form from '../../components/Form'; +import { Input } from '../../components'; +import { verifyCode } from '../../redux/modules/account'; +import { RootState } from '../../redux/reducer'; -class VerifyCodeForm extends Form { - modifyValues(values) { +export interface VeriyCodeFormProps { + sentCodeWithProvider: string; +} + +class VerifyCodeForm extends Form { + public modifyValues(values: any) { return { ...values, provider: this.props.sentCodeWithProvider }; } - render() { + public render() { const { fields: { code, rememberMe, rememberBrowser } } = this.props; - return ( + return code && rememberMe && rememberBrowser && (
{this.renderGlobalErrorList()} @@ -31,18 +36,19 @@ class VerifyCodeForm extends Form { } } -VerifyCodeForm = reduxForm({ - form: 'verifyCode', - fields: ['code', 'rememberMe', 'rememberBrowser'] -}, -(state) => ({ - sentCodeWithProvider: state.account.sentCodeWithProvider, - initialValues: { - rememberMe: true, - rememberBrowser: true - } -}), -{ } +export default reduxForm( + { + form: 'verifyCode', + fields: ['code', 'rememberMe', 'rememberBrowser'] + }, + (state: RootState) => ( + { + sentCodeWithProvider: state.account.sentCodeWithProvider, + initialValues: { + rememberMe: true, + rememberBrowser: true + } + } + ), + { } )(VerifyCodeForm); - -export default VerifyCodeForm; diff --git a/src/ReactBoilerplate/Scripts/components/index.js b/src/ReactBoilerplate/Scripts/components/index.js deleted file mode 100644 index c38c582..0000000 --- a/src/ReactBoilerplate/Scripts/components/index.js +++ /dev/null @@ -1,17 +0,0 @@ -export LoginForm from './LoginForm/LoginForm'; -export RegisterForm from './RegisterForm/RegisterForm'; -export ForgotPasswordForm from './ForgotPasswordForm/ForgotPasswordForm'; -export ResetPasswordForm from './ResetPasswordForm/ResetPasswordForm'; -export ChangePasswordForm from './ChangePasswordForm/ChangePasswordForm'; -export Input from './Input'; -export ExternalLogin from './ExternalLogin/ExternalLogin'; -export ExternalLoginButton from './ExternalLoginButton'; -export Spinner from './Spinner'; -export ErrorList from './ErrorList'; -export TwoFactor from './TwoFactor/TwoFactor'; -export ChangeEmailForm from './ChangeEmailForm/ChangeEmailForm'; -// There is a bug in babel. When exporting types that will be inherited, -// you must import them directly from the component. You can't proxy -// them like this index.js does. -// http://stackoverflow.com/questions/28551582/traceur-runtime-super-expression-must-either-be-null-or-a-function-not-undefin -// export Form from './Form'; diff --git a/src/ReactBoilerplate/Scripts/components/index.ts b/src/ReactBoilerplate/Scripts/components/index.ts new file mode 100644 index 0000000..cf6e290 --- /dev/null +++ b/src/ReactBoilerplate/Scripts/components/index.ts @@ -0,0 +1,13 @@ +export { default as LoginForm } from './LoginForm/LoginForm'; +export { default as RegisterForm } from './RegisterForm/RegisterForm'; +export { default as ForgotPasswordForm } from './ForgotPasswordForm/ForgotPasswordForm'; +export { default as ResetPasswordForm } from './ResetPasswordForm/ResetPasswordForm'; +export { default as ChangePasswordForm } from './ChangePasswordForm/ChangePasswordForm'; +export { default as Input, Field } from './Input'; +export { default as ExternalLogin } from './ExternalLogin/ExternalLogin'; +export { default as ExternalLoginButton } from './ExternalLoginButton'; +export { default as Spinner } from './Spinner'; +export { default as ErrorList } from './ErrorList'; +export { default as TwoFactor } from './TwoFactor/TwoFactor'; +export { default as ChangeEmailForm } from './ChangeEmailForm/ChangeEmailForm'; +export { default as Form } from './Form'; diff --git a/src/ReactBoilerplate/Scripts/config.js b/src/ReactBoilerplate/Scripts/config.ts similarity index 100% rename from src/ReactBoilerplate/Scripts/config.js rename to src/ReactBoilerplate/Scripts/config.ts diff --git a/src/ReactBoilerplate/Scripts/containers/About/About.js b/src/ReactBoilerplate/Scripts/containers/About/About.tsx similarity index 59% rename from src/ReactBoilerplate/Scripts/containers/About/About.js rename to src/ReactBoilerplate/Scripts/containers/About/About.tsx index 17c76dd..f63c076 100644 --- a/src/ReactBoilerplate/Scripts/containers/About/About.js +++ b/src/ReactBoilerplate/Scripts/containers/About/About.tsx @@ -1,8 +1,8 @@ -import React, { Component } from 'react'; +import * as React from 'react'; import Helmet from 'react-helmet'; -export default class About extends Component { - render() { +export default class About extends React.Component { + public render() { return (

About us...

diff --git a/src/ReactBoilerplate/Scripts/containers/App/App.scss b/src/ReactBoilerplate/Scripts/containers/App/App.scss index e5cc5f3..c4047d4 100644 --- a/src/ReactBoilerplate/Scripts/containers/App/App.scss +++ b/src/ReactBoilerplate/Scripts/containers/App/App.scss @@ -1,34 +1,33 @@ body { - padding-top: 50px; - padding-bottom: 20px; + padding-top: 50px; + padding-bottom: 20px; } -/* Wrapping element */ -/* Set some basic padding to keep content from hitting the edges */ +/* Wrapping element: Set some basic padding to keep content from hitting the edges */ .body-content { - padding-left: 15px; - padding-right: 15px; + padding-left: 15px; + padding-right: 15px; } /* Set widths on the form inputs since otherwise they're 100% wide */ input, select, textarea { - max-width: 280px; + max-width: 280px; } /* Carousel */ .carousel-caption { - z-index: 10 !important; + z-index: 10 !important; } - .carousel-caption p { - font-size: 20px; - line-height: 1.4; - } +.carousel-caption p { + font-size: 20px; + line-height: 1.4; +} @media (min-width: 768px) { - .carousel-caption { - z-index: 10 !important; - } + .carousel-caption { + z-index: 10 !important; + } } diff --git a/src/ReactBoilerplate/Scripts/containers/App/App.js b/src/ReactBoilerplate/Scripts/containers/App/App.tsx similarity index 63% rename from src/ReactBoilerplate/Scripts/containers/App/App.js rename to src/ReactBoilerplate/Scripts/containers/App/App.tsx index 9fe901d..51c6da7 100644 --- a/src/ReactBoilerplate/Scripts/containers/App/App.js +++ b/src/ReactBoilerplate/Scripts/containers/App/App.tsx @@ -1,31 +1,36 @@ -import React, { Component, PropTypes } from 'react'; +import * as React from 'react'; import { connect } from 'react-redux'; import Helmet from 'react-helmet'; -import config from '../../config'; -import { IndexLink } from 'react-router'; -import { LinkContainer, IndexLinkContainer } from 'react-router-bootstrap'; -import Navbar from 'react-bootstrap/lib/Navbar'; -import Nav from 'react-bootstrap/lib/Nav'; -import NavItem from 'react-bootstrap/lib/NavItem'; -import { logoff } from '../../redux/modules/account'; +import { NavLink } from 'react-router-dom'; +import { LinkContainer } from 'react-router-bootstrap'; import { push } from 'react-router-redux'; +import * as Navbar from 'react-bootstrap/lib/Navbar'; +import * as Nav from 'react-bootstrap/lib/Nav'; +import * as NavItem from 'react-bootstrap/lib/NavItem'; +import config from '../../config'; +import { logoff, User } from '../../redux/modules/account'; import TwoFactorModal from './Modals/TwoFactorModal'; +import { RootState } from '../../redux/reducer'; +import Routes from '../../routes'; require('./App.scss'); -class App extends Component { - static propTypes = { - children: PropTypes.object.isRequired - }; - constructor(props) { +export interface AppProps { + logoff: () => any; + pushState: (page: string) => any; + user?: User; +} + +class App extends React.Component { + public constructor(props: AppProps) { super(props); this.logoffClick = this.logoffClick.bind(this); } - logoffClick() { + public logoffClick() { this.props.logoff(); this.props.pushState('/'); } - renderLoggedInLinks(user) { + private renderLoggedInLinks(user: User) { return ( ); } - renderAnonymousLinks() { + // eslint-disable-next-line class-methods-use-this + private renderAnonymousLinks() { return ( ); } - render() { + public render() { const { user } = this.props; @@ -65,17 +71,17 @@ class App extends Component { - + {config.app.title} - +