Skip to content

Commit 0fec8ae

Browse files
Add books list feature as example (#63)
* Add books list feature as example * Add warning about books list & main template
1 parent 16cad91 commit 0fec8ae

File tree

12 files changed

+747
-480
lines changed

12 files changed

+747
-480
lines changed

client/src/Theme.js

Lines changed: 412 additions & 0 deletions
Large diffs are not rendered by default.

client/src/Welcome.js

Lines changed: 57 additions & 457 deletions
Large diffs are not rendered by default.

client/src/actions/book/list.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import fetch from '../../utils/fetch';
2+
3+
export function error(error) {
4+
return {type: 'BOOK_LIST_ERROR', error};
5+
}
6+
7+
export function loading(loading) {
8+
return {type: 'BOOK_LIST_LOADING', loading};
9+
}
10+
11+
export function success(data) {
12+
return {type: 'BOOK_LIST_SUCCESS', data};
13+
}
14+
15+
export function list(page = '/books') {
16+
return (dispatch) => {
17+
dispatch(loading(true));
18+
dispatch(error(''));
19+
20+
fetch(page)
21+
.then(response => response.json())
22+
.then(data => {
23+
dispatch(loading(false));
24+
dispatch(success(data));
25+
})
26+
.catch(e => {
27+
dispatch(loading(false));
28+
dispatch(error(e.message))
29+
});
30+
};
31+
}
32+
33+
export function reset() {
34+
return {type: 'BOOK_LIST_RESET'};
35+
}

client/src/components/book/List.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* This is a demo component using a demo template.
3+
* Please remove them and create yours.
4+
*/
5+
import React, {Component, Fragment} from 'react';
6+
import {connect} from 'react-redux';
7+
import {Link} from 'react-router-dom';
8+
import PropTypes from 'prop-types';
9+
import {list, reset} from '../../actions/book/list';
10+
import {itemToLinks} from '../../utils/helpers';
11+
12+
class List extends Component {
13+
static propTypes = {
14+
error: PropTypes.string,
15+
loading: PropTypes.bool.isRequired,
16+
data: PropTypes.object.isRequired,
17+
list: PropTypes.func.isRequired,
18+
reset: PropTypes.func.isRequired,
19+
};
20+
21+
componentDidMount() {
22+
this.props.list(this.props.match.params.page && decodeURIComponent(this.props.match.params.page));
23+
}
24+
25+
componentWillReceiveProps(nextProps) {
26+
if (this.props.match.params.page !== nextProps.match.params.page) nextProps.list(nextProps.match.params.page && decodeURIComponent(nextProps.match.params.page));
27+
}
28+
29+
componentWillUnmount() {
30+
this.props.reset();
31+
}
32+
33+
render() {
34+
return <Fragment>
35+
<div className="main__aside"></div>
36+
<div className="main__content">
37+
<div className="alert alert-warning">This is a demo component using a demo template. Please remove them and create yours.</div>
38+
<h1>Books List</h1>
39+
<div className="main__other">
40+
{this.props.loading && <div className="alert alert-info">Loading...</div>}
41+
{this.props.error && <div className="alert alert-danger">{this.props.error}</div>}
42+
43+
<table className="table table-responsive table-striped table-hover">
44+
<thead>
45+
<tr>
46+
<th>Id</th>
47+
<th>isbn</th>
48+
<th>title</th>
49+
<th>description</th>
50+
<th>author</th>
51+
<th>publicationDate</th>
52+
<th>reviews</th>
53+
</tr>
54+
</thead>
55+
<tbody>
56+
{this.props.data['hydra:member'] && this.props.data['hydra:member'].map(item =>
57+
<tr key={item['@id']}>
58+
<th scope="row"><Link to={`show/${encodeURIComponent(item['@id'])}`}>{item['@id']}</Link></th>
59+
<td>{item['isbn'] ? itemToLinks(item['isbn']) : ''}</td>
60+
<td>{item['title'] ? itemToLinks(item['title']) : ''}</td>
61+
<td>{item['description'] ? itemToLinks(item['description']) : ''}</td>
62+
<td>{item['author'] ? itemToLinks(item['author']) : ''}</td>
63+
<td>{item['publicationDate'] ? itemToLinks(item['publicationDate']) : ''}</td>
64+
<td>{item['reviews'] ? itemToLinks(item['reviews']) : ''}</td>
65+
</tr>
66+
)}
67+
</tbody>
68+
</table>
69+
</div>
70+
</div>
71+
72+
{this.pagination()}
73+
</Fragment>;
74+
}
75+
76+
pagination() {
77+
const view = this.props.data['hydra:view'];
78+
if (!view) return;
79+
80+
const {'hydra:first': first, 'hydra:previous': previous,'hydra:next': next, 'hydra:last': last} = view;
81+
82+
return <nav aria-label="Page navigation">
83+
<Link to='.' className={`btn btn-primary${previous ? '' : ' disabled'}`}><span aria-hidden="true">&lArr;</span> First</Link>
84+
<Link to={!previous || previous === first ? '.' : encodeURIComponent(previous)} className={`btn btn-primary${previous ? '' : ' disabled'}`}><span aria-hidden="true">&larr;</span> Previous</Link>
85+
<Link to={next ? encodeURIComponent(next) : '#'} className={`btn btn-primary${next ? '' : ' disabled'}`}>Next <span aria-hidden="true">&rarr;</span></Link>
86+
<Link to={last ? encodeURIComponent(last) : '#'} className={`btn btn-primary${next ? '' : ' disabled'}`}>Last <span aria-hidden="true">&rArr;</span></Link>
87+
</nav>;
88+
}
89+
}
90+
91+
const mapStateToProps = (state) => {
92+
return {
93+
data: state.book.list.data,
94+
error: state.book.list.error,
95+
loading: state.book.list.loading,
96+
};
97+
};
98+
99+
const mapDispatchToProps = (dispatch) => {
100+
return {
101+
list: (page) => dispatch(list(page)),
102+
reset: () => {
103+
dispatch(reset());
104+
},
105+
};
106+
};
107+
108+
export default connect(mapStateToProps, mapDispatchToProps)(List);

client/src/components/book/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import List from './List';
2+
3+
export {List};

client/src/index.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,29 @@ import 'font-awesome/css/font-awesome.css';
1212
import registerServiceWorker from './registerServiceWorker';
1313
// Import your reducers and routes here
1414
import Welcome from './Welcome';
15+
import Theme from './Theme';
16+
import book from './reducers/book/';
17+
import bookRoutes from './routes/book';
1518

1619
const store = createStore(
17-
combineReducers({routing, form, /* Add your reducers here */}),
20+
combineReducers({routing, form, book, /* Add your reducers here */}),
1821
applyMiddleware(thunk),
1922
);
2023

2124
const history = syncHistoryWithStore(createBrowserHistory(), store);
2225

2326
ReactDom.render(
2427
<Provider store={store}>
25-
<Router history={history}>
26-
<Switch>
27-
<Route path="/" component={Welcome} strict={true} exact={true}/>
28-
{/* Add your routes here */}
29-
<Route render={() => <h1>Not Found</h1>}/>
30-
</Switch>
31-
</Router>
28+
<Theme>
29+
<Router history={history}>
30+
<Switch>
31+
<Route path="/" component={Welcome} strict={true} exact={true}/>
32+
{bookRoutes}
33+
{/* Add your routes here */}
34+
<Route render={() => <h1>Not Found</h1>}/>
35+
</Switch>
36+
</Router>
37+
</Theme>
3238
</Provider>,
3339
document.getElementById('root')
3440
);

client/src/reducers/book/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {combineReducers} from 'redux';
2+
import list from './list';
3+
4+
export default combineReducers({list});

client/src/reducers/book/list.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {combineReducers} from 'redux'
2+
3+
export function error(state = null, action) {
4+
switch (action.type) {
5+
case 'BOOK_LIST_ERROR':
6+
return action.error;
7+
8+
case 'BOOK_LIST_RESET':
9+
return null;
10+
11+
default:
12+
return state;
13+
}
14+
}
15+
16+
export function loading(state = false, action) {
17+
switch (action.type) {
18+
case 'BOOK_LIST_LOADING':
19+
return action.loading;
20+
21+
case 'BOOK_LIST_RESET':
22+
return false;
23+
24+
default:
25+
return state;
26+
}
27+
}
28+
29+
export function data(state = {}, action) {
30+
switch (action.type) {
31+
case 'BOOK_LIST_SUCCESS':
32+
return action.data;
33+
34+
case 'BOOK_LIST_RESET':
35+
return {};
36+
37+
default:
38+
return state;
39+
}
40+
}
41+
42+
export default combineReducers({error, loading, data});

client/src/routes/book.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
import {Route} from 'react-router-dom';
3+
import {List} from '../components/book/';
4+
5+
export default [
6+
<Route path='/books/:page?' component={List} strict={true} key='list'/>,
7+
];

client/src/welcome.css renamed to client/src/theme.css

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ body {
66

77
/***** GLOBAL *****/
88

9-
.welcome {
9+
.root {
1010
height: 100vh;
1111
width: 100vw;
1212
text-align: center;
@@ -17,33 +17,33 @@ body {
1717
background-color: #ececec;
1818
}
1919

20-
.welcome a {
20+
.root a {
2121
text-decoration: none;
2222
color: #38a9b4;
2323
font-weight: bold;
2424
}
2525

26-
.welcome h1 {
26+
.root h1 {
2727
font-family: 'Roboto Slab', serif;
2828
font-weight: 300;
2929
font-size: 36px;
3030
margin: 0 0 10px;
3131
line-height: 30px;
3232
}
3333

34-
.welcome h1 strong {
34+
.root h1 strong {
3535
font-weight: 700;
3636
color: #38a9b4;
3737
}
3838

39-
.welcome h2 {
39+
.root h2 {
4040
text-transform: uppercase;
4141
font-size: 18px;
4242
font-weight: bold;
4343
margin: 25px 0 5px;
4444
}
4545

46-
.welcome h3 {
46+
.root h3 {
4747
text-transform: uppercase;
4848
font-weight: 500;
4949
color: #38a9b4;
@@ -54,12 +54,12 @@ body {
5454

5555
/***** TOP *****/
5656

57-
.welcome__top {
57+
.root__top {
5858
background-color: #67cece;
5959
padding-bottom: 40px;
6060
}
6161

62-
.welcome__flag {
62+
.root__flag {
6363
transform: rotate(30deg);
6464
position: fixed;
6565
right: -190px;
@@ -70,7 +70,7 @@ body {
7070

7171
/***** MAIN *****/
7272

73-
.welcome__main {
73+
.root__main {
7474
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14),
7575
0 1px 18px 0 rgba(0, 0, 0, 0.12), 0 3px 5px -1px rgba(0, 0, 0, 0.3);
7676
width: 80%;
@@ -240,7 +240,7 @@ a.other__button {
240240

241241
/***** HELP *****/
242242

243-
.welcome__help {
243+
.root__help {
244244
background-color: white;
245245
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2);
246246
padding: 10px;
@@ -252,7 +252,7 @@ a.other__button {
252252
text-align: center;
253253
}
254254

255-
.welcome__help h2 {
255+
.root__help h2 {
256256
color: #aaa;
257257
font-size: 12px;
258258
margin: 10px 0;
@@ -285,7 +285,7 @@ a.other__button {
285285

286286
@media (max-width: 1200px) {
287287
.main__aside,
288-
.welcome__help {
288+
.root__help {
289289
display: none;
290290
}
291291
.main__content {
@@ -296,13 +296,13 @@ a.other__button {
296296
}
297297

298298
@media (max-width: 600px) {
299-
.welcome__main {
299+
.root__main {
300300
width: calc(100% - 40px);
301301
}
302-
.welcome h1 {
302+
.root h1 {
303303
display: none;
304304
}
305-
.welcome__flag,
305+
.root__flag,
306306
.main__other {
307307
display: none;
308308
}

client/src/utils/fetch.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { SubmissionError } from 'redux-form';
2+
import { API_HOST, API_PATH } from '../config/_entrypoint';
3+
4+
const jsonLdMimeType = 'application/ld+json';
5+
6+
export default function (url, options = {}) {
7+
if ('undefined' === typeof options.headers) options.headers = new Headers();
8+
if (null === options.headers.get('Accept')) options.headers.set('Accept', jsonLdMimeType);
9+
10+
if ('undefined' !== options.body && !(options.body instanceof FormData) && null === options.headers.get('Content-Type')) {
11+
options.headers.set('Content-Type', jsonLdMimeType);
12+
}
13+
14+
const link = url.includes(API_PATH) ? API_HOST + url : API_HOST + API_PATH + url;
15+
16+
return fetch(link, options).then(response => {
17+
if (response.ok) return response;
18+
19+
return response
20+
.json()
21+
.then(json => {
22+
const error = json['hydra:description'] || response.statusText;
23+
if (!json.violations) throw Error(error);
24+
25+
let errors = {_error: error};
26+
json.violations.map((violation) => errors[violation.propertyPath] = violation.message);
27+
28+
throw new SubmissionError(errors);
29+
});
30+
});
31+
}

0 commit comments

Comments
 (0)