Skip to content

Commit aa9165b

Browse files
authored
Merge pull request codeBelt#13 from codeBelt/js/toasts
Js/toasts
2 parents 110b2f7 + fd6bf23 commit aa9165b

18 files changed

+218
-8
lines changed

src/constants/RouteEnum.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
enum RouteEnum {
22
Home = '/',
33
Episodes = '/episodes',
4+
About = '/about',
45
}
56

67
export default RouteEnum;

src/constants/ToastStatusEnum.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
enum ToastStatusEnum {
2+
Error = 'error',
3+
Warning = 'warning',
4+
Success = 'success',
5+
}
6+
7+
export default ToastStatusEnum;

src/environments/base.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default function(baseApi) {
99
shows: `${baseApi}/shows/:showId`,
1010
episodes: `${baseApi}/shows/:showId/episodes`,
1111
cast: `${baseApi}/shows/:showId/cast`,
12+
errorExample: 'https://httpstat.us/520',
1213
},
1314
isProduction: true,
1415
isDevelopment: false,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import ToastStatusEnum from '../constants/ToastStatusEnum';
2+
import ToastsAction from '../stores/toasts/ToastsAction';
3+
4+
const errorToastMiddleware = () => (store) => (next) => (action) => {
5+
if (action.error) {
6+
const errorAction = action;
7+
8+
next(ToastsAction.add(errorAction.payload.message, ToastStatusEnum.Error));
9+
}
10+
11+
next(action);
12+
};
13+
14+
export default errorToastMiddleware;

src/stores/rootReducer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { connectRouter } from 'connected-react-router';
33
import RequestingReducer from './requesting/RequestingReducer';
44
import ErrorReducer from './error/ErrorReducer';
55
import ShowsReducer from './shows/ShowsReducer';
6+
import ToastsReducer from './toasts/ToastsReducer';
67

78
export default (history) => {
89
const reducerMap = {
910
error: ErrorReducer.reducer,
1011
requesting: RequestingReducer.reducer,
1112
router: connectRouter(history),
1213
shows: new ShowsReducer().reducer,
14+
toasts: new ToastsReducer().reducer,
1315
};
1416

1517
return combineReducers(reducerMap);

src/stores/rootStore.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { routerMiddleware } from 'connected-react-router';
55
import reduxFreeze from 'redux-freeze';
66
import environment from 'environment';
77
import rootReducer from './rootReducer';
8+
import errorToastMiddleware from '../middlewares/errorToastMiddleware';
89

910
export default (initialState, history) => {
10-
const middleware = [environment.isDevelopment ? reduxFreeze : null, routerMiddleware(history), thunk].filter(Boolean);
11+
const middleware = [environment.isDevelopment ? reduxFreeze : null, thunk, routerMiddleware(history), errorToastMiddleware()].filter(Boolean);
1112

1213
const store = createStore(rootReducer(history), initialState, composeWithDevTools(applyMiddleware(...middleware)));
1314

src/stores/shows/ShowsAction.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export default class ShowsAction {
1111
static REQUEST_CAST = 'ShowsAction.REQUEST_CAST';
1212
static REQUEST_CAST_FINISHED = 'ShowsAction.REQUEST_CAST_FINISHED';
1313

14+
static REQUEST_ERROR = 'ShowsAction.REQUEST_ERROR';
15+
static REQUEST_ERROR_FINISHED = 'ShowsAction.REQUEST_ERROR_FINISHED';
16+
1417
static requestShow() {
1518
return async (dispatch, getState) => {
1619
const showId = getState().shows.currentShowId;
@@ -34,4 +37,10 @@ export default class ShowsAction {
3437
await ActionUtility.createThunkEffect(dispatch, ShowsAction.REQUEST_CAST, ShowsEffect.requestCast, showId);
3538
};
3639
}
40+
41+
static requestError() {
42+
return async (dispatch, getState) => {
43+
await ActionUtility.createThunkEffect(dispatch, ShowsAction.REQUEST_ERROR, ShowsEffect.requestError);
44+
};
45+
}
3746
}

src/stores/shows/ShowsEffect.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,18 @@ export default class ShowsEffect {
3232

3333
return response.data.map((json) => new CastModel(json));
3434
}
35+
36+
/**
37+
* This is only to trigger an error api response so we can use it for an example in the AboutPage
38+
*/
39+
static async requestError() {
40+
const endpoint = environment.api.errorExample;
41+
const response = await HttpUtility.get(endpoint);
42+
43+
if (response instanceof HttpErrorResponseModel) {
44+
return response;
45+
}
46+
47+
return response.data;
48+
}
3549
}

src/stores/toasts/ToastsAction.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import ActionUtility from '../../utilities/ActionUtility';
2+
import uuid from 'uuid/v4';
3+
4+
export default class ToastsAction {
5+
static ADD_TOAST = 'ToastsAction.ADD_TOAST';
6+
static REMOVE_TOAST = 'ToastsAction.REMOVE_TOAST';
7+
8+
static add(message, type) {
9+
return ActionUtility.createAction(ToastsAction.ADD_TOAST, {
10+
message,
11+
type,
12+
id: uuid(),
13+
});
14+
}
15+
16+
static removeById(toastId) {
17+
return ActionUtility.createAction(ToastsAction.REMOVE_TOAST, toastId);
18+
}
19+
}

src/stores/toasts/ToastsReducer.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import ToastsAction from './ToastsAction';
2+
import BaseReducer from '../../utilities/BaseReducer';
3+
4+
export default class ToastsReducer extends BaseReducer {
5+
initialState = {
6+
items: [],
7+
};
8+
9+
[ToastsAction.ADD_TOAST](state, action) {
10+
return {
11+
...state,
12+
items: [...state.items, action.payload],
13+
};
14+
}
15+
16+
[ToastsAction.REMOVE_TOAST](state, action) {
17+
const toastId = action.payload;
18+
19+
return {
20+
...state,
21+
items: state.items.filter((model) => model.id !== toastId),
22+
};
23+
}
24+
}

src/typings.d.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ declare module 'lodash.groupby';
44
declare module 'environment' {
55
const value: {
66
api: {
7-
shows;
8-
episodes;
9-
cast;
7+
shows: string;
8+
episodes: string;
9+
cast: string;
10+
errorExample: string;
1011
};
1112
isDevelopment: boolean;
1213
isProduction: boolean;

src/views/App.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { Route, Switch } from 'react-router-dom';
44
import RouteEnum from '../constants/RouteEnum';
55
import MainNav from './components/main-nav/MainNav';
66
import LoadingIndicator from './components/loading-indicator/LoadingIndicator';
7+
import Toasts from './components/toasts/Toasts';
78

89
const HomePage = lazy(() => import('./home-page/HomePage'));
9-
const NotFoundPage = lazy(() => import('./not-found-page/NotFoundPage'));
1010
const EpisodesPage = lazy(() => import('./episodes-page/EpisodesPage'));
11+
const AboutPage = lazy(() => import('./about-page/AboutPage'));
12+
const NotFoundPage = lazy(() => import('./not-found-page/NotFoundPage'));
1113

1214
export default class App extends React.Component {
1315
render() {
@@ -18,8 +20,10 @@ export default class App extends React.Component {
1820
<Switch>
1921
<Route exact={true} path={RouteEnum.Home} component={HomePage} />
2022
<Route path={RouteEnum.Episodes} component={EpisodesPage} />
23+
<Route path={RouteEnum.About} component={AboutPage} />
2124
<Route component={NotFoundPage} />
2225
</Switch>
26+
<Toasts />
2327
</Suspense>
2428
</ConnectedRouter>
2529
);

src/views/about-page/AboutPage.jsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import styles from './AboutPage.module.scss';
2+
3+
import * as React from 'react';
4+
import { connect } from 'react-redux';
5+
import { selectErrorText } from '../../selectors/error/ErrorSelector';
6+
import ShowsAction from '../../stores/shows/ShowsAction';
7+
import { selectRequesting } from '../../selectors/requesting/RequestingSelector';
8+
import LoadingIndicator from '../components/loading-indicator/LoadingIndicator';
9+
import { Header, Message, Container } from 'semantic-ui-react';
10+
11+
const mapStateToProps = (state, ownProps) => ({
12+
isRequesting: selectRequesting(state, [ShowsAction.REQUEST_ERROR]),
13+
requestErrorText: selectErrorText(state, [ShowsAction.REQUEST_ERROR_FINISHED]),
14+
});
15+
16+
class AboutPage extends React.Component {
17+
componentDidMount() {
18+
this.props.dispatch(ShowsAction.requestError());
19+
}
20+
21+
render() {
22+
const { isRequesting, requestErrorText } = this.props;
23+
24+
return (
25+
<div className={styles.wrapper}>
26+
<Header as="h2">About</Header>
27+
<LoadingIndicator isActive={isRequesting}>
28+
<Container>
29+
<p>
30+
This page is only to show how to handle API errors on the page. You will also notice a popup indicator with the actual error text. Below
31+
we create a custom error message.
32+
</p>
33+
</Container>
34+
{requestErrorText && <Message info={true} header="Error" content="Sorry there was an error requesting this content." />}
35+
</LoadingIndicator>
36+
</div>
37+
);
38+
}
39+
}
40+
41+
export { AboutPage as Unconnected };
42+
export default connect(mapStateToProps)(AboutPage);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.wrapper {
2+
}

src/views/components/main-nav/MainNav.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import * as React from 'react';
22
import { Menu, Segment } from 'semantic-ui-react';
33
import MenuNavLink from './components/MenuNavLink';
4+
import RouteEnum from '../../../constants/RouteEnum';
45

56
export default class MainNav extends React.PureComponent {
67
render() {
78
return (
89
<Segment inverted={true}>
910
<Menu inverted={true} pointing={true} secondary={true}>
10-
<Menu.Item as={MenuNavLink} to="/" name="home" />
11-
<Menu.Item as={MenuNavLink} to="/episodes" name="Episodes" />
11+
<Menu.Item as={MenuNavLink} to={RouteEnum.Home} name="Home" />
12+
<Menu.Item as={MenuNavLink} to={RouteEnum.Episodes} name="Episodes" />
13+
<Menu.Item as={MenuNavLink} to={RouteEnum.About} name="About" />
1214
</Menu>
1315
</Segment>
1416
);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import styles from './Toasts.module.scss';
2+
3+
import React from 'react';
4+
import { connect } from 'react-redux';
5+
import ToastsAction from '../../../stores/toasts/ToastsAction';
6+
import { Card, Button } from 'semantic-ui-react';
7+
import ToastStatusEnum from '../../../constants/ToastStatusEnum';
8+
9+
const mapStateToProps = (state, ownProps) => ({
10+
toasts: state.toasts.items,
11+
});
12+
13+
class Toasts extends React.Component {
14+
buttonColorMap = {
15+
[ToastStatusEnum.Error]: 'red',
16+
[ToastStatusEnum.Warning]: 'orange',
17+
[ToastStatusEnum.Success]: 'green',
18+
};
19+
20+
render() {
21+
const { toasts } = this.props;
22+
23+
if (toasts.length === 0) {
24+
return null;
25+
}
26+
27+
return (
28+
<div className={styles.wrapper}>
29+
{toasts.map((item) => {
30+
const buttonColor = this.buttonColorMap[item.type];
31+
32+
return (
33+
<Card key={item.id}>
34+
<Card.Content>
35+
<Card.Header content={item.type} />
36+
<Card.Description content={item.message} />
37+
</Card.Content>
38+
<Card.Content extra={true}>
39+
<Button color={buttonColor} onClick={this._onClickRemoveNotification(item.id)}>
40+
Close
41+
</Button>
42+
</Card.Content>
43+
</Card>
44+
);
45+
})}
46+
</div>
47+
);
48+
}
49+
50+
_onClickRemoveNotification = (id) => (event, data) => {
51+
this.props.dispatch(ToastsAction.removeById(id));
52+
};
53+
}
54+
55+
export { Toasts as Unconnected };
56+
export default connect(mapStateToProps)(Toasts);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.wrapper {
2+
display: flex;
3+
flex-direction: column;
4+
overflow: hidden;
5+
padding: 16px;
6+
position: fixed;
7+
right: 0;
8+
top: 0;
9+
z-index: 10;
10+
}

tslint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"],
33
"jsRules": {
44
"object-literal-sort-keys": false,
5-
"no-console": [true, "log"]
5+
"no-console": [true, "log"],
6+
"variable-name": [true, "ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"]
67
},
78
"rules": {
89
"array-type": [true, "array"],

0 commit comments

Comments
 (0)