Merge pull request #1 from kremalicious/feature/preferences

user preferences & router setup
This commit is contained in:
Matthias Kretschmann 2019-05-09 00:59:22 +02:00 committed by GitHub
commit 63f4e1c2e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 487 additions and 185 deletions

View File

@ -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

View File

@ -1,5 +1,4 @@
module.exports = {
accounts: ['ETH ADDRESS 1', 'ETH ADDRESS 2'],
prices: ['eur', 'usd', 'btc', 'eth'],
refreshInterval: '1m',
oceanTokenContract: '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41'

View File

@ -20,8 +20,10 @@
"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-blockies": "^1.4.1",
"react-dom": "^16.8.6"
},
"devDependencies": {
@ -31,6 +33,7 @@
"@babel/preset-env": "^7.4.4",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.4.4",
"@svgr/webpack": "^4.2.0",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.5",
"css-loader": "^2.1.1",
@ -38,6 +41,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",

View File

@ -1,24 +1,26 @@
@import '../node_modules/@oceanprotocol/typographies/css/ocean-typo.css';
html,
body {
margin: 0;
padding: 0;
height: 100%;
background: #fcfcfc !important;
}
html.dark,
.dark body {
background: #141414 !important;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #fcfcfc !important;
overflow: hidden;
}
html.dark,
.dark body {
background: #141414 !important;
}
html {
font-size: 13px;
}
@ -64,11 +66,18 @@ button {
font-weight: 600;
}
.app__content {
.app {
margin-top: 35px;
padding: 5% 7%;
cursor: default;
height: calc(100vh - 35px);
height: 100vh;
transition: .15s ease-out;
width: 100%;
overflow-y: auto;
}
.app,
.app > div {
display: flex;
align-items: center;
justify-content: center;
@ -76,112 +85,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;
}
}

View File

@ -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>
)

View File

@ -1,19 +1,19 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Consumer } from '../store/createContext'
import { AppContext } from '../store/createContext'
import { locale } from '../util/moneyFormatter'
import { formatCurrency } from '@coingecko/cryptoformat'
const Balance = ({ balance }) => (
<h1 className="number">
<Consumer>
<AppContext.Consumer>
{({ currency }) =>
formatCurrency(balance[currency], currency.toUpperCase(), locale)
.replace(/BTC/, 'Ƀ')
.replace(/ETH/, 'Ξ')
.replace(/OCEAN/, 'Ọ')
}
</Consumer>
</AppContext.Consumer>
</h1>
)

View File

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react'
import { Consumer } from '../store/createContext'
import { AppContext } from '../store/createContext'
import { locale } from '../util/moneyFormatter'
import { formatCurrency } from '@coingecko/cryptoformat'
import './Ticker.css'
@ -8,7 +8,7 @@ export default class Ticker extends PureComponent {
render() {
return (
<footer className="number-unit-wrap ticker" {...this.props}>
<Consumer>
<AppContext.Consumer>
{({ toggleCurrencies, currency, prices }) => (
<>
{Object.keys(prices).map((key, i) => (
@ -27,7 +27,7 @@ export default class Ticker extends PureComponent {
))}
</>
)}
</Consumer>
</AppContext.Consumer>
</footer>
)
}

View File

@ -1,5 +1,5 @@
.titlebar {
align-self: flex-start;
position: fixed;
width: 100%;
height: 35px;
line-height: 35px;

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Consumer } from '../store/createContext'
import { AppContext } from '../store/createContext'
import Balance from './Balance'
import { prices } from '../../config'
@ -21,7 +21,7 @@ const calculateTotalBalance = (accounts, currency) => {
const Total = () => (
<div className="number-unit number-unit--main">
<Consumer>
<AppContext.Consumer>
{({ accounts }) => {
const conversions = Object.assign(
...prices.map(key => ({
@ -36,7 +36,7 @@ const Total = () => (
return <Balance balance={balanceNew} />
}}
</Consumer>
</AppContext.Consumer>
<span className="label">Total Balance</span>
</div>
)

3
src/images/cog.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M32,17.969 L32,13.969 L27.219,11.977 C27.086,11.602 26.946,11.24 26.774,10.883 L28.704,6.078 L25.875,3.25 L21.112,5.211 C20.75,5.036 20.378,4.888 19.995,4.75 L17.969,0 L13.969,0 L11.992,4.734 C11.594,4.875 11.211,5.023 10.831,5.203 L6.078,3.294 L3.25,6.122 L5.188,10.833 C5,11.219 4.847,11.614 4.703,12.021 L0,14.031 L0,18.031 L4.706,19.992 C4.852,20.398 5.008,20.794 5.195,21.18 L3.292,25.922 L6.12,28.75 L10.844,26.805 C11.222,26.985 11.61,27.13 12.008,27.266 L14.031,32 L18.031,32 L20.01,27.242 C20.39,27.101 20.765,26.953 21.124,26.781 L25.921,28.703 L28.749,25.875 L26.78,21.102 C26.947,20.743 27.085,20.38 27.218,20.008 L32,17.969 Z M15.969,22 C12.657,22 9.969,19.312 9.969,16 C9.969,12.688 12.657,10 15.969,10 C19.281,10 21.969,12.688 21.969,16 C21.969,19.312 19.281,22 15.969,22 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@ -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'))

View File

@ -31,9 +31,9 @@ const createWindow = async () => {
frame: false,
show: false,
title: 'Ocean',
scrollBounce: true,
webPreferences: {
nodeIntegration: true
nodeIntegration: true,
scrollBounce: true
}
})
@ -102,6 +102,8 @@ const createWindow = async () => {
mainWindow.setSize(width, height, true)
})
switchTheme()
// Load menubar menu items
require('./menu.js')
}

122
src/screens/Home.css Normal file
View File

@ -0,0 +1,122 @@
.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;
}
.preferences-link {
position: absolute;
right: 5%;
top: 1.5rem;
}
.preferences-link svg {
fill: #8b98a9;
transition: fill .2s ease-out;
width: 1.25rem;
height: 1.25rem;
}
.preferences-link:hover svg {
fill: #f6388a;
}
.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;
}
}

45
src/screens/Home.jsx Normal file
View File

@ -0,0 +1,45 @@
import React, { PureComponent } from 'react'
import { Link } from '@reach/router'
import { AppContext } from '../store/createContext'
import Total from '../components/Total'
import Account from '../components/Account'
import Ticker from '../components/Ticker'
import Spinner from '../components/Spinner'
import IconCog from '../images/cog.svg'
import './Home.css'
export default class Home extends PureComponent {
static contextType = AppContext
render() {
const { isLoading, accounts, needsConfig } = this.context
return (
<>
<main className="main">
<Link className="preferences-link" to="preferences">
<IconCog />
</Link>
{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} />
</>
)
}
}

106
src/screens/Preferences.css Normal file
View File

@ -0,0 +1,106 @@
.preferences {
text-align: left;
width: 100%;
margin: 5%;
position: relative;
}
.preferences__title {
font-size: 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;
position: absolute;
top: -1.5rem;
right: 0;
color: #8b98a9;
}
.preferences__close:hover {
color: #f6388a;
}
.preference__list {
padding: 0;
border-top: 1px solid #e2e2e2;
}
.dark .preference__list {
border-top-color: #303030;
}
.preference__list li {
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e2e2e2;
padding-top: .3rem;
padding-bottom: .25rem;
}
.dark .preference__list li {
border-bottom-color: #303030;
}
.preferences button {
background: none;
border: 0;
box-shadow: none;
margin: 0;
outline: 0;
color: #f6388a;
font-size: 1rem;
text-transform: uppercase;
}
button.delete {
font-size: 2rem;
color: #41474e;
transition: color .5s ease-out;
}
button.delete:hover {
color: #f6388a;
}
.preference {
-webkit-app-region: none;
-webkit-user-select: text;
}
.preference__title {
font-size: 1rem;
color: #8b98a9;
}
.preference .identicon {
width: 1.5rem !important;
height: 1.5rem !important;
border-radius: 50%;
vertical-align: middle;
margin-top: -.2rem;
margin-right: .5rem;
}
.preference__input {
font-size: 1rem;
outline: 0;
background: none;
border: 0;
width: 80%;
color: #303030;
margin-top: .75rem;
margin-bottom: .75rem;
}
.dark .preference__input {
color: #fff;
}

103
src/screens/Preferences.jsx Normal file
View File

@ -0,0 +1,103 @@
import React, { PureComponent } from 'react'
import { Link } from '@reach/router'
import Store from 'electron-store'
import Blockies from 'react-blockies'
import './Preferences.css'
import { AppContext } from '../store/createContext'
export default class Preferences extends PureComponent {
static contextType = AppContext
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, input: '' })
this.context.setBalances(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 })
this.context.setBalances(array)
}
render() {
return (
<div className="preferences">
<h1 className="preferences__title">Preferences</h1>{' '}
<Link className="preferences__close" title="Close Preferences" to="/">
&times;
</Link>
<div className="preference">
<h2 className="preference__title">Accounts</h2>
<ul className="preference__list">
{this.state.accounts &&
this.state.accounts.map(account => (
<li key={account}>
<div>
<Blockies seed={account} size={10} scale={3} />
{account}
</div>
<button
className="delete"
onClick={e => this.handleDelete(e, account)}
title="Remove account"
>
&times;
</button>
</li>
))}
<li>
<input
type="text"
placeholder="0xxxxxxxx"
value={this.state.input}
onChange={this.handleInputChange}
className="preference__input"
/>
<button
className="preference__input__add"
onClick={e => this.handleSave(e)}
>
Add
</button>
</li>
</ul>
</div>
</div>
)
}
}

View File

@ -1,30 +1,31 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import ms from 'ms'
import { Provider } from './createContext'
import {
accounts,
refreshInterval,
oceanTokenContract,
prices
} from '../../config'
import Store from 'electron-store'
import { AppContext } from './createContext'
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 })
toggleCurrencies: currency => this.setState({ currency }),
setBalances: account => this.setBalances(account)
}
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))
@ -36,6 +37,22 @@ export default class AppProvider extends PureComponent {
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 +74,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 +83,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 +99,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(
@ -111,6 +130,10 @@ export default class AppProvider extends PureComponent {
}
render() {
return <Provider value={this.state}>{this.props.children}</Provider>
return (
<AppContext.Provider value={this.state}>
{this.props.children}
</AppContext.Provider>
)
}
}

View File

@ -1,5 +1,5 @@
import { createContext } from 'react'
const { Provider, Consumer } = createContext()
const AppContext = createContext({})
export { Provider, Consumer }
export { AppContext }

View File

@ -28,7 +28,12 @@ module.exports = {
include: defaultInclude
},
{
test: /\.(eot|svg|ttf|woff|woff2)$/,
test: /\.svg$/,
use: ['@svgr/webpack'],
include: defaultInclude
},
{
test: /\.(eot|ttf|woff|woff2)$/,
use: ['file-loader?name=font/[name]__[hash:base64:5].[ext]'],
include: defaultInclude
}