Skip to content
Open
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
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
89 changes: 87 additions & 2 deletions swapi/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Har du overvejet at bruge const?

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)
Expand Down Expand Up @@ -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 });
};
20 changes: 20 additions & 0 deletions swapi/components/category-display-connected.js
Original file line number Diff line number Diff line change
@@ -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);
29 changes: 29 additions & 0 deletions swapi/components/category-display.js
Original file line number Diff line number Diff line change
@@ -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 (<button disabled={loading} onClick={this.handleChooseEndpoint} className={getEndpointButtonClassNames}>{name}</button>);
}
}
48 changes: 48 additions & 0 deletions swapi/components/item-display-connected.js
Original file line number Diff line number Diff line change
@@ -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);
27 changes: 14 additions & 13 deletions swapi/components/item-display.js
Original file line number Diff line number Diff line change
@@ -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 (<span>
<button className="btn btn-xs btn-default" onClick={this.handleExpandToggle} aria-label={isExpanded ? 'Collapse' : 'Expand'}>
<button disabled={loading} className="btn btn-xs btn-default" onClick={this.handleExpandToggle} aria-label={isExpanded ? 'Collapse' : 'Expand'}>
{isExpanded
? <i className="fa fa-chevron-circle-up" />
: <i className="fa fa-chevron-circle-down" />}
</button>
{' '}
{name}
{ isExpanded ? <pre>{JSON.stringify(this.props.children, null, 4)}</pre> : null}
<i className={classNames('fa', getIconName(data))} />
{' '}
{data.kind === 'films' ? `Episode ${data.episode_id}: ` : null}
{data.name}
{isExpanded ? <pre>{JSON.stringify(data, null, 4)}</pre> : null}
</span>);
}
}
37 changes: 33 additions & 4 deletions swapi/components/millennium-falcon-connected.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Har du overvejet om man kunne sortere på array typer? Måske på length

}

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);
};

Expand Down
35 changes: 29 additions & 6 deletions swapi/components/millennium-falcon.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,12 +37,23 @@ export default class MillenniumFalcon extends React.PureComponent {
return (<div>
<p><i className={iconClass} /> What do you want to see?</p>
<div className="btn-group">
<button disabled={loading} onClick={() => onChooseEndpoint('people')} className="btn btn-danger">People</button>
<button disabled={loading} onClick={() => onChooseEndpoint('films')} className="btn btn-danger">Films</button>
{categoryList.map((category) => <React.Fragment key={category.key}><CategoryDisplayConnected>{category}</CategoryDisplayConnected></React.Fragment>)}
</div>
<br /><br />
<div className="btn-group">
<button disabled={!hasData || loading} onClick={this.handleExpandAll} className="btn btn-default"><i className="fa fa-chevron-circle-down" /> Expand all</button>
<button disabled={!hasData || loading} onClick={this.handleCollapseAll} className="btn btn-default"><i className="fa fa-chevron-circle-up" /> Collapse all</button>
</div>
<div className="btn-group">
<select className="form-control" disabled={!hasData || loading} onChange={this.handleOrderList}>
<option defaultValue>Order by</option>
{orderOptions !== undefined && orderOptions.map((orderOption) => <option key={orderOption.key} value={orderOption.key}>{orderOption.key}</option>)}
</select>
</div>
<br /><br />
<table className="table"><tbody>
{list.map((item) => <tr key={item.url}><td><ItemDisplay>{item}</ItemDisplay></td></tr>)}
<table className="table">
<tbody>
{list.map((item) => <tr key={item.url}><td><ItemDisplayConnected>{item}</ItemDisplayConnected></td></tr>)}
</tbody>
</table>
</div>);
Expand Down
Loading