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
3 changes: 2 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
["babel-plugin-styled-components", {
"ssr": true
}],
["@babel/plugin-proposal-class-properties"]
["@babel/plugin-proposal-class-properties"],
["@babel/plugin-transform-runtime"]
]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
9,036 changes: 9,036 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
"prop-types": "^15.6.2",
"react": "^16.5.0",
"react-dom": "^16.5.0",
"react-redux": "^5.0.7",
"redux": "^4.0.0",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"styled-components": "^3.4.6"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^9.0.0",
Expand Down
9 changes: 6 additions & 3 deletions src/api/country.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import countries from '../configs/country';
*
* @returns {Promise<any>}
*/
export default () => new Promise((resolve, reject) => {
setTimeout(() => (Math.round(Math.random()) === 0 ? resolve(countries) : reject()), 100);
});
export default {
countries: () => new Promise((resolve, reject) => {
setTimeout(() => (Math.round(Math.random()) === 0 ? resolve(countries) : reject()), 100);
})
};

5 changes: 4 additions & 1 deletion src/components/app/Button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ const ButtonStyled = styled.button`
padding: ${style('paddingHalf')};
position: relative;
text-decoration: none;
margin-left: ${style('marginHalf')};
margin-left: ${props => props.ml};
margin-right:${props => props.mr};
margin-top: ${props => props.mt};
margin-bottom: ${props => props.mb};
`;

const Button = ({ label, ...props }) => (
Expand Down
34 changes: 34 additions & 0 deletions src/components/app/Select.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

import styled from 'styled-components';
import { style } from '../../configs/theme';
import React from 'react';


const StyledSelect = styled.select`
color: #ffffff;
height: 30px;
width: 100%;
background-color: black;
border-radius: 3px;

cursor: pointer;
display: inline-block;
font-family: ${style('headerFontFamily')};
font-size: ${style('fontSize.normal')};
font-weight: ${style('headerFontWeight')};
line-height: ${style('fontSize.normal')};
position: relative;
text-decoration: none;
margin-left: ${props => props.ml};
margin-right:${props => props.mr};
margin-top: ${props => props.mt};
margin-bottom: ${props => props.mb};
`;

const Select = ({ data, onChange, value}) => {
return <StyledSelect value={value} onChange={onChange}>
{ data ? data.map((value, index) => <option value={index} key={index}>{value.name && value.population ? value.name + "-" + value.population : value.name} </option>) : <React.Fragment /> }
</StyledSelect>;
};

export default Select;
121 changes: 121 additions & 0 deletions src/components/app/countries/CountriesComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch, connect } from 'react-redux';

import countryapi from '../../../api/country';
import { setCountriesList, deleteCountryItem } from '../../../store/actions/index';
import Select from '../Select';
import { FormGroup, Label, Input, ErrorMessage, SuccessMessage } from '../forms/FormGroup';
import Button from '../Button';

const CountriesComponent = () => {
let countries = useSelector(state => state.countries);
const dispatch = useDispatch();
const [message, setMessage] = useState("");
const [showSuccessMessage, setSuccessMessage] = useState(false);
const [selected, setSelected] = useState(0);
const [currentValue, setCurrentValue] = useState({});
const [population, setPopulation] = useState(0);

const rules = [
{ rule: val => Number(val) < 0, message: 'Population should not be a negative number.'},
{ rule: val => isNaN(val), message: 'Population should be a number.'}
];

const validatePopulation = (value) => {
let hasError = false;
let errorMessage = "";

for(let key in rules) {
if(rules[key].rule(value)) {
errorMessage = rules[key].message;
hasError = true;
break;
}
}

hasError ? setMessage(errorMessage) : setMessage("");

return hasError;
};

const getCountries = async () => {
const countries_list = await retry(countryapi.countries);
dispatch(setCountriesList(countries_list));
};

const updateCountries = () => {
dispatch(setCountriesList(countries));
};

const updateSelectedCountry = (e) => {
setSelected(e.target.value);
setCurrentValue({... countries[e.target.value]});
countries[e.target.value].population ? setPopulation(countries[e.target.value].population) : setPopulation(0);
};

const updatePopulation = (e) => {
validatePopulation(e.target.value);
setPopulation(e.target.value);
};

const savePopulation = () => {
const newCountry = { ...countries[selected], population: Number(population) };
countries[selected] = newCountry;
updateCountries(countries);
setSuccessMessage(true);

setSelected(countries.findIndex(item => item.name == currentValue.name));
setTimeout(() => {
setSuccessMessage(false);
}, 2000);
};

const deletePopulation = (e) => {
dispatch(deleteCountryItem(countries, selected));

countries[e.target.value] && countries[e.target.value].population
? setPopulation(countries[e.target.value].population) : setPopulation(0);
};

const retry = (func, max = 5, retries = 0) => {
return new Promise((resolve, reject) => {
func()
.then(data => { resolve(data) })
.catch(e => {
resolve(retry(func, max, retries++));
})

});
};

useEffect(() => {
getCountries();
}, []);

return (
<div>
<FormGroup>
<Label htmlFor="label">Country</Label>
<Select
value={selected}
onChange={updateSelectedCountry}
data={countries}
/>
<br/><br/>
<Label htmlFor="label">Population</Label>
<Input
value={population}
onChange={updatePopulation}
id="label"
/>
{ message.length > 0 ? <ErrorMessage>{ message }</ErrorMessage> : <React.Fragment /> }
{ showSuccessMessage ? <SuccessMessage>Successfully saved!</SuccessMessage> : <React.Fragment /> }

<Button label="save" disabled={message.length > 0} color="dark" onClick={savePopulation}/>
<Button label="delete" color="light" ml="1px" onClick={deletePopulation}/>
</FormGroup>
</div>
);
};

export default connect()(CountriesComponent);
52 changes: 52 additions & 0 deletions src/components/app/forms/FormGroup.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from "react";
import styled from "styled-components";
import { style } from '../../../configs/theme';

export const FormGroup = styled.div`
display: block;
width: auto;
max-width: 400px;
margin: 50px auto;
padding: 20px 20px;
border: 2px solid black;
border-radius: 25px;
`;

export const Label = styled.label`
margin-bottom: 0.5em;
color: black;
display: block;
display: inline-block;

font-family: ${style('headerFontFamily')};
font-size: ${style('fontSize.normal')};
font-weight: ${style('headerFontWeight')};
line-height: ${style('fontSize.normal')};
position: relative;
text-decoration: none;
margin-left: ${props => props.ml};
margin-right:${props => props.mr};
margin-top: ${props => props.mt};
margin-bottom: ${props => props.mb};
`;

export const Input = styled.input`
padding: 0.5em;
color: black;
border: none;
border-radius: 3px;
width: 100%;
margin-bottom: 0.5em;
`;

export const ErrorMessage = styled.label`
margin-bottom: 0.5em;
color: palevioletred;
display: block;
`;

export const SuccessMessage = styled.label`
margin-bottom: 0.5em;
color: #109210;
display: block;
`;
7 changes: 4 additions & 3 deletions src/components/app/index.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

import React from 'react';
import React, { useState, useEffect } from 'react';
import { Provider } from 'react-redux';
import Theme from '../theme';
import createStore from '../../store';
import Layout from '../layout';
import H1 from './H1';

import CountriesComponent from './countries/CountriesComponent';
// create the redux store
const store = createStore();

Expand All @@ -14,8 +14,9 @@ export default () => (
<Theme>
<Layout>
<H1>
Good luck!
React Developer Test By Cleon
</H1>
<CountriesComponent />
</Layout>
</Theme>
</Provider>
Expand Down
4 changes: 4 additions & 0 deletions src/configs/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export default {
tertiaryAlt: '#FFFFFF',
quaternary: '#F46357',
quaternaryAlt: '#FFFFFF',
dark: 'black',
darkAlt: '#FFFFFF',
light: '#FFFFFF',
lightAlt: 'black'
},

fontSize: {
Expand Down
15 changes: 15 additions & 0 deletions src/store/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const setCountriesList = (countries) => {
return {
type: 'UPDATE_COUNTRIES_LIST',
payload: countries
};
};

export const deleteCountryItem = (countries, index) => {
return {
type: 'DELETE_COUNTRY_ITEM',
payload: countries,
index: index
};
};

32 changes: 28 additions & 4 deletions src/store/reducer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@

export const getInitialState = () => ({
// initial state...
countries: []
});

export default (state, action) => ({
...state,
const genericSort = (sortByField) => {
return (a, b) => {
const objA = a && a[sortByField] ? a[sortByField] : 0;
const objB = b && b[sortByField] ? b[sortByField] : 0;

if (objA < objB) {
return 1;
}
if (objA > objB) {
return -1;
}

return 0;
};
};

export default (state, action) => {
// reducers...
});
switch (action.type) {
case 'UPDATE_COUNTRIES_LIST':
action.payload.sort(genericSort('population'));
return { ...state, countries: action.payload };
case 'DELETE_COUNTRY_ITEM':
action.payload = action.payload.filter((item, index) => index !== action.index);
return { ...state, countries: action.payload };
default:
return state;
}
};