From 435626f4b7b2d25239b74a9ca7f5556e76b71bb0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 11 Jun 2019 23:12:03 -0700 Subject: [PATCH] Add support for flash messages utilizing redux --- package.json | 4 ++ .../assets/styles/components/spinners.css | 33 ++++++++++++ resources/scripts/components/App.tsx | 25 ++++++--- .../scripts/components/FlashMessageRender.tsx | 38 ++++++++++++++ resources/scripts/components/MessageBox.tsx | 12 +++-- .../auth/ForgotPasswordContainer.tsx | 51 ++++++++++++++----- .../components/auth/LoginContainer.tsx | 8 ++- resources/scripts/index.tsx | 2 +- resources/scripts/redux/actions/flash.ts | 17 +++++++ resources/scripts/redux/configure.ts | 14 +++++ resources/scripts/redux/reducers.ts | 7 +++ resources/scripts/redux/reducers/flash.ts | 21 ++++++++ resources/scripts/redux/types.d.ts | 17 +++++++ .../scripts/routers/AuthenticationRouter.tsx | 6 ++- yarn.lock | 47 +++++++++++++++-- 15 files changed, 268 insertions(+), 34 deletions(-) create mode 100644 resources/scripts/components/FlashMessageRender.tsx create mode 100644 resources/scripts/redux/actions/flash.ts create mode 100644 resources/scripts/redux/configure.ts create mode 100644 resources/scripts/redux/reducers.ts create mode 100644 resources/scripts/redux/reducers/flash.ts create mode 100644 resources/scripts/redux/types.d.ts diff --git a/package.json b/package.json index d182b39b0..f1e43863f 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "pterodactyl-panel", "dependencies": { "@hot-loader/react-dom": "^16.8.6", + "@types/react-redux": "^7.0.9", "axios": "^0.18.0", "brace": "^0.11.1", "classnames": "^2.2.6", @@ -12,9 +13,11 @@ "react": "^16.8.6", "react-dom": "^16.8.6", "react-hot-loader": "^4.9.0", + "react-redux": "^7.1.0", "react-router-dom": "^5.0.1", "react-transition-group": "^4.1.0", "redux": "^4.0.1", + "redux-persist": "^5.10.0", "socket.io-client": "^2.2.0", "ws-wrapper": "^2.0.0", "xterm": "^3.5.1" @@ -31,6 +34,7 @@ "@types/react-dom": "^16.8.4", "@types/react-router-dom": "^4.3.3", "@types/react-transition-group": "^2.9.2", + "@types/redux-persist": "^4.3.1", "@types/webpack-env": "^1.13.6", "@typescript-eslint/eslint-plugin": "^1.10.1", "@typescript-eslint/parser": "^1.10.1", diff --git a/resources/assets/styles/components/spinners.css b/resources/assets/styles/components/spinners.css index f5591f280..2141e76a6 100644 --- a/resources/assets/styles/components/spinners.css +++ b/resources/assets/styles/components/spinners.css @@ -56,3 +56,36 @@ transform: rotate(360deg); } } + +.spinner-circle { + @apply .w-8 .h-8; + border: 3px solid hsla(211, 12%, 43%, 0.2); + border-top-color: hsl(211, 12%, 43%); + border-radius: 50%; + animation: spin 1s cubic-bezier(0.55, 0.25, 0.25, 0.70) infinite; + + &.spinner-sm { + @apply .w-4 .h-4 .border-2; + } + + &.spinner-lg { + @apply .w-16 .h-16; + border-width: 6px; + } + + &.spinner-blue { + border: 3px solid hsla(212, 92%, 43%, 0.2); + border-top-color: hsl(212, 92%, 43%); + } + + &.spinner-white { + border: 3px solid rgba(255, 255, 255, 0.2); + border-top-color: rgb(255, 255, 255); + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx index 69f482e7d..9405bf524 100644 --- a/resources/scripts/components/App.tsx +++ b/resources/scripts/components/App.tsx @@ -2,16 +2,29 @@ import * as React from 'react'; import { hot } from 'react-hot-loader/root'; import { BrowserRouter as Router, Route } from 'react-router-dom'; import AuthenticationRouter from '@/routers/AuthenticationRouter'; +import { Provider } from 'react-redux'; +import { persistor, store } from '@/redux/configure'; +import { PersistGate } from 'redux-persist/integration/react'; class App extends React.PureComponent { render () { return ( - -
- - -
-
+ + + +
+ + +
+
+
+
+ ); + } + + renderLoading () { + return ( +
); } } diff --git a/resources/scripts/components/FlashMessageRender.tsx b/resources/scripts/components/FlashMessageRender.tsx new file mode 100644 index 000000000..356e536c2 --- /dev/null +++ b/resources/scripts/components/FlashMessageRender.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { FlashMessage, ReduxState } from '@/redux/types'; +import { connect } from 'react-redux'; +import MessageBox from '@/components/MessageBox'; + +type Props = Readonly<{ + flashes: FlashMessage[]; +}>; + +class FlashMessageRender extends React.PureComponent { + render () { + if (this.props.flashes.length === 0) { + return null; + } + + return ( + + { + this.props.flashes.map(flash => ( + + {flash.message} + + )) + } + + ) + } +} + +const mapStateToProps = (state: ReduxState) => ({ + flashes: state.flashes, +}); + +export default connect(mapStateToProps)(FlashMessageRender); diff --git a/resources/scripts/components/MessageBox.tsx b/resources/scripts/components/MessageBox.tsx index 8ebf11553..a962afb88 100644 --- a/resources/scripts/components/MessageBox.tsx +++ b/resources/scripts/components/MessageBox.tsx @@ -1,14 +1,18 @@ import * as React from 'react'; +export type FlashMessageType = 'success' | 'info' | 'warning' | 'error'; + interface Props { title?: string; - message: string; - type?: 'success' | 'info' | 'warning' | 'error'; + children: string; + type?: FlashMessageType; } -export default ({ title, message, type }: Props) => ( +export default ({ title, children, type }: Props) => (
{title && {title}} - {message} + + {children} +
); diff --git a/resources/scripts/components/auth/ForgotPasswordContainer.tsx b/resources/scripts/components/auth/ForgotPasswordContainer.tsx index f41de737b..dfea4f38b 100644 --- a/resources/scripts/components/auth/ForgotPasswordContainer.tsx +++ b/resources/scripts/components/auth/ForgotPasswordContainer.tsx @@ -2,9 +2,14 @@ import * as React from 'react'; import OpenInputField from '@/components/forms/OpenInputField'; import { Link } from 'react-router-dom'; import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; +import { connect } from 'react-redux'; +import { ReduxState } from '@/redux/types'; +import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash'; +import { httpErrorToHuman } from '@/api/http'; type Props = Readonly<{ - + pushFlashMessage: typeof pushFlashMessage; + clearAllFlashMessages: typeof clearAllFlashMessages; }>; type State = Readonly<{ @@ -12,7 +17,7 @@ type State = Readonly<{ isSubmitting: boolean; }>; -export default class ForgotPasswordContainer extends React.PureComponent { +class ForgotPasswordContainer extends React.PureComponent { state: State = { email: '', isSubmitting: false, @@ -22,16 +27,27 @@ export default class ForgotPasswordContainer extends React.PureComponent) => this.setState({ isSubmitting: true }, () => { + handleSubmission = (e: React.FormEvent) => { e.preventDefault(); - requestPasswordResetEmail(this.state.email) - .then(() => { - - }) - .catch(console.error) - .then(() => this.setState({ isSubmitting: false })); - }); + this.setState({ isSubmitting: true }, () => { + this.props.clearAllFlashMessages(); + requestPasswordResetEmail(this.state.email) + .then(() => { + // @todo actually handle this. + }) + .catch(error => { + console.error(error); + this.props.pushFlashMessage({ + id: 'auth:forgot-password', + type: 'error', + title: 'Error', + message: httpErrorToHuman(error), + }); + }) + .then(() => this.setState({ isSubmitting: false })); + }); + }; render () { return ( @@ -50,11 +66,11 @@ export default class ForgotPasswordContainer extends React.PureComponent