From 057f27a970e0833a6b4c8f8b43de5e04a59e79e0 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Thu, 9 May 2019 23:28:58 +0200 Subject: [PATCH] balance data fetching refactor, preferences tweaks --- package.json | 1 + src/App.css | 17 ++------ src/components/Balance.css | 35 +++++++++++++++ src/components/Balance.jsx | 36 ++++++++------- src/components/Total.jsx | 42 +++++++++--------- src/screens/Home.css | 27 ------------ src/screens/Preferences.css | 42 ++++++++++++------ src/screens/Preferences.jsx | 45 +++++++++++++------ src/store/AppProvider.jsx | 87 +++++++++++++------------------------ src/store/createContext.jsx | 2 +- src/util/fetch.js | 18 ++++++++ 11 files changed, 192 insertions(+), 160 deletions(-) create mode 100644 src/components/Balance.css create mode 100644 src/util/fetch.js diff --git a/package.json b/package.json index a11d765..1597eb2 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@coingecko/cryptoformat": "^0.3.1", "@oceanprotocol/typographies": "^0.1.0", "@reach/router": "^1.2.1", + "ethereum-address": "0.0.4", "ms": "^2.1.1", "react": "^16.8.6", "react-blockies": "^1.4.1", diff --git a/src/App.css b/src/App.css index 1097e88..105dfc8 100644 --- a/src/App.css +++ b/src/App.css @@ -1,5 +1,3 @@ -@import '../node_modules/@oceanprotocol/typographies/css/ocean-typo.css'; - *, *::before, *::after { @@ -54,23 +52,16 @@ html.fullscreen { h1, h2, h3, -h4 { - font-family: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif; - font-weight: 600; -} - -button { - font-family: 'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', - Helvetica, Arial, sans-serif; - font-weight: 600; +h4, +h5 { + font-weight: 700; } .app { margin-top: 35px; padding: 5% 7%; cursor: default; - height: 100vh; + height: calc(100vh - 5%); transition: .15s ease-out; width: 100%; overflow-y: auto; diff --git a/src/components/Balance.css b/src/components/Balance.css new file mode 100644 index 0000000..d9a1e3a --- /dev/null +++ b/src/components/Balance.css @@ -0,0 +1,35 @@ +.number { + margin: 0; + transition: .15s ease-out; + -webkit-app-region: no-drag; + -webkit-user-select: text; + font-size: 1rem; + display: inline-block; + padding: 0 .3rem; + animation: fadeIn .5s ease-out; + border-radius: 4px; +} + +.updated { + animation: updated .5s ease-out; +} + +@keyframes updated { + 0% { + background: rgba(255, 255, 255, .2); + } + + 100% { + background: rgba(255, 255, 255, 0); + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} diff --git a/src/components/Balance.jsx b/src/components/Balance.jsx index b3741dc..09ab47c 100644 --- a/src/components/Balance.jsx +++ b/src/components/Balance.jsx @@ -1,24 +1,28 @@ -import React from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import { AppContext } from '../store/createContext' import { locale } from '../util/moneyFormatter' import { formatCurrency } from '@coingecko/cryptoformat' +import './Balance.css' -const Balance = ({ balance }) => ( -

- - {({ currency }) => - formatCurrency(balance[currency], currency.toUpperCase(), locale) +export default class Balance extends PureComponent { + static contextType = AppContext + + static propTypes = { + balance: PropTypes.object.isRequired + } + + render() { + const { currency } = this.context + const { balance } = this.props + + return ( +

+ {formatCurrency(balance[currency], currency.toUpperCase(), locale) .replace(/BTC/, 'Ƀ') .replace(/ETH/, 'Ξ') - .replace(/OCEAN/, 'Ọ') - } - -

-) - -Balance.propTypes = { - balance: PropTypes.object.isRequired + .replace(/OCEAN/, 'Ọ')} +

+ ) + } } - -export default Balance diff --git a/src/components/Total.jsx b/src/components/Total.jsx index cadd4b8..3430ed6 100644 --- a/src/components/Total.jsx +++ b/src/components/Total.jsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { PureComponent } from 'react' import { AppContext } from '../store/createContext' import Balance from './Balance' import { prices } from '../../config' @@ -19,26 +19,26 @@ const calculateTotalBalance = (accounts, currency) => { return balanceTotal } -const Total = () => ( -
- - {({ accounts }) => { - const conversions = Object.assign( - ...prices.map(key => ({ - [key]: calculateTotalBalance(accounts, key) - })) - ) +export default class Total extends PureComponent { + static contextType = AppContext - const balanceNew = { - ocean: calculateTotalBalance(accounts, 'ocean'), - ...conversions - } + render() { + const conversions = Object.assign( + ...prices.map(key => ({ + [key]: calculateTotalBalance(this.context.accounts, key) + })) + ) - return - }} - - Total Balance -
-) + const balanceNew = { + ocean: calculateTotalBalance(this.context.accounts, 'ocean'), + ...conversions + } -export default Total + return ( +
+ + Total Balance +
+ ) + } +} diff --git a/src/screens/Home.css b/src/screens/Home.css index 80c1937..b23ead5 100644 --- a/src/screens/Home.css +++ b/src/screens/Home.css @@ -64,23 +64,6 @@ color: #f6388a; } -.number { - margin: 0; - transition: .15s ease-out; - font-weight: 400; - -webkit-app-region: no-drag; - -webkit-user-select: text; - font-size: 1rem; - display: inline-block; - padding: 0 .3rem; - animation: fadeIn .5s ease-out; - border-radius: 4px; -} - -.updated { - animation: updated .5s ease-out; -} - .number-unit-wrap--accounts { min-height: 55px; } @@ -102,16 +85,6 @@ font-size: 2.5rem; } -@keyframes updated { - 0% { - background: rgba(255, 255, 255, .2); - } - - 100% { - background: rgba(255, 255, 255, 0); - } -} - @keyframes fadeIn { 0% { opacity: 0; diff --git a/src/screens/Preferences.css b/src/screens/Preferences.css index f566a8c..202ec7f 100644 --- a/src/screens/Preferences.css +++ b/src/screens/Preferences.css @@ -7,17 +7,14 @@ } .preferences__title { - font-size: 2rem; + font-size: 2.2rem; margin-top: -1rem; margin-bottom: 3rem; } .preferences__close { text-decoration: none; - font-family: 'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', - Helvetica, Arial, sans-serif; - font-weight: 600; - font-size: 2.5rem; + font-size: 2rem; position: absolute; top: -1.5rem; right: 0; @@ -37,13 +34,17 @@ border-top-color: #303030; } -.preference__list li { - list-style: none; +.preference__list li, +.preference__list li > div { display: flex; align-items: center; +} + +.preference__list li { + list-style: none; justify-content: space-between; border-bottom: 1px solid #e2e2e2; - padding-top: .3rem; + padding-top: .25rem; padding-bottom: .25rem; } @@ -56,14 +57,16 @@ border: 0; box-shadow: none; margin: 0; + padding: 0; outline: 0; color: #f6388a; font-size: 1rem; - text-transform: uppercase; } button.delete { + position: relative; font-size: 2rem; + top: -.2rem; color: #41474e; transition: color .5s ease-out; } @@ -77,18 +80,27 @@ button.delete:hover { -webkit-user-select: text; } +.preference__title, +.preference__help { + display: inline-block; + margin-top: 0; + margin-bottom: .5rem; +} + .preference__title { - font-size: 1rem; + font-size: 1.2rem; +} + +.preference__help { color: #8b98a9; + margin-left: .5rem; } .preference .identicon { width: 1.5rem !important; height: 1.5rem !important; border-radius: 50%; - vertical-align: middle; - margin-top: -.2rem; - margin-right: .5rem; + margin-right: .75rem; } .preference__input { @@ -105,3 +117,7 @@ button.delete:hover { .dark .preference__input { color: #fff; } + +.preference__error { + font-size: .9rem; +} diff --git a/src/screens/Preferences.jsx b/src/screens/Preferences.jsx index 2534d75..98c1960 100644 --- a/src/screens/Preferences.jsx +++ b/src/screens/Preferences.jsx @@ -2,15 +2,16 @@ import React, { PureComponent } from 'react' import { Link } from '@reach/router' import Store from 'electron-store' import Blockies from 'react-blockies' -import './Preferences.css' +import ethereum_address from 'ethereum-address' import { AppContext } from '../store/createContext' +import './Preferences.css' export default class Preferences extends PureComponent { static contextType = AppContext store = new Store() - state = { accounts: [], input: '' } + state = { accounts: [], input: '', error: '' } componentDidMount() { if (this.store.has('accounts')) { @@ -25,15 +26,27 @@ export default class Preferences extends PureComponent { handleSave = e => { e.preventDefault() - if ( - this.state.input !== '' && - !this.state.accounts.includes(this.state.input) // duplication check - ) { - const joined = [...this.state.accounts, this.state.input] + const { accounts, input } = this.state + + const isEmpty = input === '' + const isDuplicate = accounts.includes(input) + const isAddress = ethereum_address.isAddress(input) + + if (isEmpty) { + this.setState({ error: 'Please enter an address.' }) + return + } else if (isDuplicate) { + this.setState({ error: 'Address already added. Try another one.' }) + return + } else if (!isAddress) { + this.setState({ error: 'Not an Ethereum address. Try another one.' }) + return + } else { + const joined = [...accounts, input] this.store.set('accounts', joined) - this.setState({ accounts: joined, input: '' }) - this.context.setBalances(joined) + this.setState({ accounts: joined, input: '', error: '' }) + this.context.setBalances() } } @@ -50,10 +63,12 @@ export default class Preferences extends PureComponent { this.store.set('accounts', array) this.setState({ accounts: array }) - this.context.setBalances(array) + this.context.setBalances() } render() { + const { accounts, input, error } = this.state + return (

Preferences

{' '} @@ -62,9 +77,12 @@ export default class Preferences extends PureComponent {

Accounts

+

+ Add Ethereum account addresses holding Ocean Tokens. +

    - {this.state.accounts && - this.state.accounts.map(account => ( + {accounts && + accounts.map(account => (
  • @@ -84,7 +102,7 @@ export default class Preferences extends PureComponent { @@ -96,6 +114,7 @@ export default class Preferences extends PureComponent {
+ {error !== '' &&
{error}
}
) diff --git a/src/store/AppProvider.jsx b/src/store/AppProvider.jsx index 1bbd8d5..4189794 100644 --- a/src/store/AppProvider.jsx +++ b/src/store/AppProvider.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import ms from 'ms' import Store from 'electron-store' import { AppContext } from './createContext' +import fetchData from '../util/fetch' import { refreshInterval, prices, oceanTokenContract } from '../../config' export default class AppProvider extends PureComponent { @@ -19,24 +20,19 @@ export default class AppProvider extends PureComponent { needsConfig: false, prices: Object.assign(...prices.map(key => ({ [key]: 0 }))), toggleCurrencies: currency => this.setState({ currency }), - setBalances: account => this.setBalances(account) + setBalances: () => this.setBalances() } async componentDidMount() { - const { accountsPref } = await this.getAccounts() await this.fetchAndSetPrices() - await this.setBalances(accountsPref) + await this.setBalances() - await setInterval(this.fetchAndSetPrices, ms(refreshInterval)) - await setInterval(this.setBalances, ms(refreshInterval)) + setInterval(this.fetchAndSetPrices, ms(refreshInterval)) + setInterval(this.setBalances, ms(refreshInterval)) this.setState({ isLoading: false }) } - componentWillUnmount() { - this.clearAccounts() - } - getAccounts() { let accountsPref @@ -50,64 +46,41 @@ export default class AppProvider extends PureComponent { accountsPref = [] } - return { accountsPref } + return accountsPref } - clearAccounts() { - this.setState({ accounts: [] }) - } - - async fetch(url) { - try { - const response = await fetch(url) - - if (response.status !== 200) { - return console.log('Non-200 response: ' + response.status) // eslint-disable-line - } - - const json = await response.json() - if (!json) return - - return json - } catch (error) { - console.log('Error parsing json:' + error) // eslint-disable-line - } - } - - async fetchBalance(account) { - const json = await this.fetch( + async getBalance(account) { + const json = await fetchData( `https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest` ) - const balance = (json.result /= 1000000000000000000) // Convert from wei 10^18 + const balance = (json.result /= 1000000000000000000) // Convert from vodka 10^18 return balance } - async fetchAndSetPrices() { + fetchAndSetPrices = async () => { const currencies = prices.join(',') - const json = await this.fetch( + const json = await fetchData( `https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}` ) - await this.setState({ - prices: Object.assign( - ...prices.map(key => ({ - ocean: 1, - [key]: json['ocean-protocol'][key] - })) - ) - }) + const newPrizes = Object.assign( + ...prices.map(key => ({ + ocean: 1, + [key]: json['ocean-protocol'][key] + })) + ) + + this.setState({ prices: newPrizes }) } - setBalances(accounts) { - // TODO: make this less lazy and update numbers in place - // when they are changed instead of resetting all to 0 here - this.clearAccounts() + setBalances = async () => { + const accountsPref = await this.getAccounts() - const accountsArray = accounts ? accounts : this.state.accounts + let newAccounts = [] - accountsArray.map(async account => { - const oceanBalance = await this.fetchBalance(account) + for (const account of accountsPref) { + const oceanBalance = await this.getBalance(account) const conversions = Object.assign( ...prices.map(key => ({ @@ -118,15 +91,17 @@ export default class AppProvider extends PureComponent { const newAccount = { address: account, balance: { - ocean: oceanBalance || 0, + ocean: oceanBalance, ...conversions } } - await this.setState(prevState => ({ - accounts: [...prevState.accounts, newAccount] - })) - }) + newAccounts.push(newAccount) + } + + if (newAccounts !== this.state.accounts) { + this.setState({ accounts: newAccounts }) + } } render() { diff --git a/src/store/createContext.jsx b/src/store/createContext.jsx index 75ba03c..10b0812 100644 --- a/src/store/createContext.jsx +++ b/src/store/createContext.jsx @@ -1,5 +1,5 @@ import { createContext } from 'react' -const AppContext = createContext({}) +const AppContext = createContext() export { AppContext } diff --git a/src/util/fetch.js b/src/util/fetch.js new file mode 100644 index 0000000..d0f33c3 --- /dev/null +++ b/src/util/fetch.js @@ -0,0 +1,18 @@ +const fetchData = async url => { + try { + const response = await fetch(url) + + if (response.status !== 200) { + return console.log('Non-200 response: ' + response.status) // eslint-disable-line + } + + const json = await response.json() + if (!json) return + + return json + } catch (error) { + console.log('Error parsing json:' + error) // eslint-disable-line + } +} + +export default fetchData