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