From 6f4311091dabbbc322c2340f4fc9e1f5e8e8ebec Mon Sep 17 00:00:00 2001 From: Joehc Date: Mon, 25 Feb 2019 20:09:46 +0100 Subject: [PATCH 1/2] Challenges resolved, various UI-details added, dockerfile added --- Dockerfile | 14 +++++ swapi/actions.js | 44 ++++++++++--- swapi/components/item-display.js | 61 +++++++++++++------ .../components/millennium-falcon-connected.js | 7 ++- swapi/components/millennium-falcon.js | 50 +++++++++++---- swapi/store.js | 24 ++++++++ swapi/types.js | 2 + 7 files changed, 161 insertions(+), 41 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a9f6d57 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:10 + +WORKDIR /app + +EXPOSE 3000 + +COPY package.json package.json +RUN yarn install + +COPY . /app + +RUN yarn build + +CMD [ "yarn", "start" ] \ No newline at end of file diff --git a/swapi/actions.js b/swapi/actions.js index 9476209..9c278a8 100644 --- a/swapi/actions.js +++ b/swapi/actions.js @@ -1,10 +1,26 @@ import * as types from './types'; +import { relativeTimeRounding } from 'moment'; + const sleep = (seconds, withValue) => new Promise(resolve => setTimeout(resolve, seconds * 1000, withValue)); export const onChooseEndpoint = (endpoint) => (dispatch, getState) => { dispatch({ type: types.PICK_ENDPOINT, payload: endpoint }); - dispatch({ type: types.START_LOAD }); + + const dataCached = getState().data[endpoint] + + if (!dataCached || dataCached.length === 0) { + dispatch({ type: types.START_LOAD }); + + return loadData('https://swapi.co/api/' + endpoint + '/') + .catch((err) => { + console.error(err); + alert('It went wrong.') + }) + .then(() => dispatch({ type: types.DONE_LOAD })) + } else { + return; + } function loadData(url, loadedData = []) { dispatch({ type: types.START_LOAD }); @@ -13,7 +29,9 @@ export const onChooseEndpoint = (endpoint) => (dispatch, getState) => { .then((response) => response.json()) .then((json) => { const result = json.results.map((item) => ({ ...item, name: item.title || item.name, kind: endpoint })); - loadedData = loadedData.concat(result); + + loadedData = loadedData.concat(result) + loadedData = loadedData.sort((a, b) => (a.name.toString()).localeCompare(b.name.toString(), 'en')); dispatch({ type: types.SET_DATA, @@ -34,11 +52,21 @@ export const onChooseEndpoint = (endpoint) => (dispatch, getState) => { .catch(() => console.error('Oops')) } +}; + +export const onExpandToggle = (item) => (dispatch) => { + dispatch({ type: types.TOGGLE_EXPAND_ITEM, payload: item }); +}; + +export const onMergeLists = (endpoint) => (dispatch, getState) => { + dispatch({ type: types.START_LOAD }); + dispatch({ type: types.PICK_ENDPOINT, payload: endpoint }); + + const dataCached = getState().data[endpoint] + + if (!dataCached) { + dispatch({ type: types.MERGE_LISTS, payload: endpoint }); + } - return loadData('https://swapi.co/api/' + endpoint + '/') - .catch((err) => { - console.error(err); - alert('It went wrong.') - }) - .then(() => dispatch({ type: types.DONE_LOAD })) + dispatch({ type: types.DONE_LOAD }); }; diff --git a/swapi/components/item-display.js b/swapi/components/item-display.js index 67d26c4..13c772e 100644 --- a/swapi/components/item-display.js +++ b/swapi/components/item-display.js @@ -1,37 +1,58 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; export default class ItemDisplay extends React.PureComponent { static propTypes = { children: PropTypes.shape({ - url: PropTypes.string.isRequired, name: PropTypes.string.isRequired, kind: PropTypes.string.isRequired, - }) - }; - - state = { - isExpanded: false, + url: PropTypes.string.isRequired, + expanded: PropTypes.bool, + episode_id: PropTypes.number, + gender: PropTypes.string, + }), }; - handleExpandToggle = () => { - this.setState((state) => ({ isExpanded: !state.isExpanded })); + expandToggle = () => { + this.props.setExpandToggle(this.props.children.url) }; render() { - const { children: { name }, onExpandToggle} = this.props; - const { isExpanded } = this.state; + const { children: { name, kind, expanded, gender, episode_id, charactersNames }, endpoint} = this.props; + + const iconKindClass = classNames('fa', { + 'fa-female': kind === 'people' && gender === 'female', + 'fa-male': kind === 'people' && gender === 'male', + 'fa-android': kind === 'people' && (gender === 'none' || gender === 'n/a'), + 'fa-transgender-alt': kind === 'people' && gender === 'hermaphrodite', + 'fa-flag': episode_id, + }); - return ( - - {' '} - {name} - { isExpanded ?
{JSON.stringify(this.props.children, null, 4)}
: null} -
); + return ( + + + + {' '} + {name} - + {' '} + { episode_id && + Episode {episode_id} + } + {' '} + { endpoint === 'charactersNames' && + - {charactersNames.length} + } + { endpoint === 'charactersNames' + ? expanded ?
{JSON.stringify(charactersNames, null, 4)}
: null + : expanded ?
{JSON.stringify(this.props.children, null, 4)}
: null + } +
); } } diff --git a/swapi/components/millennium-falcon-connected.js b/swapi/components/millennium-falcon-connected.js index b2e74f3..e716c0a 100644 --- a/swapi/components/millennium-falcon-connected.js +++ b/swapi/components/millennium-falcon-connected.js @@ -3,22 +3,27 @@ import { connect } from 'react-redux'; import MillenniumFalcon from '../components/millennium-falcon'; import { onChooseEndpoint } from '../actions'; +import { onExpandToggle } from '../actions'; +import { onMergeLists } from '../actions'; const mapStateToProps = (state) => { const { endpoint, data } = state; const list = (data && data[endpoint]) || []; - console.log(state.operations); + return { list, endpoint, loading: state.operations > 0, + data, } }; const mapDispatchToProps = (dispatch) => { return bindActionCreators({ onChooseEndpoint: onChooseEndpoint, + onExpandToggle: onExpandToggle, + onMergeLists: onMergeLists, }, dispatch); }; diff --git a/swapi/components/millennium-falcon.js b/swapi/components/millennium-falcon.js index 72b7884..74c56f0 100644 --- a/swapi/components/millennium-falcon.js +++ b/swapi/components/millennium-falcon.js @@ -7,6 +7,8 @@ export default class MillenniumFalcon extends React.PureComponent { static propTypes = { onChooseEndpoint: PropTypes.func.isRequired, + onExpandToggle: PropTypes.func.isRequired, + onMergeLists: PropTypes.func.isRequired, list: PropTypes.arrayOf(PropTypes.shape({ url: PropTypes.string.isRequired, })).isRequired, @@ -14,7 +16,9 @@ export default class MillenniumFalcon extends React.PureComponent { }; render() { - const { loading, list, onChooseEndpoint } = this.props; + const { loading, list, onChooseEndpoint, onExpandToggle, onMergeLists, endpoint } = this.props; + + const listName = endpoint === 'people' ? endpoint : 'films' const iconClass = classNames('fa', { 'fa-refresh': loading, @@ -22,17 +26,39 @@ export default class MillenniumFalcon extends React.PureComponent { 'fa-star': !loading, }); - return (
-

What do you want to see?

-
- - + return ( +
+

+ + {' '} + {loading + ? Loading {endpoint}, please wait... + : What do you want to see? + } +
+ {!this.props.data.films || !this.props.data.people + ? Hint: A secret button will appear when you have seen both the lists of characters and films + : You can now see the new button + } +

+
+ + + {this.props.data.films && this.props.data.people && !loading && + + } +
+

+

{!loading && endpoint && + {listName.toUpperCase()}({this.props.data[endpoint].length}) + } +

+ + + {list.map((item) => )} + +
{item}
-

- - {list.map((item) => )} - -
{item}
-
); + ); } } diff --git a/swapi/store.js b/swapi/store.js index 1734d06..ecdc6b1 100644 --- a/swapi/store.js +++ b/swapi/store.js @@ -43,6 +43,30 @@ const reducer = (state = {}, action) => { operations: state.operations - 1, } } + + case types.TOGGLE_EXPAND_ITEM: { + return { + ...state, + data: { + ...state.data, + [state.endpoint]: state.data[state.endpoint].map(item => item.url === action.payload ? { ...item, expanded: !item.expanded } : item) + } + } + + } + + case types.MERGE_LISTS: { + return { + ...state, + data: { + ...state.data, + [action.payload]: state.data.films.map(film => ({ ...film, charactersNames: film.characters.map(character => ({ + name: state.data.people.filter(person => person.url === character)[0].name + })).sort((a, b) => (a.name.toString()).localeCompare(b.name.toString(), 'en')) })) + } + } + + } } return state; diff --git a/swapi/types.js b/swapi/types.js index da5a8e4..aeed8ff 100644 --- a/swapi/types.js +++ b/swapi/types.js @@ -2,3 +2,5 @@ export const PICK_ENDPOINT = 'PICK_ENDPOINT'; export const SET_DATA = 'SET_DATA'; export const START_LOAD = 'START_LOAD'; export const DONE_LOAD = 'DONE_LOAD'; +export const TOGGLE_EXPAND_ITEM = 'TOGGLE_EXPAND_ITEM'; +export const MERGE_LISTS = 'MERGE_LISTS'; \ No newline at end of file From 2e37f84ade8b8b384476d02252937f3c679b8c02 Mon Sep 17 00:00:00 2001 From: Joehc Date: Tue, 26 Feb 2019 06:50:23 +0100 Subject: [PATCH 2/2] added build command undert scripts --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 970e0b4..c49302a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "license": "MIT", "scripts": { "start-next": "next -p 3008", - "start": "next" + "start": "next", + "build": "next build" }, "dependencies": { "babel-cli": "^6.26.0",