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
File renamed without changes.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.DS_Store
9,315 changes: 9,315 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"go": "yarn install && yarn run start",
"go-npm": "npm install && npm run start",
"build": "webpack --mode production",
"start": "webpack-dev-server --mode development --open --history-api-fallback"
"start": "webpack-dev-server --mode development --open --history-api-fallback",
"test": "mocha --require @babel/register '**/**_specs.js'",
"test:watch": "npm test -- --watch"
},
"dependencies": {
"prop-types": "^15.6.2",
Expand All @@ -24,16 +26,19 @@
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.6.2",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.2",
"babel-plugin-styled-components": "^1.6.4",
"chai": "^4.2.0",
"copy-webpack-plugin": "^4.5.2",
"eslint": "^5.5.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-react": "^7.11.1",
"html-webpack-plugin": "^3.2.0",
"mocha": "^6.2.0",
"react-hot-loader": "^4.3.6",
"webpack": "^4.18.0",
"webpack-cli": "^3.1.0",
Expand Down
27 changes: 27 additions & 0 deletions src/api/dao.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
Fetch data from the API, if it fails, keep trying until timeout
*/
import api from './country';

// based on https://gist.github.com/briancavalier/842626
export const retryPromise = (fn, ms = 250, maxRetries = 5) => (
new Promise((resolve, reject) => {
let retries = 0;

fn()
.then(resolve)
.catch(() => {
setTimeout(() => {
console.log('retrying failed promise...');
retries += 1;
if (retries === maxRetries) {
return reject(new Error('maximum retries exceeded'));
}
retryPromise(fn, ms).then(resolve);
return null;
}, ms);
});
})
);

export default () => retryPromise(api);
91 changes: 91 additions & 0 deletions src/components/app/Container.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import fetchCountries from '../../api/dao';
import {
updateCountries,
updateCountry,
deleteCountry,
selectCountry,
} from '../../store/actions';
import Dropdown from './Dropdown';
import Editor from './Editor';

class Container extends PureComponent {
componentDidMount() {
fetchCountries()
.then((countries) => {
this.props.updateCountries(countries); /* eslint-disable-line react/destructuring-assignment */
})
.catch(console.error);
}

onCountrySelected = (countryId) => {
this.props.selectCountry(countryId); /* eslint-disable-line react/destructuring-assignment */
}

onCountryUpdated = (country) => {
this.props.updateCountry(country); /* eslint-disable-line react/destructuring-assignment */
}

onCountryDeleted = (countryId) => {
this.props.deleteCountry(countryId); /* eslint-disable-line react/destructuring-assignment */
}

render() {
const { countries, selectedCountry } = this.props;

const isLoading = countries.length === 0;
if (isLoading) {
return <div>Loading countries...</div>;
}

return (
<div>
<Dropdown
items={countries}
onChange={this.onCountrySelected}
/>
{selectedCountry && (
<Editor
country={selectedCountry}
onSubmit={this.onCountryUpdated}
onDelete={this.onCountryDeleted}
/>
)}
</div>
);
}
}

Container.propTypes = {
countries: PropTypes.arrayOf(PropTypes.object),
selectedCountry: PropTypes.shape({
code: PropTypes.string,
name: PropTypes.string,
population: PropTypes.number,
}),
updateCountries: PropTypes.func.isRequired,
updateCountry: PropTypes.func.isRequired,
deleteCountry: PropTypes.func.isRequired,
selectCountry: PropTypes.func.isRequired,
};

Container.defaultProps = {
countries: [],
selectedCountry: null,
};

export const mapStateToProps = ({ countries }) => {
// find selected country
const selectedCountry = countries.find(c => c.isSelected);

return { countries, selectedCountry };
};

export const mapDispatchToProps = {
updateCountries, updateCountry, deleteCountry, selectCountry,
};

export default connect(mapStateToProps, mapDispatchToProps)(Container);
38 changes: 38 additions & 0 deletions src/components/app/Dropdown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';

function Dropdown({ items, onChange }) {
return (
<select onChange={evt => onChange(evt.target.value)}>
<option value="" disabled selected>Select country</option>
{items.map((item) => {
const { isSelected } = item;
const renderedPop = (item.population !== undefined)
? ` - pop: ${item.population}` : '';

return (
<option
key={item.code}
value={item.code}
selected={isSelected}
>
{item.name}
{renderedPop}
</option>
);
})}
</select>
);
}

Dropdown.propTypes = {
items: PropTypes.arrayOf(PropTypes.object),
onChange: PropTypes.func,
};

Dropdown.defaultProps = {
items: [],
onChange: () => {},
};

export default Dropdown;
108 changes: 108 additions & 0 deletions src/components/app/Editor.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';

import Button from './Button';
import Label from './Label';
import Input from './Input';

class Editor extends Component {
state = {
code: '',
name: '',
population: 0,
}

static getDerivedStateFromProps(props, state) {
// country code has changed
const { country } = props;
if (country.code !== state.code) {
const newState = { ...country };

// we've changed country, update state
// make sure we have some population
if (!newState.population) {
newState.population = 0;
}
return newState;
}

return state;
}

onInputChange = (name, value) => {
this.setState({ [name]: value });
}

onSubmit = (e) => {
const { onSubmit } = this.props;
e.preventDefault();

// TODO: for now we can just spread state, as we don't have non-country
// props keys in it
onSubmit({ ...this.state });
}

onDelete = () => {
const { country, onDelete } = this.props;

// ask for confirmation using browser native window
const isConfirmed = window.confirm(`Are you sure you want to delete ${country.name}`);

if (isConfirmed) {
onDelete(country.code);
}
}

render() {
const { name, population } = this.state;

return (
<form onSubmit={this.onSubmit}>
<Label label="Name" htmlFor="name">
<Input
type="input"
name="name"
value={name}
onChange={e => this.onInputChange('name', e.target.value)}
/>
</Label>
<Label label="Population" htmlFor="population">
<Input
type="input"
name="population"
value={population}
onChange={e => this.onInputChange('population', +e.target.value)}
/>
</Label>
<Button
label="Update"
type="submit"
/>
<Button
type="button"
label="Delete"
onClick={this.onDelete}
color="quaternary"
/>
</form>
);
}
}

Editor.propTypes = {
country: PropTypes.shape({
code: PropTypes.string,
name: PropTypes.string,
population: PropTypes.number,
}),
onSubmit: PropTypes.func,
onDelete: PropTypes.func,
};

Editor.defaultProps = {
country: null,
onSubmit: () => {},
onDelete: () => {},
};

export default Editor;
19 changes: 19 additions & 0 deletions src/components/app/Input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import React from 'react';
import styled from 'styled-components';

const InputStyled = styled.input`
padding: 5px 10px;
width: 100%;
max-width: 200px;
`;

const Input = ({ ...props }) => (
<InputStyled {...props} />
);

Input.propTypes = {};

Input.defaultProps = {};

export default Input;
34 changes: 34 additions & 0 deletions src/components/app/Label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

const LabelStyled = styled.label`
display: block;
margin: 10px 0;
`;

const SpanStyled = styled.span`
font-size: 12px;
`;

const Label = ({ label, children, ...props }) => (
<LabelStyled {...props}>
<SpanStyled>{label}</SpanStyled>
<div>
{children}
</div>
</LabelStyled>
);

Label.propTypes = {
label: PropTypes.string,
children: PropTypes.node,
};

Label.defaultProps = {
label: '',
children: null,
};

export default Label;
6 changes: 3 additions & 3 deletions src/components/app/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import Theme from '../theme';
import Container from './Container';
import createStore from '../../store';
import Layout from '../layout';
import H1 from './H1';
Expand All @@ -13,9 +14,8 @@ export default () => (
<Provider store={store}>
<Theme>
<Layout>
<H1>
Good luck!
</H1>
<H1>Countries</H1>
<Container />
</Layout>
</Theme>
</Provider>
Expand Down
10 changes: 8 additions & 2 deletions src/components/theme/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

/* eslint no-unused-expressions: 0 */
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { injectGlobal, ThemeProvider } from 'styled-components';
Expand Down Expand Up @@ -61,7 +61,13 @@ Theme.propTypes = {
/**
* Theme config
*/
theme: PropTypes.object,
theme: PropTypes.objectOf(
PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
),
};

Theme.defaultProps = {
Expand Down
Loading