Skip to content

Commit 435626f

Browse files
committed
Add support for flash messages utilizing redux
1 parent b93b40b commit 435626f

File tree

15 files changed

+268
-34
lines changed

15 files changed

+268
-34
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "pterodactyl-panel",
33
"dependencies": {
44
"@hot-loader/react-dom": "^16.8.6",
5+
"@types/react-redux": "^7.0.9",
56
"axios": "^0.18.0",
67
"brace": "^0.11.1",
78
"classnames": "^2.2.6",
@@ -12,9 +13,11 @@
1213
"react": "^16.8.6",
1314
"react-dom": "^16.8.6",
1415
"react-hot-loader": "^4.9.0",
16+
"react-redux": "^7.1.0",
1517
"react-router-dom": "^5.0.1",
1618
"react-transition-group": "^4.1.0",
1719
"redux": "^4.0.1",
20+
"redux-persist": "^5.10.0",
1821
"socket.io-client": "^2.2.0",
1922
"ws-wrapper": "^2.0.0",
2023
"xterm": "^3.5.1"
@@ -31,6 +34,7 @@
3134
"@types/react-dom": "^16.8.4",
3235
"@types/react-router-dom": "^4.3.3",
3336
"@types/react-transition-group": "^2.9.2",
37+
"@types/redux-persist": "^4.3.1",
3438
"@types/webpack-env": "^1.13.6",
3539
"@typescript-eslint/eslint-plugin": "^1.10.1",
3640
"@typescript-eslint/parser": "^1.10.1",

resources/assets/styles/components/spinners.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,36 @@
5656
transform: rotate(360deg);
5757
}
5858
}
59+
60+
.spinner-circle {
61+
@apply .w-8 .h-8;
62+
border: 3px solid hsla(211, 12%, 43%, 0.2);
63+
border-top-color: hsl(211, 12%, 43%);
64+
border-radius: 50%;
65+
animation: spin 1s cubic-bezier(0.55, 0.25, 0.25, 0.70) infinite;
66+
67+
&.spinner-sm {
68+
@apply .w-4 .h-4 .border-2;
69+
}
70+
71+
&.spinner-lg {
72+
@apply .w-16 .h-16;
73+
border-width: 6px;
74+
}
75+
76+
&.spinner-blue {
77+
border: 3px solid hsla(212, 92%, 43%, 0.2);
78+
border-top-color: hsl(212, 92%, 43%);
79+
}
80+
81+
&.spinner-white {
82+
border: 3px solid rgba(255, 255, 255, 0.2);
83+
border-top-color: rgb(255, 255, 255);
84+
}
85+
}
86+
87+
@keyframes spin {
88+
to {
89+
transform: rotate(360deg);
90+
}
91+
}

resources/scripts/components/App.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,29 @@ import * as React from 'react';
22
import { hot } from 'react-hot-loader/root';
33
import { BrowserRouter as Router, Route } from 'react-router-dom';
44
import AuthenticationRouter from '@/routers/AuthenticationRouter';
5+
import { Provider } from 'react-redux';
6+
import { persistor, store } from '@/redux/configure';
7+
import { PersistGate } from 'redux-persist/integration/react';
58

69
class App extends React.PureComponent {
710
render () {
811
return (
9-
<Router>
10-
<div>
11-
<Route exact path="/"/>
12-
<Route path="/auth" component={AuthenticationRouter}/>
13-
</div>
14-
</Router>
12+
<Provider store={store}>
13+
<PersistGate persistor={persistor} loading={this.renderLoading()}>
14+
<Router>
15+
<div>
16+
<Route exact path="/"/>
17+
<Route path="/auth" component={AuthenticationRouter}/>
18+
</div>
19+
</Router>
20+
</PersistGate>
21+
</Provider>
22+
);
23+
}
24+
25+
renderLoading () {
26+
return (
27+
<div className={'spinner spinner-lg'}></div>
1528
);
1629
}
1730
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as React from 'react';
2+
import { FlashMessage, ReduxState } from '@/redux/types';
3+
import { connect } from 'react-redux';
4+
import MessageBox from '@/components/MessageBox';
5+
6+
type Props = Readonly<{
7+
flashes: FlashMessage[];
8+
}>;
9+
10+
class FlashMessageRender extends React.PureComponent<Props> {
11+
render () {
12+
if (this.props.flashes.length === 0) {
13+
return null;
14+
}
15+
16+
return (
17+
<React.Fragment>
18+
{
19+
this.props.flashes.map(flash => (
20+
<MessageBox
21+
key={flash.id || flash.type + flash.message}
22+
type={flash.type}
23+
title={flash.title}
24+
>
25+
{flash.message}
26+
</MessageBox>
27+
))
28+
}
29+
</React.Fragment>
30+
)
31+
}
32+
}
33+
34+
const mapStateToProps = (state: ReduxState) => ({
35+
flashes: state.flashes,
36+
});
37+
38+
export default connect(mapStateToProps)(FlashMessageRender);
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import * as React from 'react';
22

3+
export type FlashMessageType = 'success' | 'info' | 'warning' | 'error';
4+
35
interface Props {
46
title?: string;
5-
message: string;
6-
type?: 'success' | 'info' | 'warning' | 'error';
7+
children: string;
8+
type?: FlashMessageType;
79
}
810

9-
export default ({ title, message, type }: Props) => (
11+
export default ({ title, children, type }: Props) => (
1012
<div className={`lg:inline-flex alert ${type}`} role={'alert'}>
1113
{title && <span className={'title'}>{title}</span>}
12-
<span className={'message'}>{message}</span>
14+
<span className={'message'}>
15+
{children}
16+
</span>
1317
</div>
1418
);

resources/scripts/components/auth/ForgotPasswordContainer.tsx

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@ import * as React from 'react';
22
import OpenInputField from '@/components/forms/OpenInputField';
33
import { Link } from 'react-router-dom';
44
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
5+
import { connect } from 'react-redux';
6+
import { ReduxState } from '@/redux/types';
7+
import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash';
8+
import { httpErrorToHuman } from '@/api/http';
59

610
type Props = Readonly<{
7-
11+
pushFlashMessage: typeof pushFlashMessage;
12+
clearAllFlashMessages: typeof clearAllFlashMessages;
813
}>;
914

1015
type State = Readonly<{
1116
email: string;
1217
isSubmitting: boolean;
1318
}>;
1419

15-
export default class ForgotPasswordContainer extends React.PureComponent<Props, State> {
20+
class ForgotPasswordContainer extends React.PureComponent<Props, State> {
1621
state: State = {
1722
email: '',
1823
isSubmitting: false,
@@ -22,16 +27,27 @@ export default class ForgotPasswordContainer extends React.PureComponent<Props,
2227
email: e.target.value,
2328
});
2429

25-
handleSubmission = (e: React.FormEvent<HTMLFormElement>) => this.setState({ isSubmitting: true }, () => {
30+
handleSubmission = (e: React.FormEvent<HTMLFormElement>) => {
2631
e.preventDefault();
2732

28-
requestPasswordResetEmail(this.state.email)
29-
.then(() => {
30-
31-
})
32-
.catch(console.error)
33-
.then(() => this.setState({ isSubmitting: false }));
34-
});
33+
this.setState({ isSubmitting: true }, () => {
34+
this.props.clearAllFlashMessages();
35+
requestPasswordResetEmail(this.state.email)
36+
.then(() => {
37+
// @todo actually handle this.
38+
})
39+
.catch(error => {
40+
console.error(error);
41+
this.props.pushFlashMessage({
42+
id: 'auth:forgot-password',
43+
type: 'error',
44+
title: 'Error',
45+
message: httpErrorToHuman(error),
46+
});
47+
})
48+
.then(() => this.setState({ isSubmitting: false }));
49+
});
50+
};
3551

3652
render () {
3753
return (
@@ -50,11 +66,11 @@ export default class ForgotPasswordContainer extends React.PureComponent<Props,
5066
</div>
5167
<div className={'mt-6'}>
5268
<button
53-
className={'btn btn-primary btn-jumbo'}
69+
className={'btn btn-primary btn-jumbo flex justify-center'}
5470
disabled={this.state.isSubmitting || this.state.email.length < 5}
5571
>
5672
{this.state.isSubmitting ?
57-
<span className={'spinner white'}>&nbsp;</span>
73+
<div className={'spinner-circle spinner-sm spinner-white'}></div>
5874
:
5975
'Send Email'
6076
}
@@ -73,3 +89,14 @@ export default class ForgotPasswordContainer extends React.PureComponent<Props,
7389
);
7490
}
7591
}
92+
93+
const mapStateToProps = (state: ReduxState) => ({
94+
flashes: state.flashes,
95+
});
96+
97+
const mapDispatchToProps = {
98+
pushFlashMessage,
99+
clearAllFlashMessages,
100+
};
101+
102+
export default connect(mapStateToProps, mapDispatchToProps)(ForgotPasswordContainer);

resources/scripts/components/auth/LoginContainer.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,9 @@ export default class LoginContainer extends React.PureComponent<{}, State> {
5454
<React.Fragment>
5555
{this.state.errorMessage &&
5656
<div className={'mb-4'}>
57-
<MessageBox
58-
type={'error'}
59-
title={'Error'}
60-
message={this.state.errorMessage}
61-
/>
57+
<MessageBox type={'error'} title={'Error'}>
58+
{this.state.errorMessage}
59+
</MessageBox>
6260
</div>
6361
}
6462
<form className={'login-box'} onSubmit={this.submit}>

resources/scripts/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
22
import * as ReactDOM from 'react-dom';
3-
import App from "@/components/App";
3+
import App from '@/components/App';
44

55
ReactDOM.render(<App/>, document.getElementById('app'));
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { FlashMessage } from '@/redux/types';
2+
3+
export const PUSH_FLASH_MESSAGE = 'PUSH_FLASH_MESSAGE';
4+
export const REMOVE_FLASH_MESSAGE = 'REMOVE_FLASH_MESSAGE';
5+
export const CLEAR_ALL_FLASH_MESSAGES = 'CLEAR_ALL_FLASH_MESSAGES';
6+
7+
export const pushFlashMessage = (payload: FlashMessage) => ({
8+
type: PUSH_FLASH_MESSAGE, payload,
9+
});
10+
11+
export const removeFlashMessage = (id: string) => ({
12+
type: REMOVE_FLASH_MESSAGE, payload: id,
13+
});
14+
15+
export const clearAllFlashMessages = () => ({
16+
type: CLEAR_ALL_FLASH_MESSAGES,
17+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { createStore } from 'redux';
2+
import { persistStore, persistReducer, PersistConfig } from 'redux-persist';
3+
import storage from 'redux-persist/lib/storage';
4+
import { reducers } from './reducers';
5+
6+
const persistConfig: PersistConfig = {
7+
key: 'root',
8+
storage,
9+
};
10+
11+
const persistedReducer = persistReducer(persistConfig, reducers);
12+
13+
export const store = createStore(persistedReducer);
14+
export const persistor = persistStore(store);

0 commit comments

Comments
 (0)