setup user preferences
This commit is contained in:
parent
3cb5d1b5ff
commit
bfe2a76ad6
|
@ -29,9 +29,6 @@ Clone, add adresses, and run:
|
|||
git clone git@github.com:kremalicious/ocean-balance.git
|
||||
cd ocean-balance
|
||||
|
||||
# Add one or more Ethereum addresses to config file
|
||||
vi config.js
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
# Run the app in dev mode
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
module.exports = {
|
||||
accounts: ['ETH ADDRESS 1', 'ETH ADDRESS 2'],
|
||||
prices: ['eur', 'usd', 'btc', 'eth'],
|
||||
refreshInterval: '1m',
|
||||
oceanTokenContract: '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41'
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"dependencies": {
|
||||
"@coingecko/cryptoformat": "^0.3.1",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@reach/router": "^1.2.1",
|
||||
"ms": "^2.1.1",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
@ -38,6 +39,7 @@
|
|||
"electron-devtools-installer": "^2.2.4",
|
||||
"electron-installer-dmg": "^2.0.0",
|
||||
"electron-packager": "^13.1.1",
|
||||
"electron-store": "^3.2.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-prettier": "^4.2.0",
|
||||
"eslint-plugin-react": "^7.13.0",
|
||||
|
|
127
src/App.css
127
src/App.css
|
@ -1,9 +1,16 @@
|
|||
@import '../node_modules/@oceanprotocol/typographies/css/ocean-typo.css';
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fcfcfc !important;
|
||||
}
|
||||
|
@ -13,12 +20,6 @@ html.dark,
|
|||
background: #141414 !important;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
@ -64,11 +65,15 @@ button {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app__content {
|
||||
.app {
|
||||
padding: 5% 7%;
|
||||
cursor: default;
|
||||
height: calc(100vh - 35px);
|
||||
transition: .15s ease-out;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -76,112 +81,6 @@ button {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.fullscreen .app__content {
|
||||
.fullscreen .app {
|
||||
transform: translate3d(0, -36px, 0);
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
padding: 5%;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
border: .1rem solid #e2e2e2;
|
||||
min-height: 222px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
animation: fadein .5s .5s ease-out;
|
||||
}
|
||||
|
||||
.dark .main {
|
||||
background: #222;
|
||||
border-color: #303030;
|
||||
}
|
||||
|
||||
.number-unit-wrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.number-unit {
|
||||
text-align: center;
|
||||
flex: 1 1 20%;
|
||||
margin-top: 5%;
|
||||
padding-left: 2%;
|
||||
padding-right: 2%;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #8b98a9;
|
||||
font-size: .85rem;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
margin-top: .3rem;
|
||||
transition: color .2s ease-out;
|
||||
}
|
||||
|
||||
.number-unit:hover .label {
|
||||
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;
|
||||
}
|
||||
|
||||
.number-unit--main {
|
||||
padding-bottom: 5%;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
}
|
||||
|
||||
.number-unit--main:hover .label {
|
||||
color: #8b98a9;
|
||||
}
|
||||
|
||||
.dark .number-unit--main {
|
||||
border-bottom-color: #303030;
|
||||
}
|
||||
|
||||
.number-unit--main .number {
|
||||
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;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
48
src/App.jsx
48
src/App.jsx
|
@ -1,12 +1,15 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import {
|
||||
Router,
|
||||
createMemorySource,
|
||||
createHistory,
|
||||
LocationProvider
|
||||
} from '@reach/router'
|
||||
import { webFrame } from 'electron'
|
||||
import AppProvider from './store/AppProvider'
|
||||
import { Consumer } from './store/createContext'
|
||||
import Titlebar from './components/Titlebar'
|
||||
import Total from './components/Total'
|
||||
import Account from './components/Account'
|
||||
import Ticker from './components/Ticker'
|
||||
import Spinner from './components/Spinner'
|
||||
import Home from './screens/Home'
|
||||
import Preferences from './screens/Preferences'
|
||||
import './App.css'
|
||||
|
||||
//
|
||||
|
@ -15,35 +18,22 @@ import './App.css'
|
|||
webFrame.setVisualZoomLevelLimits(1, 1)
|
||||
webFrame.setLayoutZoomLevelLimits(0, 0)
|
||||
|
||||
// https://github.com/reach/router/issues/25
|
||||
const source = createMemorySource('/')
|
||||
const history = createHistory(source)
|
||||
|
||||
export default class App extends PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<AppProvider>
|
||||
<Titlebar />
|
||||
<div className="app__content">
|
||||
<Consumer>
|
||||
{({ isLoading, accounts }) => (
|
||||
<>
|
||||
<main className="main">
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<Total />
|
||||
|
||||
<div className="number-unit-wrap number-unit-wrap--accounts">
|
||||
{accounts.map((account, i) => (
|
||||
<Account key={i} account={account} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<Ticker style={isLoading ? { opacity: 0 } : null} />
|
||||
</>
|
||||
)}
|
||||
</Consumer>
|
||||
<div className="app">
|
||||
<LocationProvider history={history}>
|
||||
<Router>
|
||||
<Home path="/" default />
|
||||
<Preferences path="preferences" />
|
||||
</Router>
|
||||
</LocationProvider>
|
||||
</div>
|
||||
</AppProvider>
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
|
||||
import App from './App'
|
||||
|
||||
document.body.style.backgroundColor = '#141414'
|
||||
|
@ -9,5 +10,4 @@ let root = document.createElement('div')
|
|||
root.id = 'root'
|
||||
document.body.appendChild(root)
|
||||
|
||||
// Now we can render our application into it
|
||||
render(<App />, document.getElementById('root'))
|
||||
|
|
|
@ -102,6 +102,8 @@ const createWindow = async () => {
|
|||
mainWindow.setSize(width, height, true)
|
||||
})
|
||||
|
||||
switchTheme()
|
||||
|
||||
// Load menubar menu items
|
||||
require('./menu.js')
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
.main {
|
||||
width: 100%;
|
||||
padding: 5%;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
border: .1rem solid #e2e2e2;
|
||||
min-height: 222px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
animation: fadein .5s .5s ease-out;
|
||||
}
|
||||
|
||||
.dark .main {
|
||||
background: #222;
|
||||
border-color: #303030;
|
||||
}
|
||||
|
||||
.number-unit-wrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.number-unit {
|
||||
text-align: center;
|
||||
flex: 1 1 20%;
|
||||
margin-top: 5%;
|
||||
padding-left: 2%;
|
||||
padding-right: 2%;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #8b98a9;
|
||||
font-size: .85rem;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
margin-top: .3rem;
|
||||
transition: color .2s ease-out;
|
||||
}
|
||||
|
||||
.number-unit:hover .label {
|
||||
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;
|
||||
}
|
||||
|
||||
.number-unit--main {
|
||||
padding-bottom: 5%;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
}
|
||||
|
||||
.number-unit--main:hover .label {
|
||||
color: #8b98a9;
|
||||
}
|
||||
|
||||
.dark .number-unit--main {
|
||||
border-bottom-color: #303030;
|
||||
}
|
||||
|
||||
.number-unit--main .number {
|
||||
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;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import { Link } from '@reach/router'
|
||||
import { Consumer } from '../store/createContext'
|
||||
import Total from '../components/Total'
|
||||
import Account from '../components/Account'
|
||||
import Ticker from '../components/Ticker'
|
||||
import Spinner from '../components/Spinner'
|
||||
import './Home.css'
|
||||
|
||||
export default class Home extends PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<Consumer>
|
||||
{({ isLoading, accounts, needsConfig }) => (
|
||||
<>
|
||||
<Link to="preferences">Settings</Link>
|
||||
<main className="main">
|
||||
{needsConfig ? (
|
||||
'Needs config'
|
||||
) : isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<Total />
|
||||
|
||||
<div className="number-unit-wrap number-unit-wrap--accounts">
|
||||
{accounts.map((account, i) => (
|
||||
<Account key={i} account={account} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<Ticker style={isLoading ? { opacity: 0 } : null} />
|
||||
</>
|
||||
)}
|
||||
</Consumer>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.preferences {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import { Link } from '@reach/router'
|
||||
import Store from 'electron-store'
|
||||
import './Preferences.css'
|
||||
|
||||
export default class Preferences extends PureComponent {
|
||||
store = new Store()
|
||||
|
||||
state = { accounts: [], input: '' }
|
||||
|
||||
componentDidMount() {
|
||||
if (this.store.has('accounts')) {
|
||||
this.setState({ accounts: this.store.get('accounts') })
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange = e => {
|
||||
this.setState({ input: e.target.value })
|
||||
}
|
||||
|
||||
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]
|
||||
this.store.set('accounts', joined)
|
||||
this.setState({ accounts: joined })
|
||||
}
|
||||
}
|
||||
|
||||
handleDelete = (e, account) => {
|
||||
e.preventDefault()
|
||||
|
||||
let array = this.state.accounts
|
||||
array = array.filter(item => account !== item)
|
||||
|
||||
const index = array.indexOf(account)
|
||||
if (index > -1) {
|
||||
array.splice(index, 1)
|
||||
}
|
||||
|
||||
this.store.set('accounts', array)
|
||||
this.setState({ accounts: array })
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="preferences">
|
||||
Hello Preferences <Link to="/">Close</Link>
|
||||
<div>
|
||||
{this.state.accounts &&
|
||||
this.state.accounts.map(account => (
|
||||
<div key={account}>
|
||||
{account}
|
||||
<button onClick={e => this.handleDelete(e, account)}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="0xxxxxxxx"
|
||||
value={this.state.input}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
<button onClick={e => this.handleSave(e)}>Add</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,41 +1,66 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ms from 'ms'
|
||||
import Store from 'electron-store'
|
||||
import { Provider } from './createContext'
|
||||
import {
|
||||
accounts,
|
||||
refreshInterval,
|
||||
oceanTokenContract,
|
||||
prices
|
||||
} from '../../config'
|
||||
import { refreshInterval, prices, oceanTokenContract } from '../../config'
|
||||
|
||||
export default class AppProvider extends PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.any.isRequired
|
||||
}
|
||||
|
||||
store = new Store()
|
||||
|
||||
state = {
|
||||
isLoading: true,
|
||||
accounts: [],
|
||||
currency: 'ocean',
|
||||
needsConfig: false,
|
||||
prices: Object.assign(...prices.map(key => ({ [key]: 0 }))),
|
||||
toggleCurrencies: currency => this.setState({ currency })
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { accountsPref } = await this.getAccounts()
|
||||
await this.fetchAndSetPrices()
|
||||
await this.setBalances()
|
||||
await this.setBalances(accountsPref)
|
||||
|
||||
await setInterval(this.fetchAndSetPrices, ms(refreshInterval))
|
||||
await setInterval(this.setBalances, ms(refreshInterval))
|
||||
|
||||
this.setState({ isLoading: false })
|
||||
|
||||
// document.addEventListener('DOMContentLoaded', () => {
|
||||
// this.store.onDidChange('accounts', async (newValue, oldValue) => {
|
||||
// const { accounts } = await this.getAccounts()
|
||||
// await this.setBalances(accounts)
|
||||
|
||||
// console.log('hello from setting window', newValue, oldValue)
|
||||
// })
|
||||
// })
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearAccounts()
|
||||
}
|
||||
|
||||
getAccounts() {
|
||||
let accountsPref
|
||||
|
||||
if (this.store.has('accounts')) {
|
||||
accountsPref = this.store.get('accounts')
|
||||
|
||||
!accountsPref.length
|
||||
? this.setState({ needsConfig: true })
|
||||
: this.setState({ needsConfig: false })
|
||||
} else {
|
||||
accountsPref = []
|
||||
}
|
||||
|
||||
return { accountsPref }
|
||||
}
|
||||
|
||||
clearAccounts() {
|
||||
this.setState({ accounts: [] })
|
||||
}
|
||||
|
@ -57,7 +82,7 @@ export default class AppProvider extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
fetchBalance = async account => {
|
||||
async fetchBalance(account) {
|
||||
const json = await this.fetch(
|
||||
`https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest`
|
||||
)
|
||||
|
@ -66,7 +91,7 @@ export default class AppProvider extends PureComponent {
|
|||
return balance
|
||||
}
|
||||
|
||||
fetchAndSetPrices = async () => {
|
||||
async fetchAndSetPrices() {
|
||||
const currencies = prices.join(',')
|
||||
const json = await this.fetch(
|
||||
`https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}`
|
||||
|
@ -82,12 +107,14 @@ export default class AppProvider extends PureComponent {
|
|||
})
|
||||
}
|
||||
|
||||
setBalances = async () => {
|
||||
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()
|
||||
|
||||
accounts.map(async account => {
|
||||
const accountsArray = accounts ? accounts : this.state.accounts
|
||||
|
||||
accountsArray.map(async account => {
|
||||
const oceanBalance = await this.fetchBalance(account)
|
||||
|
||||
const conversions = Object.assign(
|
||||
|
|
Loading…
Reference in New Issue