diff --git a/package.json b/package.json index e2438b1d..df456eb1 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,15 @@ "start": "webpack-dev-server --mode development --open --history-api-fallback" }, "dependencies": { + "bootstrap": "^4.6.0", + "promise-retry": "^2.0.1", "prop-types": "^15.6.2", "react": "^16.5.0", + "react-bootstrap": "^1.4.3", "react-dom": "^16.5.0", "react-redux": "^5.0.7", "redux": "^4.0.0", + "shortid": "^2.2.16", "styled-components": "^3.4.6" }, "devDependencies": { @@ -35,6 +39,7 @@ "eslint-plugin-react": "^7.11.1", "html-webpack-plugin": "^3.2.0", "react-hot-loader": "^4.3.6", + "redux-devtools": "^3.7.0", "webpack": "^4.18.0", "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.8" diff --git a/src/components/app/Countries.jsx b/src/components/app/Countries.jsx new file mode 100644 index 00000000..ecbc45d3 --- /dev/null +++ b/src/components/app/Countries.jsx @@ -0,0 +1,243 @@ +import React, { Component } from "react"; +import getCountry from "../../api/country"; +import { Button, Form, Spinner, Jumbotron } from "react-bootstrap"; +import shortid from "shortid"; +import promiseRetry from "promise-retry"; +import CsTable from "./Table"; +import styled from "styled-components"; +import { + addCountry, + selectCountry, + updateCountry, + deleteCountry, +} from "../../store/actions"; +import { connect } from "react-redux"; + +class Countries extends Component { + constructor(props) { + super(props); + + this.state = { + headerData: ["", "Country", "Population"], + population: "", + countryDdData: [], + country: "AFGHANISTAN", + sort: "ascending", + data: [], + selected: null, + isLoading: false, + }; + } + + componentDidMount() { + promiseRetry((retry, number) => { + if (number !== 1) { + this.setState({ + isLoading: true, + }); + } + + return getCountry() + .then((data) => { + this.setState({ + countryDdData: data, + isLoading: false, + }); + }) + .catch(retry); + }); + } + + save = (event) => { + event.preventDefault(); + const { population, country, data } = this.state; + const newData = { + population: population, + country: country, + id: data.length + 1, + }; + + this.props.addCountry(newData); + }; + + changeHandler = (e) => { + this.setState({ + [e.target.name]: e.target.value, + }); + }; + + delete = (e) => { + this.props.deleteCountry(parseInt(e)); + }; + + select = (e) => { + this.props.selectCountry(parseInt(e)); + }; + + componentWillReceiveProps(nextProps) { + if (nextProps.countries.selected !== this.state.selected) { + const { population, country } = nextProps.countries.selected; + const newState = { + ...this.state, + population, + country, + selected: nextProps.countries.selected, + }; + this.setState(newState); + } + + if (nextProps.countries.data !== this.state.data) { + const { data } = nextProps.countries; + const newState = { + ...this.state, + data, + selected: null, + }; + this.setState(newState); + } + } + + update = () => { + const { population, country, selected } = this.state; + this.props.updateCountry({ + data: { population, country }, + id: selected.id, + }); + }; + + cancel = () => { + this.setState({ + selected: null, + country: "", + population: "", + }); + }; + + sortByPopulation = () => { + const { data, sort } = this.state; + let sortData, sortVal; + + if (sort === "ascending") { + sortData = data.sort((a, b) => { + return a.population - b.population; + }); + sortVal = "descending"; + } else { + sortData = data.sort((a, b) => { + return b.population - a.population; + }); + sortVal = "ascending"; + } + + this.setState({ + data: sortData, + sort: sortVal, + }); + }; + + render() { + const { countryDdData, isLoading, sort } = this.state; + + const ButtonGroup = styled.div` + padding: 10px 15px; + `; + const Margin = styled.span` + margin: 10px 15px; + `; + + return ( + + +
+
+
+
+ + Select Country + + {countryDdData.length + ? countryDdData.map((c) => ( + + )) + : null} + + {isLoading ? ( + + Fetching Data... + + Loading... + + + ) : null} + +
+
+ + Population + + +
+
+ {!this.state.selected ? ( + + + + + ) : null} + {this.state.selected ? ( + + + + ) : null} +
+
+
+ +

List of Countries

+ + + +
+
+
+
+ ); + } +} + +export default connect( + (state) => ({ + countries: state.countries, + }), + { + addCountry, + selectCountry, + updateCountry, + deleteCountry, + } +)(Countries); diff --git a/src/components/app/Table.jsx b/src/components/app/Table.jsx new file mode 100644 index 00000000..e4174fd9 --- /dev/null +++ b/src/components/app/Table.jsx @@ -0,0 +1,81 @@ +import React, { Component } from "react"; +import { Button, Table } from "react-bootstrap"; +import shortid from "shortid"; +import { connect } from "react-redux"; + +class CsTable extends Component { + constructor(props) { + super(props); + } + + selectEvent = (e) => { + this.props.selectEmitted(e.target.id); + }; + + cancelEvent = (e) => { + this.props.cancelEmitted(e.target.id); + }; + + deleteEvent = (e) => { + this.props.deleteEmitted(e.target.id); + }; + + tableBody() { + const { data, selected } = this.props.data; + return data && data.length + ? data.map((d) => ( + + + {!selected ? ( + + ) : null} + {selected && selected.id === d.id ? ( + + ) : null} + {!selected ? ( + + ) : null} + + {d.country} + {d.population} + + )) + : null; + } + + tableHeader() { + const { headerData } = this.props.data; + return headerData.length + ? headerData.map((d) => {d}) + : null; + } + + renderTable() { + return ( + + + + {this.tableHeader()} + + {this.tableBody()} +
+
+ ); + } + + render() { + return this.renderTable(); + } +} + +export default connect(null, {})(CsTable); diff --git a/src/components/app/index.jsx b/src/components/app/index.jsx index 34998d72..27cdf8e6 100644 --- a/src/components/app/index.jsx +++ b/src/components/app/index.jsx @@ -1,22 +1,13 @@ - -import React from 'react'; -import { Provider } from 'react-redux'; -import Theme from '../theme'; -import createStore from '../../store'; -import Layout from '../layout'; -import H1 from './H1'; - +import React from "react"; +import { Provider } from "react-redux"; +import createStore from "../../store"; +import Countries from "./Countries"; +import rootReducer from "../../store/reducers"; // create the redux store -const store = createStore(); +const store = createStore(rootReducer); export default () => ( - - -

- Good luck! -

-
-
+
); diff --git a/src/index.html b/src/index.html index d0fed5f3..59cba4d1 100644 --- a/src/index.html +++ b/src/index.html @@ -1,10 +1,15 @@ + Country Populations + +
- + + \ No newline at end of file diff --git a/src/index.jsx b/src/index.jsx index f4781f92..29ce84f9 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,6 +1,5 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./components/app"; -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './components/app'; - -ReactDOM.render(, document.getElementById('app')); +ReactDOM.render(, document.getElementById("app")); diff --git a/src/store/actionTypes.js b/src/store/actionTypes.js new file mode 100644 index 00000000..d3b13134 --- /dev/null +++ b/src/store/actionTypes.js @@ -0,0 +1,4 @@ +export const ADD_COUNTRY = 'ADD_COUNTRY' +export const SELECT_COUNTRY = 'SELECT_COUNTRY' +export const UPDATE_COUNTRY = 'UPDATE_COUNTRY' +export const DELETE_COUNTRY = 'DELETE_COUNTRY' \ No newline at end of file diff --git a/src/store/actions.js b/src/store/actions.js new file mode 100644 index 00000000..755fd468 --- /dev/null +++ b/src/store/actions.js @@ -0,0 +1,30 @@ +import {ADD_COUNTRY, DELETE_COUNTRY, SELECT_COUNTRY, UPDATE_COUNTRY} from './actionTypes' + +let nextToId = 0 +export const addCountry = payload => ({ + type: ADD_COUNTRY, + payload: { + id: ++nextToId, + payload + } +}) + +export const updateCountry = payload => ({ + type: UPDATE_COUNTRY, + payload: { + data: payload.data, + id: payload.id + } +}) + +export const deleteCountry = payload => ({ + type: DELETE_COUNTRY, + payload: payload +}) + +export const selectCountry = id => ({ + type: SELECT_COUNTRY, + payload: { + id + } +}) \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js index 745dc59b..5e3ff5a6 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,5 +1,5 @@ import { createStore } from 'redux'; -import reducer, { getInitialState } from './reducer'; +import reducer from './reducers'; -export default () => createStore(reducer, getInitialState()); +export default () => createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); diff --git a/src/store/reducer.js b/src/store/reducer.js deleted file mode 100644 index ddf7fb22..00000000 --- a/src/store/reducer.js +++ /dev/null @@ -1,10 +0,0 @@ - -export const getInitialState = () => ({ - // initial state... -}); - -export default (state, action) => ({ - ...state, - - // reducers... -}); diff --git a/src/store/reducers/country.js b/src/store/reducers/country.js new file mode 100644 index 00000000..7d65af50 --- /dev/null +++ b/src/store/reducers/country.js @@ -0,0 +1,52 @@ + +import {ADD_COUNTRY, DELETE_COUNTRY, UPDATE_COUNTRY, SELECT_COUNTRY} from '../actionTypes' + +const initialState = { + population: "", + countryDdData: [], + data: [], + selected: null, + isLoading: false, +} + +export default (state = initialState, action) => { + switch (action.type) { + case ADD_COUNTRY: { + const {payload} = action.payload + return { + ...state, + data: [...state.data, payload] + } + } + case UPDATE_COUNTRY: { + const {data, id} = action.payload + let copy = [...state.data] + const findRecord = copy.findIndex(d => d.id === id) + copy[findRecord] = data + return { + ...state, + data: copy + } + } + case DELETE_COUNTRY: { + const id = action.payload + let copy = [...state.data] + const findRecord = copy.findIndex(d => d.id === id) + copy.splice(findRecord, 1) + return { + ...state, + data: copy + } + } + case SELECT_COUNTRY: { + const {id} = action.payload + return { + ...state, + selected: {...state.data.find((d) => d.id === id)} + } + } + default: { + return state; + } + } +} \ No newline at end of file diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js new file mode 100644 index 00000000..ef5f5128 --- /dev/null +++ b/src/store/reducers/index.js @@ -0,0 +1,4 @@ +import { combineReducers } from "redux"; +import countries from './country' + +export default combineReducers({countries}) \ No newline at end of file