diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..97df8c0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM node:11 + +RUN mkdir /usr/src/app +WORKDIR /usr/src/app + +COPY . /usr/src/app + +RUN yarn + +CMD ["npm", "start"] + +EXPOSE 3000 diff --git a/swapi/actions.js b/swapi/actions.js index 9476209..00084fd 100644 --- a/swapi/actions.js +++ b/swapi/actions.js @@ -3,10 +3,32 @@ import * as types from './types'; const sleep = (seconds, withValue) => new Promise(resolve => setTimeout(resolve, seconds * 1000, withValue)); export const onChooseEndpoint = (endpoint) => (dispatch, getState) => { + const url = `https://swapi.co/api/${endpoint}/`; + dispatch({ type: types.PICK_ENDPOINT, payload: endpoint }); dispatch({ type: types.START_LOAD }); - function loadData(url, loadedData = []) { + const isCached = () => { + var current_state = getState(); + var data = current_state.data[endpoint]; + + if (data === undefined || data.length === 0) + return false; + + dispatch({ + type: types.SET_DATA, + payload: { + endpoint: endpoint, + data: data, + } + }); + + dispatch({ type: types.DONE_LOAD }); + + return true; + } + + const loadData = (url, loadedData = []) => { dispatch({ type: types.START_LOAD }); return fetch(url) @@ -35,10 +57,73 @@ export const onChooseEndpoint = (endpoint) => (dispatch, getState) => { } - return loadData('https://swapi.co/api/' + endpoint + '/') + const loadSchema = (url) => { + dispatch({ type: types.START_LOAD }); + + return fetch(url + 'schema') + .then((response) => response.json()) + .then((json) => { + dispatch({ + type: types.SET_SCHEMA, + payload: { + endpoint: endpoint, + data: json, + } + }); + + dispatch({ type: types.DONE_LOAD }); + }) + .catch(() => console.error('Oops')) + } + + if (isCached()) + return; + + return Promise.all([loadData(url), loadSchema(url)]) .catch((err) => { console.error(err); alert('It went wrong.') }) .then(() => dispatch({ type: types.DONE_LOAD })) }; + +export const onOrderList = (selectedKey) => (dispatch, getState) => { + dispatch({ type: types.START_LOAD }); + + var current_state = getState(); + + var data = [].concat(current_state.data[current_state.endpoint]); + data.sort((a, b) => ((a[selectedKey] || '').toString()).localeCompare((b[selectedKey] || '').toString(), 'en', {sensitivity: 'base'})); + + dispatch({ + type: types.SET_DATA, + payload: { + endpoint: current_state.endpoint, + data: data, + } + }); + + dispatch({ type: types.DONE_LOAD }); +}; + +export const onExpandToggle = (url) => (dispatch, getState) => { + dispatch({ type: types.START_LOAD }); + + dispatch({ + type: types.EXPAND_ITEM, + url: url, + }); + + dispatch({ type: types.DONE_LOAD }); +}; + +export const onExpandAll = (expand) => (dispatch, getState) => { + dispatch({ type: types.START_LOAD }); + + dispatch({ + type: types.EXPAND_ITEM, + expandAll: expand, + }); + + dispatch({ type: types.DONE_LOAD }); +}; diff --git a/swapi/components/category-display-connected.js b/swapi/components/category-display-connected.js new file mode 100644 index 0000000..224c82f --- /dev/null +++ b/swapi/components/category-display-connected.js @@ -0,0 +1,20 @@ +import { bindActionCreators} from 'redux'; +import { connect } from 'react-redux'; +import CategoryDisplay from '../components/category-display'; + +import { onChooseEndpoint } from '../actions'; + +const mapStateToProps = (state, categoryDisplay) => { + return { + loading: state.operations > 0, + endpoint: state.endpoint + } +}; + +const mapDispatchToProps = (dispatch) => { + return bindActionCreators({ + onChooseEndpoint, + }, dispatch); +}; + +export default connect(mapStateToProps, mapDispatchToProps)(CategoryDisplay); diff --git a/swapi/components/category-display.js b/swapi/components/category-display.js new file mode 100644 index 0000000..43f42c2 --- /dev/null +++ b/swapi/components/category-display.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export default class CategoryDisplay extends React.PureComponent { + + static propTypes = { + onChooseEndpoint: PropTypes.func.isRequired, + children: PropTypes.shape({ + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }).isRequired, + endpoint: PropTypes.string, + loading: PropTypes.bool.isRequired, + }; + + handleChooseEndpoint = () => this.props.onChooseEndpoint(this.props.children.key); + + render() { + const { children: { key, name }, loading, endpoint } = this.props; + + const getEndpointButtonClassNames = classNames('btn', { + 'btn-default': key !== endpoint, + 'btn-success': key === endpoint + }); + + return (); + } +} diff --git a/swapi/components/item-display-connected.js b/swapi/components/item-display-connected.js new file mode 100644 index 0000000..fe608a4 --- /dev/null +++ b/swapi/components/item-display-connected.js @@ -0,0 +1,48 @@ +import { bindActionCreators} from 'redux'; +import { connect } from 'react-redux'; +import ItemDisplay from '../components/item-display'; + +import { onExpandToggle } from '../actions'; + +const getIconName = (data) => { + switch (data.kind) { + case 'people': + if (data.gender == 'female') + return 'fa-female' + else if (data.gender == 'male') + return 'fa-male' + else + return 'fa-android' + case 'films': + return 'fa-film' + case 'planets': + return 'fa-globe' + case 'species': + return 'fa-user' + case 'starships': + return 'fa-plane' + case 'vehicles': + return 'fa-bus' + } +} + +const mapStateToProps = (state, itemDisplay) => { + const data = Object.assign({}, itemDisplay.children); + const isExpanded = data.isExpanded; + delete data.isExpanded; + + return { + data, + isExpanded, + loading: state.operations > 0, + getIconName + } +}; + +const mapDispatchToProps = (dispatch) => { + return bindActionCreators({ + onExpandToggle, + }, dispatch); +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ItemDisplay); diff --git a/swapi/components/item-display.js b/swapi/components/item-display.js index 67d26c4..03f65f1 100644 --- a/swapi/components/item-display.js +++ b/swapi/components/item-display.js @@ -1,37 +1,38 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; export default class ItemDisplay extends React.PureComponent { static propTypes = { + onExpandToggle: PropTypes.func.isRequired, children: PropTypes.shape({ url: PropTypes.string.isRequired, name: PropTypes.string.isRequired, kind: PropTypes.string.isRequired, - }) + }), + isExpanded: PropTypes.bool, + loading: PropTypes.bool.isRequired, + getIconName: PropTypes.func.isRequired, }; - state = { - isExpanded: false, - }; - - handleExpandToggle = () => { - this.setState((state) => ({ isExpanded: !state.isExpanded })); - }; + handleExpandToggle = () => this.props.onExpandToggle(this.props.children.url); render() { - const { children: { name }, onExpandToggle} = this.props; - const { isExpanded } = this.state; + const { data, onExpandToggle, isExpanded, loading, getIconName } = this.props; return ( - ); } } diff --git a/swapi/components/millennium-falcon-connected.js b/swapi/components/millennium-falcon-connected.js index b2e74f3..65e1e8e 100644 --- a/swapi/components/millennium-falcon-connected.js +++ b/swapi/components/millennium-falcon-connected.js @@ -2,23 +2,52 @@ import { bindActionCreators} from 'redux'; import { connect } from 'react-redux'; import MillenniumFalcon from '../components/millennium-falcon'; -import { onChooseEndpoint } from '../actions'; +import { onChooseEndpoint, onOrderList, onExpandAll } from '../actions'; + +const getSchemaProperties = (schema, endpoint) => { + return (schema && schema[endpoint] && schema[endpoint].properties) || []; +} + +const getOrderableSchemaProperties = (schema, endpoint) => { + var schemaProperties = getSchemaProperties(schema, endpoint); + return Object.keys(schemaProperties) + .map((propertyKey) => { + var value = schemaProperties[propertyKey]; + value['key'] = propertyKey; + return value; + }) + .filter((property) => (property.type === 'string' || property.type === 'integer') && property.format === undefined); +} const mapStateToProps = (state) => { - const { endpoint, data } = state; + const { endpoint, data, schema } = state; const list = (data && data[endpoint]) || []; - console.log(state.operations); + + const categoryList = [ + {key: 'people', name: 'People'}, + {key: 'films', name: 'Films'}, + {key: 'planets', name: 'Planets'}, + {key: 'species', name: 'Species'}, + {key: 'starships', name: 'Starships'}, + {key: 'vehicles', name: 'Vehicles'} + ]; + return { list, + categoryList, endpoint, loading: state.operations > 0, + hasData: list.length > 0, + orderOptions: getOrderableSchemaProperties(schema, endpoint), } }; const mapDispatchToProps = (dispatch) => { return bindActionCreators({ - onChooseEndpoint: onChooseEndpoint, + onChooseEndpoint, + onOrderList, + onExpandAll, }, dispatch); }; diff --git a/swapi/components/millennium-falcon.js b/swapi/components/millennium-falcon.js index 72b7884..9ea6c44 100644 --- a/swapi/components/millennium-falcon.js +++ b/swapi/components/millennium-falcon.js @@ -1,20 +1,32 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import ItemDisplay from './item-display'; +import ItemDisplayConnected from './item-display-connected'; +import CategoryDisplayConnected from './category-display-connected'; export default class MillenniumFalcon extends React.PureComponent { static propTypes = { onChooseEndpoint: PropTypes.func.isRequired, + onOrderList: PropTypes.func.isRequired, + onExpandAll: PropTypes.func.isRequired, list: PropTypes.arrayOf(PropTypes.shape({ url: PropTypes.string.isRequired, })).isRequired, + categoryList: PropTypes.arrayOf(PropTypes.shape({ + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + })).isRequired, side: PropTypes.string, }; + handleOrderList = (e) => this.props.onOrderList(e.target.value); + + handleExpandAll = () => this.props.onExpandAll(true); + handleCollapseAll = () => this.props.onExpandAll(false); + render() { - const { loading, list, onChooseEndpoint } = this.props; + const { loading, hasData, list, categoryList, onChooseEndpoint, onOrderList, onExpandAll, endpoint, orderOptions } = this.props; const iconClass = classNames('fa', { 'fa-refresh': loading, @@ -25,12 +37,23 @@ export default class MillenniumFalcon extends React.PureComponent { return (
What do you want to see?