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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.idea/
28 changes: 3 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,7 @@
# React Developer Test

## The Task
## Dillon Lee

Create a page that allows the user to select a country from a list and enter their population.
This task should take 2-3 hours but don't worry if you aren't able to complete all items, just
make sure to show your understanding of the core technologies we use.
`yarn start` - run application

1. Fork this repo
2. Get the list of available countries from the country API in `src/api/country.js`
3. Create a form which allows the user to select a country from a dropdown and enter their population
4. Sort the countries by population
5. Allow entries to be updated
6. Allow entries to be deleted
7. Add some styling
8. When you're done commit your code and create a pull request

A basic project outline has been created to help you get started quickly but feel free to start
from scratch if you have a prefered setup.

We predominantly use React, Redux, StyledComponents, Node.js, Webpack, Babel

Feel free to use the internet including Google and Stackoverflow to help with the task

## Any questions?

Please just ask.

Good luck and thanks for taking the time to complete this task!
`yarn tests` - run tests
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
"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": "yarn jest"
},
"dependencies": {
"prop-types": "^15.6.2",
"prop-types": "^15.7.2",
"react": "^16.5.0",
"react-dom": "^16.5.0",
"react-redux": "^5.0.7",
Expand All @@ -28,15 +29,23 @@
"babel-loader": "^8.0.2",
"babel-plugin-styled-components": "^1.6.4",
"copy-webpack-plugin": "^4.5.2",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.11.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",
"jest": "^24.5.0",
"react-hot-loader": "^4.3.6",
"webpack": "^4.18.0",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.8"
},
"jest": {
"setupFilesAfterEnv": [
"<rootDir>src/setupTests.js"
]
}
}
128 changes: 128 additions & 0 deletions src/components/app/PopulationForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addPopulation } from "../../store/actions";

const InputSection = styled.div`
align-items: center;
margin-bottom: 20px;


select, input[type="number"] {
height: 40px;
background: white;
border: grey 1px solid;
display: block;
width: 100%;
}

input[type="submit"] {
width: 100%;
background: blue;
color: white;
padding: 10px;
border-radius: 5px;
}
`;

const Error = styled.span`
color: red;
`;
Error.displayName = 'Error';


export class PopulationForm extends React.PureComponent {

static propTypes = {
countryList: PropTypes.array,
addPopulation: PropTypes.func
}

state = {
country: this.props.countryList[0].name,
population: '',
error: '',
};

handleChange = (event) => {
const stateKey = event.target.getAttribute('name');
const stateVal = event.target.value;
this.setState(() => ({
[stateKey] : stateVal,
}));
}

handleSubmit = (event) => {
event.preventDefault();
const { country, population } = this.state;
if(!country || !population){
this.setState(() => ({error: 'please complete form'}));
return;
}

this.props.addPopulation({country, population});
this.resetState();
}

resetState = () => {
this.setState(() => ({
country: this.props.countryList[0].name,
population: '',
error: '',
}))
}

render () {
const { country, population, error } = this.state;
const { countryList } = this.props;
return (
<form onSubmit={this.handleSubmit}>
<InputSection>
<label htmlFor="country">Country</label>
<select
value={country}
id="country"
name="country"
onChange={this.handleChange}
>
{countryList.map(({name, code}) => (
<option key={code}>{name}</option>
))}
</select>
</InputSection>

<InputSection>
<label htmlFor="population">Population</label>
<input
name="population"
id="population"
type="number"
placeholder="population"
value={population}
onChange={this.handleChange}
/>
</InputSection>

<InputSection>
<input type="submit" value="Add Population"/>
</InputSection>
<Error>{error}</Error>

</form>
)
}
}

const mapStateToProps = (state) => ({
countryList: state.countryList,
});

const mapDispatchToProps = (dispatch) => ({
addPopulation: (country) => dispatch(addPopulation(country))
});

export default connect(
mapStateToProps,
mapDispatchToProps,
)(PopulationForm)
58 changes: 58 additions & 0 deletions src/components/app/PopulationForm.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { mount } from 'enzyme';
import { PopulationForm } from './PopulationForm';

describe('PopulationForm', () => {

let props;

beforeEach(() => {
props = {
countryList: [
{name: 'england', code: 'gb'},
{name: 'france', code: 'fr'},
],
addPopulation: jest.fn()
}
})

it('should call props.AddPopulation with country and population onSubmit', () => {
const wrapper = mount(<PopulationForm {...props}/>);
const handleSubmitSpy = jest.spyOn(wrapper.instance(), 'handleSubmit');
wrapper.find('select').simulate('change', {target : { value : 'france', getAttribute:() => 'country'}});
wrapper.find('input[type="number"]').simulate('change', {target : { value : 1234, getAttribute:() => 'population'}});
expect(wrapper.state().country).toEqual('france');
expect(wrapper.state().population).toEqual(1234);
expect(wrapper.state().error).toEqual('');

wrapper.find('input[type="submit"]').simulate('submit');
expect(handleSubmitSpy).toHaveBeenCalled();
expect(props.addPopulation).toHaveBeenCalledWith({country: 'france', population: 1234});
});

it('should not call props.AddPopulation if the country or population is missing', () => {
const wrapper = mount(<PopulationForm {...props}/>);
wrapper.find('select').simulate('change', {target : { value : 'France', getAttribute:() => 'country'}});
wrapper.find('input[type="submit"]').simulate('submit');
expect(props.addPopulation).not.toHaveBeenCalled();
});

it('should use the first item in the country list as the default country', () => {
const wrapper = mount(<PopulationForm {...props}/>);
wrapper.find('input[type="number"]').simulate('change', {target : { value : 1234, getAttribute:() => 'population'}});
wrapper.find('input[type="submit"]').simulate('submit');
expect(props.addPopulation).toHaveBeenCalledWith({country: props.countryList[0].name, population: 1234});
});

it('should display an error message if the country or population is missing', () => {
const wrapper = mount(<PopulationForm {...props}/>);
wrapper.find('select').simulate('change', {target : { value : 'France', getAttribute:() => 'country'}});
wrapper.find('input[type="submit"]').simulate('submit');
expect(wrapper.state().error).toEqual('please complete form');
expect(wrapper.find('Error span').text('please complete form'));
});




})
61 changes: 61 additions & 0 deletions src/components/app/PopulationList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import styled from 'styled-components';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {DELETE_POPULATION} from "../../store/actions";

const DeleteButton = styled.button`
padding: 20px;
background-color: red;
color: white;
border-radius: 5px;
`;
DeleteButton.displayName = 'DeleteButton';

const List = styled.ul`
list-style: none;

li {
display: flex;
align-items: center;

span, button{
flex: 1;
}
}
`;


export const PopulationList = ({populationList, deletePopulation}) => {
return (
<List>
{populationList
.sort((a,b) =>(a.population - b.population))
.map(({country, population}) => (
<li key={country}>
<span>Country: {country}<br/> Population: {population}</span>
<DeleteButton onClick={() => deletePopulation(country)}>delete</DeleteButton>
</li>
))}
</List>
)
};

PopulationList.propTypes = {
countryList: PropTypes.array,
deletePopulation: PropTypes.func
}

const mapStateToProps = (state) => ({
populationList: state.populationList
});

const mapDispatchToProps = (dispatch) => ({
deletePopulation: (country) => dispatch({type: DELETE_POPULATION, payload: country}),
});


export default connect(
mapStateToProps,
mapDispatchToProps,
)(PopulationList)
50 changes: 50 additions & 0 deletions src/components/app/PopulationList.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { shallow } from 'enzyme';
import { PopulationList } from './PopulationList';

describe('PopulationList', () => {

let props;

beforeAll(() => {
props = {
populationList: [
{ country: 'France', population: '13000' }
],
deletePopulation: jest.fn()
};
})

it('should display a country and population for each item in the populationList', () => {
const wrapper = shallow(<PopulationList {...props}/>);

expect(wrapper.find('li').at(0).text()).toMatch(/France/);
expect(wrapper.find('li').at(0).text()).toMatch(/13000/);
});

it('should call deletePopulation when delete is clicked', () => {
const wrapper = shallow(<PopulationList {...props}/>)
wrapper.find('DeleteButton').simulate('click');
expect(props.deletePopulation).toHaveBeenCalled();
expect(props.deletePopulation).toHaveBeenCalledWith('France');
});

it('should display the country and population in order of population', () => {
props = {
populationList: [
{ country: 'France', population: '10' },
{ country: 'England', population: '40' },
{ country: 'Germany', population: '20' },
{ country: 'Sweden', population: '30' },
],
deletePopulation: jest.fn()
}
const wrapper = shallow(<PopulationList {...props} />);
expect(wrapper.find('li').at(0).text()).toMatch(/France/);
expect(wrapper.find('li').at(1).text()).toMatch(/Germany/);
expect(wrapper.find('li').at(2).text()).toMatch(/Sweden/);
expect(wrapper.find('li').at(3).text()).toMatch(/England/);
})

});

4 changes: 4 additions & 0 deletions src/components/app/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import Theme from '../theme';
import createStore from '../../store';
import Layout from '../layout';
import H1 from './H1';
import CountryForm from './PopulationForm';
import CountryList from './PopulationList';

// create the redux store
const store = createStore();
Expand All @@ -16,6 +18,8 @@ export default () => (
<H1>
Good luck!
</H1>
<CountryForm/>
<CountryList/>
</Layout>
</Theme>
</Provider>
Expand Down
Loading