diff --git a/package-lock.json b/package-lock.json
index f781539..d1720c3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6404,6 +6404,14 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
+ "hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw==",
+ "requires": {
+ "react-is": "^16.7.0"
+ }
+ },
"hosted-git-info": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz",
@@ -9605,6 +9613,16 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
+ "pigeon-maps": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/pigeon-maps/-/pigeon-maps-0.14.0.tgz",
+ "integrity": "sha512-ee7yceJpRH3Jpb5iooYn0oqykNs18P3zjO7zKvq+9nf109QoiYx9b6Tniy8LxxpG83uDez0+DrGJOOHiW7gXnw=="
+ },
+ "pigeon-marker": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/pigeon-marker/-/pigeon-marker-0.3.4.tgz",
+ "integrity": "sha512-6sQoSI8Mii7eWWz3TorssbNmptVoP4heKMDt27AeU8I//MFQfQZS6JfCmW3zXF4a6dk4uisr4m26akUYSblfnQ=="
+ },
"pinkie": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
@@ -11120,6 +11138,19 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
},
+ "react-redux": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.3.tgz",
+ "integrity": "sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w==",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "hoist-non-react-statics": "^3.3.0",
+ "invariant": "^2.2.4",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.9.0"
+ }
+ },
"react-scripts": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.3.0.tgz",
@@ -11268,6 +11299,20 @@
"strip-indent": "^3.0.0"
}
},
+ "redux": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
+ "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "symbol-observable": "^1.2.0"
+ }
+ },
+ "redux-thunk": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
+ "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
+ },
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@@ -12658,6 +12703,11 @@
"util.promisify": "~1.0.0"
}
},
+ "symbol-observable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
+ },
"symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
diff --git a/package.json b/package.json
index 14a41eb..2d0a59f 100644
--- a/package.json
+++ b/package.json
@@ -9,10 +9,15 @@
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
+ "pigeon-maps": "^0.14.0",
+ "pigeon-marker": "^0.3.4",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
- "react-scripts": "3.3.0"
+ "react-redux": "^7.1.3",
+ "react-scripts": "3.3.0",
+ "redux": "^4.0.5",
+ "redux-thunk": "^2.3.0"
},
"scripts": {
"start": "react-scripts start",
diff --git a/src/App.css b/src/App.css
index 74b5e05..7d268be 100644
--- a/src/App.css
+++ b/src/App.css
@@ -2,37 +2,8 @@
text-align: center;
}
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
+.map {
+ border: 1px solid black;
+ margin: 0 auto;
+ max-width: 700px;
}
diff --git a/src/App.js b/src/App.js
index ce9cbd2..1447b5e 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,24 +1,12 @@
import React from 'react';
-import logo from './logo.svg';
import './App.css';
+import ISSLocation from './components/ISSLocation';
function App() {
return (
-
+
International Space Station Location Tracker
+
);
}
diff --git a/src/App.test.js b/src/App.test.js
deleted file mode 100644
index 4db7ebc..0000000
--- a/src/App.test.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-import { render } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- const { getByText } = render();
- const linkElement = getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/actions/index.js b/src/actions/index.js
new file mode 100644
index 0000000..5ebcd38
--- /dev/null
+++ b/src/actions/index.js
@@ -0,0 +1,32 @@
+import { getCurrentISSLocation } from '../services/issAPI';
+
+export const RECEIVE_ISS_LOCATION_FAILURE = 'RECEIVE_ISS_LOCATION_FAILURE';
+export const RECEIVE_ISS_LOCATION_SUCCESS = 'RECEIVE_ISS_LOCATION_SUCCESS';
+export const REQUEST_ISS_LOCATION = 'REQUEST_ISS_LOCATION';
+
+const requestISSLocation = () => ({
+ type: REQUEST_ISS_LOCATION,
+});
+
+const receiveISSLocationFailure = (error) => ({
+ type: RECEIVE_ISS_LOCATION_FAILURE,
+ error,
+});
+
+const receiveISSLocationSuccess = ({ iss_position: { latitude, longitude } }) => ({
+ type: RECEIVE_ISS_LOCATION_SUCCESS,
+ latitude: parseFloat(latitude),
+ longitude: parseFloat(longitude),
+});
+
+export function fetchISSLocation() {
+ return (dispatch) => {
+ dispatch(requestISSLocation());
+
+ return getCurrentISSLocation()
+ .then(
+ (location) => dispatch(receiveISSLocationSuccess(location)),
+ (error) => dispatch(receiveISSLocationFailure(error.message)),
+ );
+ };
+}
diff --git a/src/components/ISSLocation.js b/src/components/ISSLocation.js
new file mode 100644
index 0000000..2bcf07c
--- /dev/null
+++ b/src/components/ISSLocation.js
@@ -0,0 +1,88 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import Map from 'pigeon-maps';
+import Marker from 'pigeon-marker';
+
+import { fetchISSLocation } from '../actions';
+
+class ISSLocation extends Component {
+ componentDidMount() {
+ const { getCurrentISSLocation } = this.props;
+
+ this.timer = setInterval(
+ getCurrentISSLocation,
+ 2000,
+ );
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.timer);
+ }
+
+ render() {
+ const {
+ error,
+ isFetching,
+ latitude,
+ longitude,
+ } = this.props;
+ const isLocationPresent = latitude && longitude;
+
+ return (
+
+
+
+
+ {isFetching && 'Loading...'}
+ {!isFetching && isLocationPresent && `Current ISS location: latitude = ${latitude}, longitude = ${longitude}`}
+ {!isFetching && error}
+
+ );
+ }
+}
+
+const mapStateToProps = ({
+ issLocation: {
+ error,
+ isFetching,
+ latitude,
+ longitude,
+ },
+}) => (
+ {
+ error,
+ isFetching,
+ latitude,
+ longitude,
+ }
+);
+
+const mapDispatchToProps = (dispatch) => ({
+ getCurrentISSLocation: () => dispatch(fetchISSLocation()),
+});
+
+ISSLocation.propTypes = {
+ error: PropTypes.string,
+ getCurrentISSLocation: PropTypes.func.isRequired,
+ isFetching: PropTypes.bool.isRequired,
+ latitude: PropTypes.number,
+ longitude: PropTypes.number,
+};
+
+ISSLocation.defaultProps = {
+ error: null,
+ latitude: null,
+ longitude: null,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ISSLocation);
diff --git a/src/index.js b/src/index.js
index 87d1be5..02d2bed 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,12 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
+
import './index.css';
import App from './App';
-import * as serviceWorker from './serviceWorker';
-
-ReactDOM.render(, document.getElementById('root'));
+import store from './store';
-// If you want your app to work offline and load faster, you can change
-// unregister() to register() below. Note this comes with some pitfalls.
-// Learn more about service workers: https://bit.ly/CRA-PWA
-serviceWorker.unregister();
+ReactDOM.render(
+
+
+ , document.getElementById('root'),
+);
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 6b60c10..0000000
--- a/src/logo.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/src/reducers/index.js b/src/reducers/index.js
new file mode 100644
index 0000000..890d5bf
--- /dev/null
+++ b/src/reducers/index.js
@@ -0,0 +1,8 @@
+import { combineReducers } from 'redux';
+import issLocation from './issLocation';
+
+const rootReducer = combineReducers({
+ issLocation,
+});
+
+export default rootReducer;
diff --git a/src/reducers/issLocation.js b/src/reducers/issLocation.js
new file mode 100644
index 0000000..6b0fc61
--- /dev/null
+++ b/src/reducers/issLocation.js
@@ -0,0 +1,38 @@
+import {
+ RECEIVE_ISS_LOCATION_FAILURE,
+ RECEIVE_ISS_LOCATION_SUCCESS,
+ REQUEST_ISS_LOCATION,
+} from '../actions';
+
+const INITIAL_ISS_LOCATION_STATE = {
+ isFetching: false,
+};
+
+const issLocation = (state = INITIAL_ISS_LOCATION_STATE, action) => {
+ console.log('received action:', action);
+
+ switch (action.type) {
+ case REQUEST_ISS_LOCATION:
+ return {
+ ...state,
+ isFetching: true,
+ };
+ case RECEIVE_ISS_LOCATION_SUCCESS:
+ return {
+ ...state,
+ latitude: action.latitude,
+ longitude: action.longitude,
+ isFetching: false,
+ };
+ case RECEIVE_ISS_LOCATION_FAILURE:
+ return {
+ ...state,
+ error: action.error,
+ isFetching: false,
+ };
+ default:
+ return state;
+ }
+};
+
+export default issLocation;
diff --git a/src/serviceWorker.js b/src/serviceWorker.js
deleted file mode 100644
index 8703ddb..0000000
--- a/src/serviceWorker.js
+++ /dev/null
@@ -1,137 +0,0 @@
-// This optional code is used to register a service worker.
-// register() is not called by default.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on subsequent visits to a page, after all the
-// existing tabs open on the page have been closed, since previously cached
-// resources are updated in the background.
-
-// To learn more about the benefits of this model and instructions on how to
-// opt-in, read https://bit.ly/CRA-PWA
-
-const isLocalhost = Boolean(
- window.location.hostname === 'localhost' ||
- // [::1] is the IPv6 localhost address.
- window.location.hostname === '[::1]' ||
- // 127.0.0.0/8 are considered localhost for IPv4.
- window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
- )
-);
-
-export function register(config) {
- if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if PUBLIC_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
- }
-
- window.addEventListener('load', () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
- if (isLocalhost) {
- // This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
-
- // Add some additional logging to localhost, pointing developers to the
- // service worker/PWA documentation.
- navigator.serviceWorker.ready.then(() => {
- console.log(
- 'This web app is being served cache-first by a service ' +
- 'worker. To learn more, visit https://bit.ly/CRA-PWA'
- );
- });
- } else {
- // Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
- }
- });
- }
-}
-
-function registerValidSW(swUrl, config) {
- navigator.serviceWorker
- .register(swUrl)
- .then(registration => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- if (installingWorker == null) {
- return;
- }
- installingWorker.onstatechange = () => {
- if (installingWorker.state === 'installed') {
- if (navigator.serviceWorker.controller) {
- // At this point, the updated precached content has been fetched,
- // but the previous service worker will still serve the older
- // content until all client tabs are closed.
- console.log(
- 'New content is available and will be used when all ' +
- 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
- );
-
- // Execute callback
- if (config && config.onUpdate) {
- config.onUpdate(registration);
- }
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log('Content is cached for offline use.');
-
- // Execute callback
- if (config && config.onSuccess) {
- config.onSuccess(registration);
- }
- }
- }
- };
- };
- })
- .catch(error => {
- console.error('Error during service worker registration:', error);
- });
-}
-
-function checkValidServiceWorker(swUrl, config) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl, {
- headers: { 'Service-Worker': 'script' }
- })
- .then(response => {
- // Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get('content-type');
- if (
- response.status === 404 ||
- (contentType != null && contentType.indexOf('javascript') === -1)
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
- }
- })
- .catch(() => {
- console.log(
- 'No internet connection found. App is running in offline mode.'
- );
- });
-}
-
-export function unregister() {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister();
- });
- }
-}
diff --git a/src/services/issAPI.js b/src/services/issAPI.js
new file mode 100644
index 0000000..bfb7015
--- /dev/null
+++ b/src/services/issAPI.js
@@ -0,0 +1,12 @@
+const ISS_BASE_API = 'http://api.open-notify.org';
+
+export const getCurrentISSLocation = () => (
+ fetch(`${ISS_BASE_API}/iss-now.json`)
+ .then((response) => (
+ response
+ .json()
+ .then((json) => (response.ok ? Promise.resolve(json) : Promise.reject(json)))
+ ))
+);
+
+export default getCurrentISSLocation;
diff --git a/src/setupTests.js b/src/setupTests.js
deleted file mode 100644
index 74b1a27..0000000
--- a/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom/extend-expect';
diff --git a/src/store/index.js b/src/store/index.js
new file mode 100644
index 0000000..fa4e65b
--- /dev/null
+++ b/src/store/index.js
@@ -0,0 +1,8 @@
+import { applyMiddleware, createStore } from 'redux';
+import thunk from 'redux-thunk';
+
+import reducer from '../reducers';
+
+const store = createStore(reducer, applyMiddleware(thunk));
+
+export default store;