Skip to content

Add books list feature as example #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
412 changes: 412 additions & 0 deletions client/src/Theme.js

Large diffs are not rendered by default.

514 changes: 57 additions & 457 deletions client/src/Welcome.js

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions client/src/actions/book/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import fetch from '../../utils/fetch';

export function error(error) {
return {type: 'BOOK_LIST_ERROR', error};
}

export function loading(loading) {
return {type: 'BOOK_LIST_LOADING', loading};
}

export function success(data) {
return {type: 'BOOK_LIST_SUCCESS', data};
}

export function list(page = '/books') {
return (dispatch) => {
dispatch(loading(true));
dispatch(error(''));

fetch(page)
.then(response => response.json())
.then(data => {
dispatch(loading(false));
dispatch(success(data));
})
.catch(e => {
dispatch(loading(false));
dispatch(error(e.message))
});
};
}

export function reset() {
return {type: 'BOOK_LIST_RESET'};
}
108 changes: 108 additions & 0 deletions client/src/components/book/List.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* This is a demo component using a demo template.
* Please remove them and create yours.
*/
import React, {Component, Fragment} from 'react';
import {connect} from 'react-redux';
import {Link} from 'react-router-dom';
import PropTypes from 'prop-types';
import {list, reset} from '../../actions/book/list';
import {itemToLinks} from '../../utils/helpers';

class List extends Component {
static propTypes = {
error: PropTypes.string,
loading: PropTypes.bool.isRequired,
data: PropTypes.object.isRequired,
list: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
};

componentDidMount() {
this.props.list(this.props.match.params.page && decodeURIComponent(this.props.match.params.page));
}

componentWillReceiveProps(nextProps) {
if (this.props.match.params.page !== nextProps.match.params.page) nextProps.list(nextProps.match.params.page && decodeURIComponent(nextProps.match.params.page));
}

componentWillUnmount() {
this.props.reset();
}

render() {
return <Fragment>
<div className="main__aside"></div>
<div className="main__content">
<div className="alert alert-warning">This is a demo component using a demo template. Please remove them and create yours.</div>
<h1>Books List</h1>
<div className="main__other">
{this.props.loading && <div className="alert alert-info">Loading...</div>}
{this.props.error && <div className="alert alert-danger">{this.props.error}</div>}

<table className="table table-responsive table-striped table-hover">
<thead>
<tr>
<th>Id</th>
<th>isbn</th>
<th>title</th>
<th>description</th>
<th>author</th>
<th>publicationDate</th>
<th>reviews</th>
</tr>
</thead>
<tbody>
{this.props.data['hydra:member'] && this.props.data['hydra:member'].map(item =>
<tr key={item['@id']}>
<th scope="row"><Link to={`show/${encodeURIComponent(item['@id'])}`}>{item['@id']}</Link></th>
<td>{item['isbn'] ? itemToLinks(item['isbn']) : ''}</td>
<td>{item['title'] ? itemToLinks(item['title']) : ''}</td>
<td>{item['description'] ? itemToLinks(item['description']) : ''}</td>
<td>{item['author'] ? itemToLinks(item['author']) : ''}</td>
<td>{item['publicationDate'] ? itemToLinks(item['publicationDate']) : ''}</td>
<td>{item['reviews'] ? itemToLinks(item['reviews']) : ''}</td>
</tr>
)}
</tbody>
</table>
</div>
</div>

{this.pagination()}
</Fragment>;
}

pagination() {
const view = this.props.data['hydra:view'];
if (!view) return;

const {'hydra:first': first, 'hydra:previous': previous,'hydra:next': next, 'hydra:last': last} = view;

return <nav aria-label="Page navigation">
<Link to='.' className={`btn btn-primary${previous ? '' : ' disabled'}`}><span aria-hidden="true">&lArr;</span> First</Link>
<Link to={!previous || previous === first ? '.' : encodeURIComponent(previous)} className={`btn btn-primary${previous ? '' : ' disabled'}`}><span aria-hidden="true">&larr;</span> Previous</Link>
<Link to={next ? encodeURIComponent(next) : '#'} className={`btn btn-primary${next ? '' : ' disabled'}`}>Next <span aria-hidden="true">&rarr;</span></Link>
<Link to={last ? encodeURIComponent(last) : '#'} className={`btn btn-primary${next ? '' : ' disabled'}`}>Last <span aria-hidden="true">&rArr;</span></Link>
</nav>;
}
}

const mapStateToProps = (state) => {
return {
data: state.book.list.data,
error: state.book.list.error,
loading: state.book.list.loading,
};
};

const mapDispatchToProps = (dispatch) => {
return {
list: (page) => dispatch(list(page)),
reset: () => {
dispatch(reset());
},
};
};

export default connect(mapStateToProps, mapDispatchToProps)(List);
3 changes: 3 additions & 0 deletions client/src/components/book/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import List from './List';

export {List};
22 changes: 14 additions & 8 deletions client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,29 @@ import 'font-awesome/css/font-awesome.css';
import registerServiceWorker from './registerServiceWorker';
// Import your reducers and routes here
import Welcome from './Welcome';
import Theme from './Theme';
import book from './reducers/book/';
import bookRoutes from './routes/book';

const store = createStore(
combineReducers({routing, form, /* Add your reducers here */}),
combineReducers({routing, form, book, /* Add your reducers here */}),
applyMiddleware(thunk),
);

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

ReactDom.render(
<Provider store={store}>
<Router history={history}>
<Switch>
<Route path="/" component={Welcome} strict={true} exact={true}/>
{/* Add your routes here */}
<Route render={() => <h1>Not Found</h1>}/>
</Switch>
</Router>
<Theme>
<Router history={history}>
<Switch>
<Route path="/" component={Welcome} strict={true} exact={true}/>
{bookRoutes}
{/* Add your routes here */}
<Route render={() => <h1>Not Found</h1>}/>
</Switch>
</Router>
</Theme>
</Provider>,
document.getElementById('root')
);
Expand Down
4 changes: 4 additions & 0 deletions client/src/reducers/book/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {combineReducers} from 'redux';
import list from './list';

export default combineReducers({list});
42 changes: 42 additions & 0 deletions client/src/reducers/book/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {combineReducers} from 'redux'

export function error(state = null, action) {
switch (action.type) {
case 'BOOK_LIST_ERROR':
return action.error;

case 'BOOK_LIST_RESET':
return null;

default:
return state;
}
}

export function loading(state = false, action) {
switch (action.type) {
case 'BOOK_LIST_LOADING':
return action.loading;

case 'BOOK_LIST_RESET':
return false;

default:
return state;
}
}

export function data(state = {}, action) {
switch (action.type) {
case 'BOOK_LIST_SUCCESS':
return action.data;

case 'BOOK_LIST_RESET':
return {};

default:
return state;
}
}

export default combineReducers({error, loading, data});
7 changes: 7 additions & 0 deletions client/src/routes/book.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import {Route} from 'react-router-dom';
import {List} from '../components/book/';

export default [
<Route path='/books/:page?' component={List} strict={true} key='list'/>,
];
30 changes: 15 additions & 15 deletions client/src/welcome.css → client/src/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ body {

/***** GLOBAL *****/

.welcome {
.root {
height: 100vh;
width: 100vw;
text-align: center;
Expand All @@ -17,33 +17,33 @@ body {
background-color: #ececec;
}

.welcome a {
.root a {
text-decoration: none;
color: #38a9b4;
font-weight: bold;
}

.welcome h1 {
.root h1 {
font-family: 'Roboto Slab', serif;
font-weight: 300;
font-size: 36px;
margin: 0 0 10px;
line-height: 30px;
}

.welcome h1 strong {
.root h1 strong {
font-weight: 700;
color: #38a9b4;
}

.welcome h2 {
.root h2 {
text-transform: uppercase;
font-size: 18px;
font-weight: bold;
margin: 25px 0 5px;
}

.welcome h3 {
.root h3 {
text-transform: uppercase;
font-weight: 500;
color: #38a9b4;
Expand All @@ -54,12 +54,12 @@ body {

/***** TOP *****/

.welcome__top {
.root__top {
background-color: #67cece;
padding-bottom: 40px;
}

.welcome__flag {
.root__flag {
transform: rotate(30deg);
position: fixed;
right: -190px;
Expand All @@ -70,7 +70,7 @@ body {

/***** MAIN *****/

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

/***** HELP *****/

.welcome__help {
.root__help {
background-color: white;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2);
padding: 10px;
Expand All @@ -252,7 +252,7 @@ a.other__button {
text-align: center;
}

.welcome__help h2 {
.root__help h2 {
color: #aaa;
font-size: 12px;
margin: 10px 0;
Expand Down Expand Up @@ -285,7 +285,7 @@ a.other__button {

@media (max-width: 1200px) {
.main__aside,
.welcome__help {
.root__help {
display: none;
}
.main__content {
Expand All @@ -296,13 +296,13 @@ a.other__button {
}

@media (max-width: 600px) {
.welcome__main {
.root__main {
width: calc(100% - 40px);
}
.welcome h1 {
.root h1 {
display: none;
}
.welcome__flag,
.root__flag,
.main__other {
display: none;
}
Expand Down
31 changes: 31 additions & 0 deletions client/src/utils/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SubmissionError } from 'redux-form';
import { API_HOST, API_PATH } from '../config/_entrypoint';

const jsonLdMimeType = 'application/ld+json';

export default function (url, options = {}) {
if ('undefined' === typeof options.headers) options.headers = new Headers();
if (null === options.headers.get('Accept')) options.headers.set('Accept', jsonLdMimeType);

if ('undefined' !== options.body && !(options.body instanceof FormData) && null === options.headers.get('Content-Type')) {
options.headers.set('Content-Type', jsonLdMimeType);
}

const link = url.includes(API_PATH) ? API_HOST + url : API_HOST + API_PATH + url;

return fetch(link, options).then(response => {
if (response.ok) return response;

return response
.json()
.then(json => {
const error = json['hydra:description'] || response.statusText;
if (!json.violations) throw Error(error);

let errors = {_error: error};
json.violations.map((violation) => errors[violation.propertyPath] = violation.message);

throw new SubmissionError(errors);
});
});
}
Loading