diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..33812f55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/src/config.js diff --git a/README.md b/README.md index dc85464b..e22fc7af 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # assignment_exchange_rates How much does a Big Mac cost in Italy? + +Alex Thomas diff --git a/package.json b/package.json new file mode 100644 index 00000000..70613fc3 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "assignment_exchange_rates", + "version": "0.1.0", + "private": true, + "dependencies": { + "bootstrap": "^4.0.0-alpha.6", + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-scripts": "1.0.14" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..a11777cc Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..9a67001c --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + Currency Converter + + + + +
+ + + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 00000000..be607e41 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "192x192", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 00000000..b84af98d --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); +}); diff --git a/src/components/App.js b/src/components/App.js new file mode 100644 index 00000000..9894f50f --- /dev/null +++ b/src/components/App.js @@ -0,0 +1,85 @@ +import React, { Component } from "react"; +import CurrencyConverterContainer from "./CurrencyConverterContainer"; +import CurrencyRatesTableContainer from "./CurrencyRatesTableContainer"; +import JumbotronFluid from "./elements/JumbotronFluid"; +import config from '../config' +class App extends Component { + constructor() { + super(), + (this.state = { + isFetching: false, + baseCurrency: "USD", + rates: [], + date: new Date().toISOString().slice(0, 10) + }); + } + + + getRates = () => { + fetch( + `http://api.fixer.io/${this.state.date}?base=${this.state.baseCurrency}?{config.key}` + ) + .then(response => response.json()) + .then(json => { + this.setState({ + rates: json.rates, + isFetching: false + }); + }); + }; + componentDidMount() { + // Before performing the fetch, set isFetching to true + this.setState({ isFetching: true }, this.getRates()); + } + shouldComponentUpdate() { + return this.state.isFetching !== false; + } + + switch_currency = e => { + e.preventDefault(); + //console.log("select-target", e.target); + //console.log("select-target.value", e.target.value); + this.setState( + { + baseCurrency: e.target.value + }, + this.setState({ isFetching: true }, this.getRates()) + ); + }; + + setDate = e => { + //console.log("date-target", e.target); + //console.log("date-target.value", e.target.value); + this.setState( + { + date: e.target.value, + isFetching: true + }, + this.getRates() + ); + }; + render() { + const { baseCurrency, rates, date } = this.state; + const currenciesArray = Object.keys(rates); + //console.log("RATES Passed", rates); + console.log("RATES Passed", config.key); + return ( +
+ + +
+ +
+ ); + } +} + +export default App; diff --git a/src/components/BaseCurrencyForm.js b/src/components/BaseCurrencyForm.js new file mode 100644 index 00000000..3239e60e --- /dev/null +++ b/src/components/BaseCurrencyForm.js @@ -0,0 +1,42 @@ +import React from "react"; +import Select from "./elements/Select"; +import Button from "./elements/Button"; + +const BaseCurrencyForm = props => { + const { + rates, + baseCurrency, + switch_currency, + setDate, + date, + currenciesArray + } = props; + return ( +
+ +
+
+ ); +}; + +BaseCurrencyForm.defaultProps = { + type: "button", + color: "default", + children: "Submit" +}; + +export default BaseCurrencyForm; diff --git a/src/components/ConverterBaseCurrencySelector.js b/src/components/ConverterBaseCurrencySelector.js new file mode 100644 index 00000000..85fa4058 --- /dev/null +++ b/src/components/ConverterBaseCurrencySelector.js @@ -0,0 +1,28 @@ +import React from "react"; +import Select from "./elements/Select"; +import PropTypes from "prop-types"; + +const ConverterBaseCurrencySelector = ({ + selectCurrency, + converterBaseCurrency, + currenciesArray +}) => { + //currenciesArray.unshift(converterBaseCurrency); + return ( + + ); +}; + +ConverterConvertedCurrencySelector.propTypes = { + convertedCurrency: PropTypes.string.isRequired, + selectCurrency: PropTypes.func.isRequired, + currenciesArray: PropTypes.array.isRequired +}; + +export default ConverterConvertedCurrencySelector; diff --git a/src/components/CurrencyConverter.js b/src/components/CurrencyConverter.js new file mode 100644 index 00000000..fd8c1d4a --- /dev/null +++ b/src/components/CurrencyConverter.js @@ -0,0 +1,8 @@ +import React from "react"; +import Converter from "./elements/Converter"; + +const CurrencyConverter = () => { + return
; +}; + +export default CurrencyConverter; diff --git a/src/components/CurrencyConverterContainer.js b/src/components/CurrencyConverterContainer.js new file mode 100644 index 00000000..ca5b8503 --- /dev/null +++ b/src/components/CurrencyConverterContainer.js @@ -0,0 +1,125 @@ +import React, { Component } from "react"; +import Input from "./elements/Input"; +import CurrencyConverterForm from "./CurrencyConverterForm"; +import CurrencyConverterOutput from "./CurrencyConverterOutput"; +import PropTypes from "prop-types"; + +class CurrencyConverterContainer extends Component { + constructor(props) { + super(props); + this.state = { + converterBaseCurrency: "USD", + convertedCurrency: "EUR", + converterBaseValue: 1, + currenciesArray: [], + convertingRate: "", + convertingOutcome: 0 + }; + } + + //gets array of currencies for currency selectors + //will take the base currency + getCurrencies = base => { + this.setState({ + isFetching: true + }); + fetch(`http://api.fixer.io/latest?base=${base}`) + .then(response => { + return response.json(); + }) + .then(json => { + console.log("Fetched selector currencies"); + this.setState({ + isFetching: false, + currenciesArray: Object.keys(json.rates) + }); + }); + }; + // component did mount with api call to populate selectors + componentDidMount = () => { + this.getCurrencies(this.state.converterBaseCurrency); + }; + //another api call to get rates for selected currencies + //will take base cuurency and the converted currency + + handleClick = e => { + e.preventDefault(); + this.setState({ + isFetching: true + }); + fetch( + `http://api.fixer.io/latest?base=${this.state + .converterBaseCurrency}&symbols=${this.state.convertedCurrency}` + ) + .then(response => response.json()) + .then(json => { + let rate = Object.values(json.rates)[0]; + this.setState( + { + convertingRate: rate + }, + () => { + let outcome = + this.state.converterBaseValue * this.state.convertingRate; + let length = this.state.convertedBaseValue + 3; + outcome = outcome.toFixed(3); + this.setState({ + convertingOutcome: outcome + }); + } + ); + }); + }; + baseCurrencyInput = e => { + let input = e.target.value; + this.setState({ converterBaseValue: input }); + }; + selectBaseCurrency = e => { + this.setState({ converterBaseCurrency: e.target.value }); + this.getCurrencies(e.target.value); + }; + selectConvertedCurrency = e => { + this.setState({ convertedCurrency: e.target.value }); + this.getCurrencies(e.target.value); + }; + render() { + const { + converterBaseCurrency, + convertedCurrency, + converterBaseValue, + currenciesArray, + convertingRate, + convertingOutcome + } = this.state; + console.log("rendered"); + return ( +
+

Currency Calculator

+
+ +
+

Rate:

+

{convertingRate}

+
+ + +
+ ); + } +} + +CurrencyConverterContainer.propTypes = { + converterBaseCurrency: PropTypes.string.isRequired, + convertedCurrency: PropTypes.string.isRequired, + converterBaseValue: PropTypes.number, + currenciesArray: PropTypes.array.isRequired, + convertingRate: PropTypes.number, + convertingOutcome: PropTypes.number +}; +export default CurrencyConverterContainer; diff --git a/src/components/CurrencyConverterForm.js b/src/components/CurrencyConverterForm.js new file mode 100644 index 00000000..f1bd0a70 --- /dev/null +++ b/src/components/CurrencyConverterForm.js @@ -0,0 +1,57 @@ +import React from "react"; +import Input from "./elements/Input"; +import ConverterBaseCurrencySelector from "./ConverterBaseCurrencySelector"; +import ConverterConvertedCurrencySelector from "./ConverterConvertedCurrencySelector"; +import PropTypes from "prop-types"; + +const CurrencyConverterForm = ({ + converterBaseCurrency, + selectBaseCurrency, + currenciesArray, //for populating selectors + convertedCurrency, //second selector + selectConvertedCurrency, //function upon selection + converterBaseValue, //value of user input + baseCurrencyInput, //function changes converterBaseValue upon user input + handleClick //function for submit btn +}) => { + return ( +
+ + +

to

+ + +
+ ); +}; + +CurrencyConverterForm.propTypes = { + converterBaseCurrency: PropTypes.string.isRequired, + selectBaseCurrency: PropTypes.string.isRequired, + currenciesArray: PropTypes.array.isRequired, + convertedCurrency: PropTypes.string.isRequired, + selectConvertedCurrency: PropTypes.func.isRequired, + converterBaseValue: PropTypes.number, + baseCurrencyInput: PropTypes.func.isRequired, + handleClick: PropTypes.func.isRequired +}; + +export default CurrencyConverterForm; diff --git a/src/components/CurrencyConverterOutput.jsx b/src/components/CurrencyConverterOutput.jsx new file mode 100644 index 00000000..f69174e0 --- /dev/null +++ b/src/components/CurrencyConverterOutput.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const CurrencyConverterOutput = ({ + converterBaseValue, + converterBaseCurrency, + convertingOutcome, + convertedCurrency +}) => { + return ( +
+

{converterBaseValue}

+

{converterBaseCurrency}

+

=

+

{convertingOutcome}

+

{convertedCurrency}

+
+ ); +}; + +CurrencyConverterOutput.propTypes = { + converterBaseValue: PropTypes.number, + converterBaseCurrency: PropTypes.string.isRequired, + convertingOutcome: PropTypes.number, + convertedCurrency: PropTypes.string.isRequired, + customConverterBaseValue: function( + props, + converterBaseValue, + CurrencyConverterOutput + ) { + if (typeof converterBaseValue !== "number") { + return new Error("Value must be a number"); + } + } +}; + +export default CurrencyConverterOutput; diff --git a/src/components/CurrencyRatesTable.js b/src/components/CurrencyRatesTable.js new file mode 100644 index 00000000..bad4a0f7 --- /dev/null +++ b/src/components/CurrencyRatesTable.js @@ -0,0 +1,12 @@ +import React from "react"; +import Table from "./elements/Table"; + +const CurrencyRatesTable = props => { + return ( +
+ + + ); +}; + +export default CurrencyRatesTable; diff --git a/src/components/CurrencyRatesTableContainer.js b/src/components/CurrencyRatesTableContainer.js new file mode 100644 index 00000000..541ee7ba --- /dev/null +++ b/src/components/CurrencyRatesTableContainer.js @@ -0,0 +1,15 @@ +import React from "react"; +import CurrencyRatesTable from "./CurrencyRatesTable"; +import BaseCurrencyForm from "./BaseCurrencyForm"; + +const CurrencyRatesTableContainer = props => { + //console.log("Table props", props); + return ( +
+ + +
+ ); +}; + +export default CurrencyRatesTableContainer; diff --git a/src/components/elements/Alert.js b/src/components/elements/Alert.js new file mode 100644 index 00000000..e6cdf49a --- /dev/null +++ b/src/components/elements/Alert.js @@ -0,0 +1,13 @@ +import React from 'react' + +const Alert = ({type, children}) => ( +
+ {children} +
+) + +Alert.defaultProps = { + type: 'success' +} + +export default Alert diff --git a/src/components/elements/Button.js b/src/components/elements/Button.js new file mode 100644 index 00000000..cbcbac0d --- /dev/null +++ b/src/components/elements/Button.js @@ -0,0 +1,23 @@ +import React from 'react' + +const Button = (props) => { + const {size, color, children, type} = props + const sizeClass = size ? `btn-${size}` : '' + + return ( + + ) +} + +Button.defaultProps = { + type: 'button', + color: 'default', + children: 'Submit', +} + +export default Button diff --git a/src/components/elements/Converter.js b/src/components/elements/Converter.js new file mode 100644 index 00000000..43a8bbf6 --- /dev/null +++ b/src/components/elements/Converter.js @@ -0,0 +1,10 @@ +const Converter = (baseValue, rate) => { + let exchangeValue = baseValue * rate; + let reverseExchangeValue = baseValue / rate; + return { + exchangeValue, + reverseExchangeValue + }; +}; + +export default Converter; diff --git a/src/components/elements/Input.js b/src/components/elements/Input.js new file mode 100644 index 00000000..43ae7332 --- /dev/null +++ b/src/components/elements/Input.js @@ -0,0 +1,13 @@ +import React from "react"; + +const Input = props => { + const classNames = `form-control ${props.className}`; + + return ; +}; + +Input.defaultProps = { + type: "text" +}; + +export default Input; diff --git a/src/components/elements/InputGroup.js b/src/components/elements/InputGroup.js new file mode 100644 index 00000000..7c105ff7 --- /dev/null +++ b/src/components/elements/InputGroup.js @@ -0,0 +1,10 @@ +import React from "react"; + +const InputGroup = ({ name, labelText, children }) => ( +
+ + {children} +
+); + +export default InputGroup; diff --git a/src/components/elements/JumbotronFluid.js b/src/components/elements/JumbotronFluid.js new file mode 100644 index 00000000..3c2c6f91 --- /dev/null +++ b/src/components/elements/JumbotronFluid.js @@ -0,0 +1,17 @@ +import React from 'react' +import Showable from './Showable' + +const JumbotronFluid = ({heading, lead}) => ( +
+
+ +

{heading}

+
+ +

{lead}

+
+
+
+) + +export default JumbotronFluid diff --git a/src/components/elements/Select.js b/src/components/elements/Select.js new file mode 100644 index 00000000..1453e8a7 --- /dev/null +++ b/src/components/elements/Select.js @@ -0,0 +1,34 @@ +import React from "react"; + +const Select = props => { + const { baseValue, handleSwitch, data } = props; + + //gets keys from dataObject + //console.log("DAATA", data); + const optionsList = data.map((item, i) => { + return ( + + ); + }); + return ( + + ); +}; + +Select.defaultProps = { + type: "text" +}; + +export default Select; diff --git a/src/components/elements/Showable.js b/src/components/elements/Showable.js new file mode 100644 index 00000000..11872e3a --- /dev/null +++ b/src/components/elements/Showable.js @@ -0,0 +1,11 @@ +import React from "react"; + +const Showable = ({ show, children }) => { + if (!show) { + return null; + } + + return
{children}
; +}; + +export default Showable; diff --git a/src/components/elements/Table.js b/src/components/elements/Table.js new file mode 100644 index 00000000..41a3e4ad --- /dev/null +++ b/src/components/elements/Table.js @@ -0,0 +1,29 @@ +import React from "react"; +import Converter from "./Converter"; + +const Table = props => { + const { baseCurrency, rates } = props; + const currenciesArr = []; + + const currencies = Object.keys(rates).forEach(function(key, index) { + currenciesArr.push( + + + + + + ); + }); + return ( +
{key}{rates[key]}{1 / rates[key]}
+ + + + + + {currenciesArr} +
CurrencyUnits per {baseCurrency}{baseCurrency} per Unit
+ ); +}; + +export default Table; diff --git a/big_mac_2017_01.js b/src/components/elements/big_mac_2017_01.js similarity index 100% rename from big_mac_2017_01.js rename to src/components/elements/big_mac_2017_01.js diff --git a/src/index.css b/src/index.css new file mode 100644 index 00000000..f0a4785a --- /dev/null +++ b/src/index.css @@ -0,0 +1,104 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} + +.currency_table { + width: 90%; + margin: 2rem auto; + max-height: 15rem; + overflow: auto; + font-size: 1.2rem; +} +.currency_table * { + margin: 0 auto; +} +.currency_table th { + width: 1%; +} +#ChooseBaseCurrency { + width: 90%; + text-align: center; +} + +#ChooseBaseCurrency * { + width: 30%; + margin: 0.5rem 3rem; + display: inline-block; + font-size: 1.5rem; +} +@media (max-width: 865px) { + #ChooseBaseCurrency * { + width: 60%; + margin: 1rem 3rem; + display: inline-block; + font-size: 1.5rem; + } + .currency_table { + width: 92%; + margin: 1rem auto; + max-height: fit-content; + font-size: 1rem; + } +} + +section h1 { + width: 50%; + margin: 5rem auto; + text-decoration: none; + background-color: #9fbcdf; + outline: 38px solid #9fbcdf; +} + +.converter_wrapper { + text-align: center; + margin-top: 3rem; +} + +#currencyConverterForm { + outline: 1px solid #d9d8d8; + padding: 0.5rem; + margin-bottom: 8rem; +} + +#currencyConverterForm input, +#currencyConverterForm select { + width: 8rem; + height: 2.5rem; + margin: 0.5rem 1.5rem; +} +.converter_get_input, +.outcome { + display: flex; + flex-wrap: wrap; + width: fit-content; + margin: 0.5rem auto; +} +#converter_to { + font-family: sans-serif; + margin: -0.1rem 3rem; + font-size: 2rem; + font-style: italic; + font-weight: 500; +} + +.rate { + display: flex; + width: fit-content; + margin: 0.5rem auto; +} + +.rate p { + margin: 0 2rem; + font-size: 2rem; +} + +.outcome { + border: 2px solid #6ec2ba; + margin: 2rem auto; +} +.outcome p { + width: 10rem; + font-size: 2rem; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..f8f271ca --- /dev/null +++ b/src/index.js @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import "bootstrap/dist/css/bootstrap.css"; +import "./index.css"; +import App from "./components/App"; +import registerServiceWorker from "./registerServiceWorker"; + +ReactDOM.render(, document.getElementById("root")); +registerServiceWorker(); diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 00000000..6b60c104 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js new file mode 100644 index 00000000..4a3ccf02 --- /dev/null +++ b/src/registerServiceWorker.js @@ -0,0 +1,108 @@ +// In production, we register a service worker to serve assets from local cache. + +// 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 the "N+1" visit to a page, since previously +// cached resources are updated in the background. + +// To learn more about the benefits of this model, read https://goo.gl/KwvDNy. +// This link also includes instructions on opting out of this behavior. + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export default function register() { + 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); + 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/facebookincubator/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (!isLocalhost) { + // Is not local host. Just register service worker + registerValidSW(swUrl); + } else { + // This is running on localhost. Lets check if a service worker still exists or not. + checkValidServiceWorker(swUrl); + } + }); + } +} + +function registerValidSW(swUrl) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the old content will have been purged and + // the fresh content will have been added to the cache. + // It's the perfect time to display a "New content is + // available; please refresh." message in your web app. + console.log('New content is available; please refresh.'); + } 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.'); + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + if ( + response.status === 404 || + response.headers.get('content-type').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); + } + }) + .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(); + }); + } +}