reafactor for css modules

This commit is contained in:
Matthias Kretschmann 2019-09-08 21:47:57 +02:00
parent e07b8be25c
commit 3745051637
Signed by: m
GPG Key ID: 606EEEF3C479A91F
41 changed files with 697 additions and 692 deletions

View File

@ -8,6 +8,3 @@ end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.css,*.scss]
indent_size = 4

View File

@ -3,10 +3,5 @@
"stylelint-config-standard",
"stylelint-config-css-modules",
"./node_modules/prettier-stylelint/config.js"
],
"rules": {
"indentation": 4,
"declaration-empty-line-before": null,
"number-leading-zero": "never"
}
]
}

View File

@ -11,7 +11,9 @@
"package": "electron-builder build -mwl -p never && open ./dist",
"dist": "rm -rf {dist,build}/ && npm run build && npm run package",
"release": "release-it --non-interactive",
"changelog": "auto-changelog -p"
"changelog": "auto-changelog -p",
"format": "prettier --write 'src/**/*.{js,jsx}' && npm run format:css",
"format:css": "prettier-stylelint --write --quiet 'src/**/*.{css,scss}'"
},
"repository": "https://github.com/kremalicious/blowfish.git",
"homepage": "https://github.com/kremalicious/blowfish",
@ -38,6 +40,7 @@
"auto-changelog": "^1.16.1",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"classnames": "^2.2.6",
"copy-webpack-plugin": "^5.0.4",
"cross-env": "^5.2.1",
"css-loader": "^3.2.0",
@ -51,14 +54,12 @@
"file-loader": "^4.2.0",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.12.0",
"prettier": "^1.18.2",
"prettier-stylelint": "^0.4.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-pose": "^4.0.8",
"release-it": "^12.3.6",
"sass-loader": "^8.0.0",
"style-loader": "^1.0.0",
"stylelint": "^10.1.0",
"stylelint-config-css-modules": "^1.4.0",

View File

@ -132,22 +132,22 @@ const installDevTools = async mainWindow => {
const createWindowEvents = mainWindow => {
mainWindow.on('enter-full-screen', () =>
mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.add(\'fullscreen\')'
"document.getElementsByTagName('html')[0].classList.add('fullscreen')"
)
)
mainWindow.on('leave-full-screen', () =>
mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.remove(\'fullscreen\')'
"document.getElementsByTagName('html')[0].classList.remove('fullscreen')"
)
)
mainWindow.on('blur', () =>
mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.add(\'blur\')'
"document.getElementsByTagName('html')[0].classList.add('blur')"
)
)
mainWindow.on('focus', () =>
mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.remove(\'blur\')'
"document.getElementsByTagName('html')[0].classList.remove('blur')"
)
)
}
@ -191,10 +191,10 @@ const switchTheme = () => {
isDarkMode
? mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.add(\'dark\')'
"document.getElementsByTagName('html')[0].classList.add('dark')"
)
: mainWindow.webContents.executeJavaScript(
'document.getElementsByTagName(\'html\')[0].classList.remove(\'dark\')'
"document.getElementsByTagName('html')[0].classList.remove('dark')"
)
}
}

View File

@ -1,96 +0,0 @@
*,
*::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;
}
html.fullscreen {
font-size: 24px;
}
#root {
height: 100%;
position: relative;
font-size: 1rem;
line-height: 1.3;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Ubuntu,
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1, 'pnum' 1, 'tnum' 0,
'onum' 0, 'lnum' 0, 'dlig' 1;
color: #303030;
transform: translate3d(0, 0, 0);
-webkit-app-region: drag;
-webkit-user-select: none;
}
.dark #root {
color: #e2e2e2;
}
h1,
h2,
h3,
h4,
h5 {
font-weight: 700;
}
a,
button {
text-decoration: none;
cursor: default;
}
a h1 {
color: unset;
}
.app {
padding: 10% 7% 5% 7%;
cursor: default;
transition: .15s ease-out;
width: 100%;
height: 100%;
overflow-y: auto;
}
.darwin .app {
padding-top: calc(35px + 10%);
}
.fullscreen.darwin .app {
transform: translate3d(0, -36px, 0);
}
.box {
width: 100%;
padding: 5%;
background: #fff;
border-radius: 5px;
border: .1rem solid #e2e2e2;
}
.dark .box {
background: #222;
border-color: #303030;
}

View File

@ -10,10 +10,10 @@ import {
import { webFrame, ipcRenderer } from 'electron'
import posed, { PoseGroup } from 'react-pose'
import Titlebar from './components/Titlebar'
import { defaultAnimation } from './components/Animations'
import Home from './screens/Home'
import Preferences from './screens/Preferences'
import './App.css'
import { defaultAnimation } from './components/Animations'
import styles from './App.module.css'
//
// Disable zooming
@ -55,7 +55,7 @@ export default class App extends PureComponent {
return (
<>
{process.platform === 'darwin' && <Titlebar />}
<div className="app">
<div className={styles.app}>
<PosedRouter>
<Home path="/" default />
<Preferences path="/preferences" />

View File

@ -0,0 +1,16 @@
.app {
padding: 10% 7% 5% 7%;
cursor: default;
transition: 0.15s ease-out;
width: 100%;
height: 100%;
overflow-y: auto;
}
:global(.darwin) .app {
padding-top: calc(35px + 10%);
}
:global(.fullscreen.darwin) .app {
transform: translate3d(0, -36px, 0);
}

View File

@ -1,33 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { openUrl } from '../../utils'
import Balance from './Balance'
export default class Account extends PureComponent {
static propTypes = {
account: PropTypes.shape({
address: PropTypes.string.isRequired,
balance: PropTypes.object.isRequired
})
}
render() {
const { account } = this.props
const { balance, address } = account
return (
<div className="number-unit">
<Balance balance={balance} />
<a
className="label"
onClick={() =>
openUrl(`https://etherscan.io/address/${address}#tokentxns`)
}
title="Click to view on Etherscan"
>
{address.substring(0, 12)}&hellip;
</a>
</div>
)
}
}

View File

@ -1,30 +1,36 @@
import React, { PureComponent } from 'react'
import React, { useContext } from 'react'
import PropTypes from 'prop-types'
import posed, { PoseGroup } from 'react-pose'
import { AppContext } from '../store/createContext'
import { cryptoFormatter } from '../../utils'
import styles from './Balance.module.scss'
import { fadeIn } from './Animations'
import Label from './Label'
import styles from './Balance.module.css'
const Animation = posed.h1(fadeIn)
export default class Balance extends PureComponent {
static contextType = AppContext
const Balance = ({ balance, label, large }) => {
const { currency } = useContext(AppContext)
static propTypes = {
balance: PropTypes.object.isRequired
}
render() {
const { currency } = this.context
const { balance } = this.props
return (
return (
<div className={styles.balance}>
<PoseGroup animateOnMount>
<Animation key={currency} className={styles.number}>
<Animation
key={currency}
className={large ? styles.numberLarge : styles.number}
>
{cryptoFormatter(balance[currency], currency)}
</Animation>
</PoseGroup>
)
}
{label && <Label>{label}</Label>}
</div>
)
}
Balance.propTypes = {
balance: PropTypes.object.isRequired,
label: PropTypes.string,
large: PropTypes.bool
}
export default Balance

View File

@ -0,0 +1,33 @@
.balance {
text-align: center;
width: 100%;
}
.number {
margin: 0;
-webkit-app-region: no-drag;
-webkit-user-select: text;
font-size: 1rem;
display: inline-block;
padding: 0 0.3rem;
border-radius: 4px;
}
.numberLarge {
composes: number;
font-size: 2.5rem;
}
.updated {
animation: updated 0.5s ease-out;
}
@keyframes updated {
0% {
background: rgba(255, 255, 255, 0.2);
}
100% {
background: rgba(255, 255, 255, 0);
}
}

View File

@ -1,27 +0,0 @@
.number {
margin: 0;
-webkit-app-region: no-drag;
-webkit-user-select: text;
font-size: 1rem;
display: inline-block;
padding: 0 .3rem;
border-radius: 4px;
:global(.number-unit--main) & {
font-size: 2.5rem;
}
}
.updated {
animation: updated .5s ease-out;
}
@keyframes updated {
0% {
background: rgba(255, 255, 255, .2);
}
100% {
background: rgba(255, 255, 255, 0);
}
}

View File

@ -0,0 +1,6 @@
import React from 'react'
import styles from './Divider.module.css'
const Divider = () => <hr className={styles.divider} />
export default Divider

View File

@ -0,0 +1,11 @@
.divider {
width: 100%;
height: 1px;
background: #e2e2e2;
margin-top: 5%;
border: 0;
}
:global(.dark) .divider {
background: #303030;
}

View File

@ -0,0 +1,15 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './Label.module.css'
const Label = ({ children, ...props }) => (
<span className={styles.label} {...props}>
{children}
</span>
)
Label.propTypes = {
children: PropTypes.any.isRequired
}
export default Label

View File

@ -0,0 +1,7 @@
.label {
color: #8b98a9;
font-size: 0.85rem;
display: block;
white-space: nowrap;
margin-top: 0.5rem;
}

View File

@ -1,5 +1,5 @@
import React from 'react'
import styles from './Spinner.module.scss'
import styles from './Spinner.module.css'
const Spinner = () => <div className={styles.spinner} />

View File

@ -0,0 +1,28 @@
.spinner {
position: relative;
text-align: center;
margin: auto;
width: 100%;
}
.spinner::before {
content: '';
box-sizing: border-box;
position: absolute;
top: 0;
left: 50%;
width: 20px;
height: 20px;
margin-top: -20px;
margin-left: -10px;
border-radius: 50%;
border: 2px solid #7b1173;
border-top-color: #e000cf;
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}

View File

@ -1,28 +0,0 @@
.spinner {
position: relative;
text-align: center;
margin: auto;
width: 100%;
&::before {
content: '';
box-sizing: border-box;
position: absolute;
top: 0;
left: 50%;
width: 20px;
height: 20px;
margin-top: -20px;
margin-left: -10px;
border-radius: 50%;
border: 2px solid #7b1173;
border-top-color: #e000cf;
animation: spinner .6s linear infinite;
}
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}

View File

@ -1,56 +0,0 @@
.ticker {
margin-top: 3rem;
width: 100%;
}
.ticker .number-unit {
margin-top: 0;
}
.ticker button {
background: none;
border: 1px solid #e2e2e2;
box-shadow: none;
margin: 0 auto;
outline: 0;
font-size: .75rem;
border-radius: .3rem;
padding: .3rem .4rem;
display: block;
width: 100%;
max-width: 12rem;
transition: border .2s ease-out;
color: #41474e;
}
.ticker button:disabled {
pointer-events: none;
}
.dark .ticker button {
border-color: #303030;
color: #8b98a9;
}
.label--price {
font-size: .95rem;
}
.change {
font-size: .65rem;
display: inline-block;
margin-left: .25rem;
}
.change--positive {
color: forestgreen;
}
.change--negative {
color: crimson;
}
.active .change--positive,
.active .change--negative {
color: #fff !important;
}

View File

@ -1,75 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import posed, { PoseGroup } from 'react-pose'
import { AppContext } from '../store/createContext'
import { cryptoFormatter } from '../../utils'
import './Ticker.css'
import { fadeIn } from './Animations'
const Item = posed.div(fadeIn)
const Change = ({ currency }) => (
<AppContext.Consumer>
{({ priceChanges }) => {
const isNegative = JSON.stringify(priceChanges[currency]).startsWith('-')
let classes = isNegative ? 'change--negative' : 'change--positive'
return (
<span className={`change ${classes}`} title="24hr change">
{!isNegative && '+'}
{Number(priceChanges[currency]).toFixed(1)}%
</span>
)
}}
</AppContext.Consumer>
)
Change.propTypes = {
currency: PropTypes.string.isRequired
}
export default class Ticker extends PureComponent {
static contextType = AppContext
items = () => {
const {
prices,
needsConfig,
currency,
toggleCurrencies,
accentColor
} = this.context
const activeStyle = {
backgroundColor: accentColor,
color: '#fff',
borderColor: accentColor
}
// convert Map to array first, cause for...of or forEach returns
// undefined, so it cannot be mapped to a collection of elements
return [...prices.entries()].map(([key, value]) => (
<Item key={key} className="number-unit">
<button
className={`label label--price ${key === currency && 'active'}`}
onClick={() => toggleCurrencies(key)}
disabled={needsConfig}
style={key === currency && !needsConfig ? activeStyle : {}}
>
{cryptoFormatter(value, key)}
{key !== 'ocean' && <Change currency={key} />}
</button>
</Item>
))
}
render() {
return (
<footer className="ticker" {...this.props}>
<div className="number-unit-wrap">
<PoseGroup animateOnMount>{this.items()}</PoseGroup>
</div>
</footer>
)
}
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import pkg from '../../../package.json'
import styles from './Titlebar.module.scss'
import styles from './Titlebar.module.css'
const Titlebar = () => (
<header className={styles.titlebar}>

View File

@ -0,0 +1,48 @@
.titlebar {
position: fixed;
width: 100%;
height: 35px;
line-height: 35px;
text-align: center;
user-select: none;
background: linear-gradient(to top, #ccc 0%, #d6d6d6 1px, #ebebeb 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
transition: opacity 0.15s ease-out;
}
:global(.dark) .titlebar {
background: linear-gradient(to top, #141416 0%, #38383c 1px, #3f3f44 100%);
box-shadow: none;
}
:global(.fullscreen) .titlebar {
opacity: 0;
}
:global(.blur) .titlebar {
background: #f6f6f6;
}
:global(.blur.dark) .titlebar {
background: linear-gradient(to top, #141416 0%, #2d2a32 1px, #2d2a32 100%);
}
.titlebarTitle {
line-height: 35px;
height: 35px;
font-weight: 400;
font-size: 13px;
color: #555;
}
:global(.dark) .titlebarTitle {
color: #b6b3ba;
}
:global(.blur) .titlebarTitle {
color: #b6b6b6;
}
:global(.blur.dark) .titlebarTitle {
color: #67666e;
}

View File

@ -1,58 +0,0 @@
.titlebar {
position: fixed;
width: 100%;
height: 35px;
line-height: 35px;
text-align: center;
user-select: none;
background: linear-gradient(to top, #ccc 0%, #d6d6d6 1px, #ebebeb 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5);
transition: opacity .15s ease-out;
:global(.dark) & {
background: linear-gradient(
to top,
#141416 0%,
#38383c 1px,
#3f3f44 100%
);
box-shadow: none;
}
:global(.fullscreen) & {
opacity: 0;
}
:global(.blur) & {
background: #f6f6f6;
}
:global(.blur.dark) & {
background: linear-gradient(
to top,
#141416 0%,
#2d2a32 1px,
#2d2a32 100%
);
}
}
.titlebarTitle {
line-height: 35px;
height: 35px;
font-weight: 400;
font-size: 13px;
color: #555;
:global(.dark) & {
color: #b6b3ba;
}
:global(.blur) & {
color: #b6b6b6;
}
:global(.blur.dark) & {
color: #67666e;
}
}

View File

@ -1,44 +0,0 @@
import React, { PureComponent } from 'react'
import { AppContext } from '../store/createContext'
import Balance from './Balance'
import { conversions } from '../../config'
const calculateTotalBalance = (accounts, currency) => {
const balanceTotalArray = []
for (const account of accounts) {
balanceTotalArray.push(account.balance[currency])
}
// Convert array to numbers and add numbers together
const balanceTotal = balanceTotalArray.reduce(
(a, b) => Number(a) + Number(b),
0
)
return balanceTotal
}
export default class Total extends PureComponent {
static contextType = AppContext
render() {
const conversionsBalance = Object.assign(
...conversions.map(key => ({
[key]: calculateTotalBalance(this.context.accounts, key)
}))
)
const balanceNew = {
ocean: calculateTotalBalance(this.context.accounts, 'ocean'),
...conversionsBalance
}
return (
<div className="number-unit number-unit--main">
<Balance balance={balanceNew} />
<span className="label">Total Balance</span>
</div>
)
}
}

View File

@ -1,20 +1,20 @@
import React from 'react'
import React, { useContext } from 'react'
import { Link } from '@reach/router'
import IconRocket from '../images/rocket.svg'
import styles from './Welcome.module.scss'
import { AppContext } from '../store/createContext'
import IconRocket from '../images/rocket.svg'
import styles from './Welcome.module.css'
const Welcome = () => (
<div className={styles.welcome}>
<IconRocket />
<AppContext.Consumer>
{context => (
<Link style={{ color: context.accentColor }} to="preferences">
Add your first address to get started.
</Link>
)}
</AppContext.Consumer>
</div>
)
const Welcome = () => {
const { accentColor } = useContext(AppContext)
return (
<div className={styles.welcome}>
<IconRocket />
<Link style={{ color: accentColor }} to="preferences">
Add your first address to get started.
</Link>
</div>
)
}
export default Welcome

View File

@ -0,0 +1,18 @@
.welcome {
width: 100%;
height: 100%;
text-align: center;
margin-top: 2rem;
}
.welcome svg {
display: block;
width: 5rem;
height: 5rem;
margin: 0 auto 2rem auto;
fill: #e2e2e2;
}
:global(.dark) .welcome svg {
fill: #41474e;
}

View File

@ -1,18 +0,0 @@
.welcome {
width: 100%;
height: 100%;
text-align: center;
margin-top: 2rem;
svg {
display: block;
width: 5rem;
height: 5rem;
margin: 0 auto 2rem auto;
fill: #e2e2e2;
}
:global(.dark) & svg {
fill: #41474e;
}
}

79
src/renderer/index.css Normal file
View File

@ -0,0 +1,79 @@
*,
*::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;
}
html.fullscreen {
font-size: 24px;
}
#root {
height: 100%;
position: relative;
font-size: 1rem;
line-height: 1.3;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Ubuntu,
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1, 'pnum' 1, 'tnum' 0,
'onum' 0, 'lnum' 0, 'dlig' 1;
color: #303030;
transform: translate3d(0, 0, 0);
-webkit-app-region: drag;
-webkit-user-select: none;
}
.dark #root {
color: #e2e2e2;
}
h1,
h2,
h3,
h4,
h5 {
font-weight: 700;
}
a,
button {
text-decoration: none;
cursor: default;
}
a h1 {
color: unset;
}
.box {
width: 100%;
padding: 5%;
background: #fff;
border-radius: 5px;
border: 0.1rem solid #e2e2e2;
}
.dark .box {
background: #222;
border-color: #303030;
}

View File

@ -3,6 +3,7 @@ import { render } from 'react-dom'
import AppProvider from './store/AppProvider'
import App from './App'
import pkg from '../../package.json'
import './index.css'
document.body.style.backgroundColor = '#141414'

View File

@ -1,60 +0,0 @@
.main {
min-height: 222px;
display: flex;
align-items: center;
flex-wrap: wrap;
position: relative;
}
.preferences-link {
position: absolute;
right: 5%;
top: 1.5rem;
}
.preferences-link svg {
fill: #8b98a9;
transition: .2s ease-out;
width: 1.25rem;
height: 1.25rem;
}
.dark .preferences-link svg {
fill: #8b98a9;
opacity: .5;
}
.number-unit-wrap {
display: grid;
grid-gap: .5rem;
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
justify-items: start;
width: 100%;
}
.number-unit {
text-align: center;
width: 100%;
}
.label {
color: #8b98a9;
font-size: .85rem;
display: block;
white-space: nowrap;
margin-top: .5rem;
}
.number-unit-wrap--accounts {
min-height: 55px;
padding-top: 2rem;
}
.number-unit--main {
padding-bottom: 5%;
border-bottom: 1px solid #e2e2e2;
}
.dark .number-unit--main {
border-bottom-color: #303030;
}

View File

@ -1,46 +0,0 @@
import React, { PureComponent } from 'react'
import { Link } from '@reach/router'
import { AppContext } from '../store/createContext'
import Welcome from '../components/Welcome'
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 box">
<Link className="preferences-link" to="/preferences">
<IconCog />
</Link>
{needsConfig ? (
<Welcome />
) : 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} />
</>
)
}
}

View File

@ -0,0 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types'
import { openUrl } from '../../../utils'
import Balance from '../../components/Balance'
const Account = ({ account }) => {
const { balance, address } = account
return (
<Balance
balance={balance}
label={`${address.substring(0, 12)}...`}
labelOnClick={() =>
openUrl(`https://etherscan.io/address/${address}#tokentxns`)
}
/>
)
}
Account.propTypes = {
account: PropTypes.shape({
address: PropTypes.string.isRequired,
balance: PropTypes.object.isRequired
})
}
export default Account

View File

@ -0,0 +1,71 @@
import React, { useContext } from 'react'
import PropTypes from 'prop-types'
import posed, { PoseGroup } from 'react-pose'
import { AppContext } from '../../store/createContext'
import { cryptoFormatter } from '../../../utils'
import stylesIndex from './index.module.css'
import styles from './Ticker.module.css'
import { fadeIn } from '../../components/Animations'
const Item = posed.div(fadeIn)
const Change = ({ currency }) => {
const { priceChanges } = useContext(AppContext)
const isNegative = JSON.stringify(priceChanges[currency]).startsWith('-')
let classes = isNegative ? styles.negative : styles.positive
return (
<span className={classes} title="24hr change">
{!isNegative && '+'}
{Number(priceChanges[currency]).toFixed(1)}%
</span>
)
}
Change.propTypes = {
currency: PropTypes.string.isRequired
}
const Items = () => {
const {
prices,
needsConfig,
currency,
toggleCurrencies,
accentColor
} = useContext(AppContext)
const activeStyle = {
backgroundColor: accentColor,
color: '#fff',
borderColor: accentColor
}
// convert Map to array first, cause for...of or forEach returns
// undefined, so it cannot be mapped to a collection of elements
return [...prices.entries()].map(([key, value]) => (
<Item key={key} className={styles.number}>
<button
className={key === currency ? styles.labelActive : styles.label}
onClick={() => toggleCurrencies(key)}
disabled={needsConfig}
style={key === currency && !needsConfig ? activeStyle : {}}
>
{cryptoFormatter(value, key)}
{key !== 'ocean' && <Change currency={key} />}
</button>
</Item>
))
}
const Ticker = props => (
<footer className={styles.ticker} {...props}>
<div className={stylesIndex.balanceWrap}>
<PoseGroup animateOnMount>
<Items key="items" />
</PoseGroup>
</div>
</footer>
)
export default Ticker

View File

@ -0,0 +1,76 @@
.ticker {
margin-top: 3rem;
width: 100%;
}
.ticker button {
background: none;
border: 1px solid #e2e2e2;
box-shadow: none;
margin: 0 auto;
outline: 0;
font-size: 0.75rem;
border-radius: 0.3rem;
padding: 0.3rem 0.4rem;
display: block;
width: 100%;
max-width: 12rem;
transition: border 0.2s ease-out;
color: #41474e;
}
.ticker button:disabled {
pointer-events: none;
}
:global(.dark) .ticker button {
border-color: #303030;
color: #8b98a9;
}
.label--price {
font-size: 0.95rem;
}
.change {
font-size: 0.65rem;
display: inline-block;
margin-left: 0.25rem;
}
.positive {
composes: change;
color: forestgreen;
}
.negative {
composes: change;
color: crimson;
}
:global(.active) .positive,
:global(.active) .negative {
color: #fff !important;
}
.number {
text-align: center;
width: 100%;
}
.label {
color: #8b98a9;
font-size: 0.85rem;
display: block;
white-space: nowrap;
margin-top: 0.5rem;
}
.labelActive {
composes: label;
}
.labelActive .positive,
.labelActive .negative {
color: #fff !important;
}

View File

@ -0,0 +1,39 @@
import React, { useContext } from 'react'
import { AppContext } from '../../store/createContext'
import Balance from '../../components/Balance'
import { conversions } from '../../../config'
const calculateTotalBalance = (accounts, currency) => {
const balanceTotalArray = []
for (const account of accounts) {
balanceTotalArray.push(account.balance[currency])
}
// Convert array to numbers and add numbers together
const balanceTotal = balanceTotalArray.reduce(
(a, b) => Number(a) + Number(b),
0
)
return balanceTotal
}
const Total = () => {
const { accounts } = useContext(AppContext)
const conversionsBalance = Object.assign(
...conversions.map(key => ({
[key]: calculateTotalBalance(accounts, key)
}))
)
const balanceNew = {
ocean: calculateTotalBalance(accounts, 'ocean'),
...conversionsBalance
}
return <Balance balance={balanceNew} label="Total Balance" large />
}
export default Total

View File

@ -0,0 +1,54 @@
import React, { useContext } from 'react'
import { Link } from '@reach/router'
import classnames from 'classnames'
import { AppContext } from '../../store/createContext'
import Welcome from '../../components/Welcome'
import Spinner from '../../components/Spinner'
import Divider from '../../components/Divider'
import Total from './Total'
import Account from './Account'
import Ticker from './Ticker'
import IconCog from '../../images/cog.svg'
import styles from './index.module.css'
const Accounts = () => {
const { accounts } = useContext(AppContext)
return (
<div className={styles.balanceWrapAccounts}>
{accounts.map((account, i) => (
<Account key={i} account={account} />
))}
</div>
)
}
const Home = () => {
const { isLoading, needsConfig } = useContext(AppContext)
return (
<>
<main className={classnames(styles.main, 'box')}>
<Link className={styles.preferences} to="/preferences">
<IconCog />
</Link>
{needsConfig ? (
<Welcome />
) : isLoading ? (
<Spinner />
) : (
<>
<Total />
<Divider />
<Accounts />
</>
)}
</main>
<Ticker style={isLoading ? { opacity: 0 } : null} />
</>
)
}
export default Home

View File

@ -0,0 +1,39 @@
.main {
min-height: 222px;
display: flex;
align-items: center;
flex-wrap: wrap;
position: relative;
}
.preferences {
position: absolute;
right: 5%;
top: 1.5rem;
}
.preferences svg {
fill: #8b98a9;
transition: 0.2s ease-out;
width: 1.25rem;
height: 1.25rem;
}
:global(.dark) .preferences svg {
fill: #8b98a9;
opacity: 0.5;
}
.balanceWrap {
display: grid;
grid-gap: 0.5rem;
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
justify-items: start;
width: 100%;
}
.balanceWrapAccounts {
composes: balanceWrap;
min-height: 55px;
padding-top: 2rem;
}

View File

@ -99,7 +99,9 @@ export default class Accounts extends PureComponent {
this.setState({ error: 'Address already added. Try another one.' })
return
} else if (!isAddress) {
this.setState({ error: 'Not an Ethereum address. Try another one.' })
this.setState({
error: 'Not an Ethereum address. Try another one.'
})
return
} else {
const joined = [...accounts, input]

View File

@ -1,114 +1,114 @@
.preferences {
text-align: left;
width: 100%;
position: relative;
padding-bottom: 4rem;
text-align: left;
width: 100%;
position: relative;
padding-bottom: 4rem;
}
.preferences__title {
width: 100%;
font-size: 2.2rem;
margin-top: -1rem;
margin-bottom: 2rem;
width: 100%;
font-size: 2.2rem;
margin-top: -1rem;
margin-bottom: 2rem;
}
.preferences__close {
text-decoration: none;
font-size: 2rem;
position: absolute;
top: 0;
right: 0;
color: #8b98a9;
text-decoration: none;
font-size: 2rem;
position: absolute;
top: 0;
right: 0;
color: #8b98a9;
}
.preference__list {
padding: 0;
border-top: 1px solid #e2e2e2;
padding: 0;
border-top: 1px solid #e2e2e2;
}
.dark .preference__list {
border-top-color: #303030;
border-top-color: #303030;
}
.preference__list li,
.preference__list li > div {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
.preference__list li {
list-style: none;
justify-content: space-between;
border-bottom: 1px solid #e2e2e2;
padding-top: .25rem;
padding-bottom: .25rem;
list-style: none;
justify-content: space-between;
border-bottom: 1px solid #e2e2e2;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.dark .preference__list li {
border-bottom-color: #303030;
border-bottom-color: #303030;
}
.preferences button {
background: none;
border: 0;
box-shadow: none;
margin: 0;
padding: 0;
outline: 0;
font-size: 1rem;
background: none;
border: 0;
box-shadow: none;
margin: 0;
padding: 0;
outline: 0;
font-size: 1rem;
}
button.delete {
position: relative;
font-size: 2rem;
top: -.2rem;
color: #41474e;
transition: color .5s ease-out;
position: relative;
font-size: 2rem;
top: -0.2rem;
color: #41474e;
transition: color 0.5s ease-out;
}
.preference {
-webkit-app-region: none;
-webkit-user-select: text;
-webkit-app-region: none;
-webkit-user-select: text;
}
.preference__title,
.preference__help {
display: inline-block;
margin-top: 0;
margin-bottom: .5rem;
display: inline-block;
margin-top: 0;
margin-bottom: 0.5rem;
}
.preference__title {
font-size: 1.2rem;
font-size: 1.2rem;
}
.preference__help {
color: #8b98a9;
margin-left: .5rem;
color: #8b98a9;
margin-left: 0.5rem;
}
.preference .identicon {
width: 1.5rem !important;
height: 1.5rem !important;
border-radius: 50%;
margin-right: .75rem;
width: 1.5rem !important;
height: 1.5rem !important;
border-radius: 50%;
margin-right: 0.75rem;
}
.preference__input {
font-size: 1rem;
outline: 0;
background: none;
border: 0;
width: 80%;
color: #303030;
margin-top: .75rem;
margin-bottom: .75rem;
font-size: 1rem;
outline: 0;
background: none;
border: 0;
width: 80%;
color: #303030;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.dark .preference__input {
color: #fff;
color: #fff;
}
.preference__error {
font-size: .9rem;
font-size: 0.9rem;
}

View File

@ -1,20 +1,16 @@
import React, { PureComponent } from 'react'
import React from 'react'
import { Link } from '@reach/router'
import Accounts from './Accounts'
import './index.css'
import Accounts from './Accounts'
const Preferences = () => (
<div className="preferences">
<h1 className="preferences__title">Preferences</h1>{' '}
<Link className="preferences__close" title="Close Preferences" to="/">
&times;
</Link>
<Accounts />
</div>
)
export default class Preferences extends PureComponent {
render() {
return (
<div className="preferences">
<h1 className="preferences__title">Preferences</h1>{' '}
<Link className="preferences__close" title="Close Preferences" to="/">
&times;
</Link>
<Accounts />
</div>
)
}
}
export default Preferences

View File

@ -24,11 +24,12 @@ module.exports = {
},
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: ['style-loader', 'css-loader'],
include: defaultInclude
},
{
test: /\.module\.(scss|sass)$/,
test: /\.module\.css$/,
include: defaultInclude,
loader: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
@ -41,26 +42,6 @@ module.exports = {
localsConvention: 'camelCase',
sourceMap: isDevelopment
}
},
{
loader: 'sass-loader',
options: {
sourceMap: isDevelopment
}
}
]
},
{
test: /\.(scss|sass)$/,
exclude: /\.module.(scss|sass)$/,
loader: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'sass-loader',
options: {
sourceMap: isDevelopment
}
}
]
},