Skip to content

Commit 0fce820

Browse files
committed
Implement frontend
1 parent fb0e7df commit 0fce820

10 files changed

Lines changed: 223 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from 'react';
2+
import { Provider } from 'react-redux';
3+
import store from './store';
4+
import Countries from './countries/Countries';
5+
6+
const App = () => (
7+
<Provider store={store}>
8+
<div>
9+
<h1>Countries</h1>
10+
<Countries />
11+
</div>
12+
</Provider>
13+
);
14+
15+
export default App;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import React from 'react';
2+
3+
const Loading = () =>
4+
(<p>Loading...</p>);
5+
6+
export default Loading;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { Component, PropTypes } from 'react';
2+
import { connect } from 'react-redux';
3+
import { fetchCountriesIfNeeded } from './actions';
4+
import CountryListing from './CountryListing';
5+
import Loading from '../components/Loading';
6+
7+
class Countries extends Component {
8+
componentDidMount() {
9+
this.props.dispatch(fetchCountriesIfNeeded());
10+
}
11+
render() {
12+
return this.props.isLoaded ?
13+
<CountryListing /> :
14+
<Loading />;
15+
}
16+
}
17+
18+
Countries.propTypes = {
19+
isLoaded: PropTypes.bool.isRequired
20+
};
21+
22+
const mapStateToProps = state => ({
23+
isLoaded: (state.countries.items != null &&
24+
!state.countries.isFetching)
25+
});
26+
27+
export default connect(mapStateToProps)(Countries);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React, { PropTypes } from 'react';
2+
3+
const CountryList = ({ countries }) => {
4+
const items = countries
5+
.map(country => (<li key={country}>{country}</li>));
6+
return (<ul>{items}</ul>);
7+
};
8+
9+
CountryList.propTypes = {
10+
countries: PropTypes.arrayOf(PropTypes.string)
11+
};
12+
13+
export default CountryList;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React, { PropTypes } from 'react';
2+
import { connect } from 'react-redux';
3+
import { changeFilter } from './actions';
4+
import CountryList from './CountryList';
5+
6+
const CountryListing = ({ items, filter, dispatch }) => {
7+
const handleFilterChange = (e) => {
8+
const newFilter = e.target.value;
9+
dispatch(changeFilter(newFilter));
10+
};
11+
const filtered = items
12+
.filter(item => item.includes(filter));
13+
return (
14+
<div>
15+
<input onChange={handleFilterChange} value={filter} />
16+
<CountryList countries={filtered} />
17+
</div>
18+
);
19+
};
20+
21+
CountryListing.propTypes = {
22+
items: PropTypes.arrayOf(PropTypes.string).isRequired,
23+
filter: PropTypes.string
24+
};
25+
26+
const mapStateToProps = state => state.countries;
27+
28+
export default connect(mapStateToProps)(CountryListing);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export const CHANGE_FILTER = 'CHANGE_FILTER';
2+
export const changeFilter = filter => ({
3+
type: CHANGE_FILTER,
4+
filter
5+
});
6+
7+
export const REQUEST_COUNTRIES = 'REQUEST_COUNTRIES';
8+
export const requestCountries = () => ({
9+
type: REQUEST_COUNTRIES
10+
});
11+
12+
export const RECEIVE_COUNTRIES = 'RECEIVE_COUNTRIES';
13+
export const receiveCountries = response => ({
14+
type: RECEIVE_COUNTRIES,
15+
response
16+
});
17+
18+
export const fetchCountries = () =>
19+
(dispatch) => {
20+
dispatch(requestCountries());
21+
return fetch('./countries.json')
22+
.then(response => response.json())
23+
.then(json => dispatch(receiveCountries(json)));
24+
};
25+
26+
const shouldFetchCountries = (state) => {
27+
const countries = state.countries;
28+
return !countries.items && !countries.isFetching;
29+
};
30+
31+
export const fetchCountriesIfNeeded = () =>
32+
(dispatch, getState) => (
33+
shouldFetchCountries(getState()) ?
34+
dispatch(fetchCountries()) :
35+
null
36+
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
REQUEST_COUNTRIES,
3+
RECEIVE_COUNTRIES,
4+
CHANGE_FILTER
5+
} from './actions';
6+
7+
const initialState = {
8+
items: null,
9+
isFetching: false,
10+
filter: ''
11+
};
12+
13+
const countriesReducer = (state = initialState, action) => {
14+
switch (action.type) {
15+
case CHANGE_FILTER:
16+
return Object.assign({}, state, {
17+
filter: action.filter
18+
});
19+
case RECEIVE_COUNTRIES:
20+
return Object.assign({}, state, {
21+
items: action.response.countries,
22+
isFetching: false
23+
});
24+
case REQUEST_COUNTRIES:
25+
return Object.assign({}, state, {
26+
items: [],
27+
isFetching: true
28+
});
29+
default:
30+
return state;
31+
}
32+
};
33+
34+
export default countriesReducer;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/* eslint-disable no-underscore-dangle */
2+
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
3+
import thunkMiddleware from 'redux-thunk';
4+
import countriesReducer from './countries/reducer';
5+
6+
const initialState = {};
7+
8+
// https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup
9+
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
10+
11+
const middlewares = [
12+
thunkMiddleware
13+
];
14+
15+
const rootReducer = combineReducers({
16+
countries: countriesReducer
17+
});
18+
19+
// http://redux.js.org/docs/advanced/ExampleRedditAPI.html
20+
const store = createStore(
21+
rootReducer,
22+
initialState,
23+
composeEnhancers(applyMiddleware(...middlewares))
24+
);
25+
26+
export default store;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Countries</title>
6+
</head>
7+
<body>
8+
<div id="root"></div>
9+
<script src="//cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.34/system.js"></script>
10+
<script>
11+
System.config({
12+
transpiler: 'babel',
13+
packages: {
14+
'./app': { defaultExtension: 'js' }
15+
},
16+
map: {
17+
'react-dom': '//cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react-dom.min.js',
18+
'react': '//cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react.min.js',
19+
'babel': '//cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.38/browser.min.js',
20+
'redux': '//cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.min.js',
21+
'react-redux': '//cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.1/react-redux.min.js',
22+
'redux-thunk': '//cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.1.0/redux-thunk.min.js'
23+
}
24+
});
25+
System
26+
.import('./index.js')
27+
.catch(err => console.error('Global error:', err));
28+
</script>
29+
</body>
30+
</html>

src/main/resources/static/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import App from './app/App';
4+
5+
ReactDOM.render(
6+
<App />,
7+
document.getElementById('root')
8+
);

0 commit comments

Comments
 (0)