diff --git a/.eslintrc b/.eslintrc index 390923c..3373b46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -34,7 +34,7 @@ "max-len": [ "error", { - "code": 120, + "code": 80, "tabWidth": 2, "ignoreUrls": true, "ignoreComments": true, @@ -43,7 +43,6 @@ "ignoreTrailingComments": true } ], - "consistent-return": 0, - "react/destructuring-assignment": 0 + "consistent-return": 0 } } diff --git a/__tests__/actions/WalletSummary.test.js b/__tests__/actions/wallet-summary.test.js similarity index 96% rename from __tests__/actions/WalletSummary.test.js rename to __tests__/actions/wallet-summary.test.js index 1eadbb5..0df55d5 100644 --- a/__tests__/actions/WalletSummary.test.js +++ b/__tests__/actions/wallet-summary.test.js @@ -32,6 +32,8 @@ describe('WalletSummary Actions', () => { transparent: 5000, shielded: 5000, addresses: [], + transactions: {}, + zecPrice: 50, }; store.dispatch(loadWalletSummarySuccess(payload)); diff --git a/__tests__/components/Sidebar.test.js b/__tests__/components/sidebar.test.js similarity index 86% rename from __tests__/components/Sidebar.test.js rename to __tests__/components/sidebar.test.js index d8636ae..76975bd 100644 --- a/__tests__/components/Sidebar.test.js +++ b/__tests__/components/sidebar.test.js @@ -13,7 +13,7 @@ describe('', () => { // $FlowFixMe const { asFragment } = render( - + , ); diff --git a/__tests__/reducers/WalletSummary.test.js b/__tests__/reducers/wallet-summary.test.js similarity index 100% rename from __tests__/reducers/WalletSummary.test.js rename to __tests__/reducers/wallet-summary.test.js diff --git a/app/assets/images/console_icon.svg b/app/assets/images/console_icon.svg new file mode 100644 index 0000000..b079435 --- /dev/null +++ b/app/assets/images/console_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/console_icon_active.svg b/app/assets/images/console_icon_active.svg new file mode 100644 index 0000000..66106e9 --- /dev/null +++ b/app/assets/images/console_icon_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/dashboard_icon.svg b/app/assets/images/dashboard_icon.svg new file mode 100644 index 0000000..12bd57c --- /dev/null +++ b/app/assets/images/dashboard_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/dashboard_icon_active.svg b/app/assets/images/dashboard_icon_active.svg new file mode 100644 index 0000000..acb3945 --- /dev/null +++ b/app/assets/images/dashboard_icon_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/menu_icon.svg b/app/assets/images/menu_icon.svg new file mode 100644 index 0000000..2dbc39f --- /dev/null +++ b/app/assets/images/menu_icon.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/assets/images/receive_icon.svg b/app/assets/images/receive_icon.svg new file mode 100644 index 0000000..5576d8f --- /dev/null +++ b/app/assets/images/receive_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/receive_icon_active.svg b/app/assets/images/receive_icon_active.svg new file mode 100644 index 0000000..4ec6f51 --- /dev/null +++ b/app/assets/images/receive_icon_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/send_icon.svg b/app/assets/images/send_icon.svg new file mode 100644 index 0000000..1c0a162 --- /dev/null +++ b/app/assets/images/send_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/send_icon_active.svg b/app/assets/images/send_icon_active.svg new file mode 100644 index 0000000..58a759d --- /dev/null +++ b/app/assets/images/send_icon_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/settings_icon.svg b/app/assets/images/settings_icon.svg new file mode 100644 index 0000000..0503b15 --- /dev/null +++ b/app/assets/images/settings_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/settings_icon_active.svg b/app/assets/images/settings_icon_active.svg new file mode 100644 index 0000000..88802f3 --- /dev/null +++ b/app/assets/images/settings_icon_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/transaction_received_icon.svg b/app/assets/images/transaction_received_icon.svg new file mode 100644 index 0000000..e55a426 --- /dev/null +++ b/app/assets/images/transaction_received_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/transaction_sent_icon.svg b/app/assets/images/transaction_sent_icon.svg new file mode 100644 index 0000000..990a3a5 --- /dev/null +++ b/app/assets/images/transaction_sent_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/transactions_icon.svg b/app/assets/images/transactions_icon.svg new file mode 100644 index 0000000..becaac7 --- /dev/null +++ b/app/assets/images/transactions_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/transactions_icon_active.svg b/app/assets/images/transactions_icon_active.svg new file mode 100644 index 0000000..0913d8b --- /dev/null +++ b/app/assets/images/transactions_icon_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/zcash-icon.png b/app/assets/images/zcash-icon.png new file mode 100644 index 0000000..060bd00 Binary files /dev/null and b/app/assets/images/zcash-icon.png differ diff --git a/app/components/button.js b/app/components/button.js index 650bc7b..5b1cc18 100644 --- a/app/components/button.js +++ b/app/components/button.js @@ -4,6 +4,7 @@ import React from 'react'; import styled from 'styled-components'; import { Link } from 'react-router-dom'; /* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable max-len */ // $FlowFixMe import { darken } from 'polished'; @@ -13,8 +14,14 @@ const defaultStyles = ` // $FlowFixMe props => props.theme.fontFamily }; - font-weight: bold; - font-size: 0.9em; + font-weight: ${ + // $FlowFixMe + props => props.theme.fontWeight.bold +}; + font-size: ${ + // $FlowFixMe + props => `${props.theme.fontSize.text}em` +}; cursor: pointer; outline: none; min-width: 100px; diff --git a/app/components/column.js b/app/components/column.js new file mode 100644 index 0000000..ba58e4d --- /dev/null +++ b/app/components/column.js @@ -0,0 +1,29 @@ +// @flow + +import React from 'react'; +import styled from 'styled-components'; +import type { Node } from 'react'; + +const Flex = styled.div` + display: flex; + flex-direction: column; + align-items: ${props => props.alignItems}; + justify-content: ${props => props.justifyContent}; +`; + +type Props = { + alignItems?: string, + justifyContent?: string, + className?: string, + children: Node, +}; + +export const ColumnComponent = ({ children, ...props }: Props) => ( + {React.Children.map(children, ch => ch)} +); + +ColumnComponent.defaultProps = { + alignItems: 'flex-start', + justifyContent: 'flex-start', + className: '', +}; diff --git a/app/components/dropdown.js b/app/components/dropdown.js index 6d6d42f..59c9101 100644 --- a/app/components/dropdown.js +++ b/app/components/dropdown.js @@ -11,7 +11,10 @@ import { TextComponent } from './text'; /* eslint-disable max-len */ const MenuWrapper = styled.div` - background-image: ${props => `linear-gradient(to right, ${darken(0.05, props.theme.colors.activeItem)}, ${props.theme.colors.activeItem})`}; + background-image: ${props => `linear-gradient(to right, ${darken( + 0.05, + props.theme.colors.activeItem, + )}, ${props.theme.colors.activeItem})`}; padding: 10px 20px; border-radius: 10px; margin-left: -10px; @@ -68,22 +71,27 @@ export class DropdownComponent extends Component { }; render() { + const { isOpen } = this.state; + const { label, options, renderTrigger } = this.props; + return ( this.setState(() => ({ isOpen: false }))}> + this.setState(() => ({ isOpen: false }))} + > - {this.props.label && ( + {label && ( - + )} - {this.props.options.map(({ label, onClick }) => ( + {options.map(({ label: optionLabel, onClick }) => ( - + ))} @@ -91,7 +99,10 @@ export class DropdownComponent extends Component { ]} tipSize={7} > - {this.props.renderTrigger(() => this.setState(state => ({ isOpen: !state.isOpen })), this.state.isOpen)} + {renderTrigger( + () => this.setState(state => ({ isOpen: !state.isOpen })), + isOpen, + )} ); } diff --git a/app/components/header.js b/app/components/header.js new file mode 100644 index 0000000..6b67bd1 --- /dev/null +++ b/app/components/header.js @@ -0,0 +1,68 @@ +// @flow +import React from 'react'; + +import styled from 'styled-components'; + +import { ZCashLogo } from './zcash-logo'; +import { TextComponent } from './text'; + +const Wrapper = styled.div` + height: ${props => props.theme.headerHeight}; + width: 100vw; + display: flex; + flex-direction: row; + background-color: ${props => props.theme.colors.background}; +`; + +const LogoWrapper = styled.div` + height: ${props => props.theme.headerHeight}; + width: ${props => props.theme.sidebarWidth}; + background-image: linear-gradient( + to right, + ${props => props.theme.colors.sidebarLogoGradientBegin}, + ${props => props.theme.colors.sidebarLogoGradientEnd} + ); + margin-bottom: 20px; +`; + +const TitleWrapper = styled.div` + width: ${props => `calc(100% - ${props.theme.sidebarWidth})`}; + height: ${props => props.theme.headerHeight}; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + padding-top: 10px; + padding-left: ${props => props.theme.layoutPaddingLeft}; + padding-right: ${props => props.theme.layoutPaddingRight}; +`; + +const Title = styled(TextComponent)` + font-size: ${props => `${props.theme.fontSize.title}em`}; + margin-top: 10px; + margin-bottom: 10px; + text-transform: capitalize; +`; + +const Divider = styled.div` + width: 100%; + background-color: ${props => props.theme.colors.text}; + height: 1px; + opacity: 0.1; +`; + +type Props = { + title: string, +}; + +export const HeaderComponent = ({ title }: Props) => ( + + + + + + + <Divider /> + </TitleWrapper> + </Wrapper> +); diff --git a/app/components/input.js b/app/components/input.js index 68ce8fb..3e61a97 100644 --- a/app/components/input.js +++ b/app/components/input.js @@ -36,8 +36,12 @@ type Props = { export const InputComponent = ({ inputType, onChange, ...props }: Props) => { const inputTypes = { - input: () => <Input onChange={evt => onChange(evt.target.value)} {...props} />, - textarea: () => <Textarea onChange={evt => onChange(evt.target.value)} {...props} />, + input: () => ( + <Input onChange={evt => onChange(evt.target.value)} {...props} /> + ), + textarea: () => ( + <Textarea onChange={evt => onChange(evt.target.value)} {...props} /> + ), dropdown: () => null, }; diff --git a/app/components/layout.js b/app/components/layout.js index 2dc0ba7..47c0b28 100644 --- a/app/components/layout.js +++ b/app/components/layout.js @@ -1,14 +1,15 @@ // @flow - import React from 'react'; import styled from 'styled-components'; const Layout = styled.div` display: flex; flex-direction: column; - width: 100%; - height: 100vh; - background: ${props => props.theme.colors.secondary}; + width: ${props => `calc(100% - ${props.theme.sidebarWidth})`}; + height: ${props => `calc(100vh - ${props.theme.headerHeight})`}; + background-color: ${props => props.theme.colors.background}; + padding-left: ${props => props.theme.layoutPaddingLeft}; + padding-right: ${props => props.theme.layoutPaddingRight}; `; type Props = { diff --git a/app/components/qrcode.js b/app/components/qrcode.js index 64dc250..862d572 100644 --- a/app/components/qrcode.js +++ b/app/components/qrcode.js @@ -8,7 +8,9 @@ type Props = { size?: number, }; -export const QRCode = ({ value, size }: Props) => <QR value={value} size={size} />; +export const QRCode = ({ value, size }: Props) => ( + <QR value={value} size={size} /> +); QRCode.defaultProps = { size: 128, diff --git a/app/components/row.js b/app/components/row.js index 9fb68f3..83a483c 100644 --- a/app/components/row.js +++ b/app/components/row.js @@ -14,6 +14,7 @@ const Flex = styled.div` type Props = { alignItems?: string, justifyContent?: string, + className?: string, children: Node, }; @@ -24,4 +25,5 @@ export const RowComponent = ({ children, ...props }: Props) => ( RowComponent.defaultProps = { alignItems: 'flex-start', justifyContent: 'flex-start', + className: '', }; diff --git a/app/components/sidebar.js b/app/components/sidebar.js index 2dc953f..4295d52 100644 --- a/app/components/sidebar.js +++ b/app/components/sidebar.js @@ -2,43 +2,76 @@ import React from 'react'; import styled from 'styled-components'; -import { Link } from 'react-router-dom'; +import { Link, type Location } from 'react-router-dom'; import { MENU_OPTIONS } from '../constants/sidebar'; const Wrapper = styled.div` display: flex; flex-direction: column; - width: 200px; - height: 100vh; + width: ${props => props.theme.sidebarWidth}; + height: ${props => `calc(100vh - ${props.theme.headerHeight})`}; font-family: ${props => props.theme.fontFamily} - background-color: ${props => props.theme.colors.sidebarBg}; - padding: 20px; + background-color: ${props => props.theme.colors.sidebarBg}; + padding-top: 15px; `; const StyledLink = styled(Link)` - color: ${props => props.theme.colors.sidebarItem}; - font-size: 16px; + color: ${props => (props.isActive + ? props.theme.colors.sidebarItemActive + : props.theme.colors.sidebarItem)}; + font-size: ${props => `${props.theme.fontSize.text}em`}; text-decoration: none; - font-weight: 700; - padding: 5px 0; + font-weight: ${props => (props.isActive + ? props.theme.fontWeight.bold + : props.theme.fontWeight.default)}; + padding: 0 20px; + height: 35px; + width: 100%; + margin: 12.5px 0; + display: flex; + align-items: center; + outline: none; + border-right: ${props => (props.isActive + ? `1px solid ${props.theme.colors.sidebarItemActive}` + : 'none')}; + + &:hover { + color: ${/* eslint-disable-next-line max-len */ + props => (props.isActive + ? props.theme.colors.sidebarItemActive + : props.theme.colors.sidebarHoveredItemLabel)}; + background-color: ${props => props.theme.colors.sidebarHoveredItem}; + } +`; + +const Icon = styled.img` + width: 20px; + height: 20px; + margin-right: 15px; `; type MenuItem = { route: string, label: string, + icon: (isActive: boolean) => string, }; type Props = { options?: MenuItem[], + location: Location, }; -export const SidebarComponent = ({ options }: Props) => ( +export const SidebarComponent = ({ options, location }: Props) => ( <Wrapper> - {(options || []).map(item => ( - <StyledLink key={item.route} to={item.route}> - {item.label} - </StyledLink> - ))} + {(options || []).map((item) => { + const isActive = location.pathname === item.route; + return ( + <StyledLink isActive={isActive} key={item.route} to={item.route}> + <Icon src={item.icon(isActive)} alt={`Sidebar Icon ${item.route}`} /> + {item.label} + </StyledLink> + ); + })} </Wrapper> ); diff --git a/app/components/text.js b/app/components/text.js index 645c553..2e110fc 100644 --- a/app/components/text.js +++ b/app/components/text.js @@ -10,7 +10,10 @@ const Text = styled.p` color: ${props => props.color || props.theme.colors.text}; margin: 0; padding: 0; - font-weight: ${props => (props.isBold ? 'bold' : '400')}; + font-weight: ${props => (props.isBold + ? props.theme.fontWeight.bold + : props.theme.fontWeight.default)}; + text-align: ${props => props.align}; `; type Props = { @@ -18,13 +21,25 @@ type Props = { isBold?: boolean, color?: string, className?: string, - size?: string, + size?: string | number, + align?: string, }; export const TextComponent = ({ - value, isBold, color, className, size, + value, + isBold, + color, + className, + size, + align, }: Props) => ( - <Text className={className} isBold={isBold} color={color} size={size}> + <Text + className={className} + isBold={isBold} + color={color} + size={`${String(size)}em`} + align={align} + > {value} </Text> ); @@ -33,5 +48,6 @@ TextComponent.defaultProps = { className: '', isBold: false, color: theme.colors.text, - size: '1em', + size: theme.fontSize.text, + align: 'left', }; diff --git a/app/components/transaction-daily.js b/app/components/transaction-daily.js new file mode 100644 index 0000000..68a3cd7 --- /dev/null +++ b/app/components/transaction-daily.js @@ -0,0 +1,65 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; +import { TransactionItemComponent, type Transaction } from './transaction-item'; +import { TextComponent } from './text'; + +const Wrapper = styled.div` + margin-top: 20px; +`; + +const TransactionsWrapper = styled.div` + border-radius: 7.5px; + overflow: hidden; + background-color: ${props => props.theme.colors.cardBackgroundColor}; + padding: 0; + margin: 0; + box-sizing: border-box; + margin-bottom: 20px; +`; + +const Day = styled(TextComponent)` + text-transform: uppercase; + color: ${props => props.theme.colors.transactionsDate}; + font-size: ${props => `${props.theme.fontSize.text * 0.9}em`}; + font-weight: ${props => props.theme.fontWeight.bold}; + margin-bottom: 5px; +`; + +const Divider = styled.div` + width: 100%; + height: 1px; + background-color: ${props => props.theme.colors.inactiveItem}; +`; + +type Props = { + transactionsDate: string, + transactions: Transaction[], + zecPrice: number, +}; + +export const TransactionDailyComponent = ({ + transactionsDate, + transactions, + zecPrice, +}: Props) => ( + <Wrapper> + <Day value={transactionsDate} /> + <TransactionsWrapper> + {transactions.map(({ + date, type, address, amount, + }, idx) => ( + <div> + <TransactionItemComponent + type={type} + date={date} + address={address || ''} + amount={amount} + zecPrice={zecPrice} + /> + {idx < transactions.length - 1 && <Divider />} + </div> + ))} + </TransactionsWrapper> + </Wrapper> +); diff --git a/app/components/transaction-daily.mdx b/app/components/transaction-daily.mdx new file mode 100644 index 0000000..f190de3 --- /dev/null +++ b/app/components/transaction-daily.mdx @@ -0,0 +1,38 @@ +--- +name: Transaction Daily +--- + +import { Playground, PropsTable } from 'docz' + +import { TransactionDailyComponent } from './transaction-daily.js' +import { DoczWrapper } from '../theme.js' + +# Transaction Item + +<PropsTable of={TransactionDailyComponent} /> + +## Basic Usage + +<Playground> + <DoczWrapper> + {() => ( + <TransactionDailyComponent + transactionsDate={new Date().toISOString()} + transactions={[ + { + type: 'received', + address: '123456789123456789123456789123456789', + amount: 1.7891, + date: new Date().toISOString(), + }, + { + type: 'sent', + address: '123456789123456789123456789123456789', + amount: 0.8458, + date: new Date().toISOString(), + }, + ]} + /> + )} + </DoczWrapper> +</Playground> diff --git a/app/components/transaction-item.js b/app/components/transaction-item.js new file mode 100644 index 0000000..0001b94 --- /dev/null +++ b/app/components/transaction-item.js @@ -0,0 +1,103 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; +import dateFns from 'date-fns'; + +import SentIcon from '../assets/images/transaction_sent_icon.svg'; +import ReceivedIcon from '../assets/images/transaction_received_icon.svg'; + +import { RowComponent } from './row'; +import { ColumnComponent } from './column'; +import { TextComponent } from './text'; + +import theme from '../theme'; + +import formatNumber from '../utils/formatNumber'; +import truncateAddress from '../utils/truncateAddress'; + +const Wrapper = styled(RowComponent)` + background-color: ${props => props.theme.colors.cardBackgroundColor}; + padding: 15px 17px; +`; + +const Icon = styled.img` + width: 20px; + height: 20px; +`; + +const TransactionTypeLabel = styled(TextComponent)` + color: ${props => (props.isReceived + ? props.theme.colors.transactionReceived + : props.theme.colors.transactionSent)}; + text-transform: capitalize; +`; + +const TransactionTime = styled(TextComponent)` + color: ${props => props.theme.colors.inactiveItem}; +`; + +const TransactionColumn = styled(ColumnComponent)` + margin-left: 10px; + margin-right: 80px; + min-width: 60px; +`; + +export type Transaction = { + type: 'send' | 'receive', + date: string, + address: string, + amount: number, + zecPrice: number, +}; + +export const TransactionItemComponent = ({ + type, + date, + address, + amount, + zecPrice, +}: Transaction) => { + const isReceived = type === 'receive'; + const transactionTime = dateFns.format(new Date(date), 'HH:mm A'); + const transactionValueInZec = formatNumber({ + value: amount, + append: `${isReceived ? '+' : '-'}ZEC `, + }); + const transactionValueInUsd = formatNumber({ + value: amount * zecPrice, + append: `${isReceived ? '+' : '-'}USD $`, + }); + const transactionAddress = truncateAddress(address); + + return ( + <Wrapper alignItems='center' justifyContent='space-between'> + <RowComponent alignItems='center'> + <RowComponent alignItems='center'> + <Icon + src={isReceived ? ReceivedIcon : SentIcon} + alt='Transaction Type Icon' + /> + <TransactionColumn> + <TransactionTypeLabel isReceived={isReceived} value={type} /> + <TransactionTime value={transactionTime} /> + </TransactionColumn> + </RowComponent> + <TextComponent value={transactionAddress} align='left' /> + </RowComponent> + <ColumnComponent alignItems='flex-end'> + <TextComponent + value={transactionValueInZec} + color={ + isReceived + ? theme.colors.transactionReceived + : theme.colors.transactionSent + } + /> + <TextComponent + value={transactionValueInUsd} + color={theme.colors.inactiveItem} + /> + </ColumnComponent> + </Wrapper> + ); +}; diff --git a/app/components/transaction-item.mdx b/app/components/transaction-item.mdx new file mode 100644 index 0000000..ed0beb3 --- /dev/null +++ b/app/components/transaction-item.mdx @@ -0,0 +1,42 @@ +--- +name: Transaction Item +--- + +import { Playground, PropsTable } from 'docz' + +import { TransactionItemComponent } from './transaction-item.js' +import { DoczWrapper } from '../theme.js' + +# Transaction Item + +<PropsTable of={TransactionItemComponent} /> + +## Sent + +<Playground> + <DoczWrapper> + {() => ( + <TransactionItemComponent + type="sent" + address="123456789123456789123456789123456789" + amount={0.8652} + date={new Date().toISOString()} + /> + )} + </DoczWrapper> +</Playground> + +## Received + +<Playground> + <DoczWrapper> + {() => ( + <TransactionItemComponent + type="received" + address="123456789123456789123456789123456789" + amount={1.7891} + date={new Date().toISOString()} + /> + )} + </DoczWrapper> +</Playground> diff --git a/app/components/wallet-summary.js b/app/components/wallet-summary.js index bc216df..63816e0 100644 --- a/app/components/wallet-summary.js +++ b/app/components/wallet-summary.js @@ -1,11 +1,13 @@ // @flow import React from 'react'; import styled from 'styled-components'; -import { IoIosMore } from 'react-icons/io'; import { TextComponent } from './text'; import { RowComponent } from './row'; import { DropdownComponent } from './dropdown'; +import MenuIcon from '../assets/images/menu_icon.svg'; + +import formatNumber from '../utils/formatNumber'; import theme from '../theme'; @@ -15,13 +17,13 @@ const Wrapper = styled.div` background-color: ${props => props.theme.colors.cardBackgroundColor}; border-radius: 5px; padding: 37px 45px; - margin: 20px; + margin-top: 20px; position: relative; `; const AllAddresses = styled(TextComponent)` margin-bottom: 2.5px; - font-size: 0.625em; + font-size: ${props => `${props.theme.fontSize.text}em`}; `; const ValueBox = styled.div` @@ -48,9 +50,12 @@ const SeeMoreButton = styled.button` align-items: center; justify-content: center; outline: none; + border-style: solid; border-radius: 100%; border-width: 1px; - border-color: ${props => (props.isOpen ? props.theme.colors.activeItem : props.theme.colors.text)}; + border-color: ${props => (props.isOpen + ? props.theme.colors.activeItem + : props.theme.colors.inactiveItem)}; background-color: transparent; padding: 5px; cursor: pointer; @@ -63,44 +68,78 @@ const SeeMoreButton = styled.button` } `; +const SeeMoreIcon = styled.img` + width: 25px; + height: 25px; +`; + type Props = { total: number, shielded: number, transparent: number, - dollarValue: number, + zecPrice: number, addresses: string[], }; -const formatNumber = number => number.toLocaleString('de-DE'); - export const WalletSummaryComponent = ({ - total, shielded, transparent, dollarValue, addresses, + total, + shielded, + transparent, + zecPrice, + addresses, }: Props) => ( <Wrapper> <DropdownComponent label='All Addresses' renderTrigger={(toggleVisibility, isOpen) => ( <SeeMoreButton onClick={toggleVisibility} isOpen={isOpen}> - <IoIosMore color={theme.colors.text} size={15} /> + <SeeMoreIcon src={MenuIcon} alt='Menu Icon' /> </SeeMoreButton> )} options={addresses.map(addr => ({ label: addr, onClick: x => x }))} /> <AllAddresses value='ALL ADDRESSES' isBold /> <ValueBox> - <TextComponent size='2.625em' value={`ZEC ${formatNumber(total)}`} isBold /> - <USDValue value={`USD $${formatNumber(total * dollarValue)}`} size='1.750em' /> + <TextComponent + size={theme.fontSize.zecValueBase * 2.5} + value={`ZEC ${formatNumber({ value: total })}`} + isBold + /> + <USDValue + value={`USD $${formatNumber({ value: total * zecPrice })}`} + size={theme.fontSize.zecValueBase * 2} + /> </ValueBox> <RowComponent> <ValueBox> - <ShieldedValue value='● SHIELDED' isBold size='0.625em' /> - <TextComponent value={`ZEC ${formatNumber(shielded)}`} isBold size='1.125em' /> - <USDValue value={`USD $${formatNumber(shielded * dollarValue)}`} size='0.750em' /> + <ShieldedValue + value='● SHIELDED' + isBold + size={theme.fontSize.text * 0.8} + /> + <TextComponent + value={`ZEC ${formatNumber({ value: shielded })}`} + isBold + size={theme.fontSize.zecValueBase} + /> + <USDValue + value={`USD $${formatNumber({ value: shielded * zecPrice })}`} + /> </ValueBox> <ValueBox> - <Label value='● TRANSPARENT' isBold size='0.625em' /> - <TextComponent value={`ZEC ${formatNumber(transparent)}`} isBold size='1.125em' /> - <USDValue value={`USD $${formatNumber(transparent * dollarValue)}`} size='0.750em' /> + <Label + value='● TRANSPARENT' + isBold + size={theme.fontSize.text * 0.8} + /> + <TextComponent + value={`ZEC ${formatNumber({ value: transparent })}`} + isBold + size={theme.fontSize.zecValueBase} + /> + <USDValue + value={`USD $${formatNumber({ value: transparent * zecPrice })}`} + /> </ValueBox> </RowComponent> </Wrapper> diff --git a/app/components/wallet-summary.mdx b/app/components/wallet-summary.mdx index d7d75b6..abe53de 100644 --- a/app/components/wallet-summary.mdx +++ b/app/components/wallet-summary.mdx @@ -17,7 +17,13 @@ import { DoczWrapper } from '../theme.js' <DoczWrapper> {() => ( <div style={{ width: '700px' }}> - <WalletSummaryComponent total={5000} shielded={2500} transparent={2500} dollarValue={56} /> + <WalletSummaryComponent + total={5000} + shielded={2500} + transparent={2500} + dollarValue={56} + addresses={['12345678asdaas9', '98asdasd765asd4sad321']} + /> </div> )} </DoczWrapper> diff --git a/app/components/with-daemon-status-check.js b/app/components/with-daemon-status-check.js index 7275db3..e587b7d 100644 --- a/app/components/with-daemon-status-check.js +++ b/app/components/with-daemon-status-check.js @@ -9,6 +9,7 @@ type State = { type Props = {}; +/* eslint-disable max-len */ export const withDaemonStatusCheck = <PassedProps: {}>( WrappedComponent: ComponentType<PassedProps>, ): ComponentType<$Diff<PassedProps, Props>> => class extends Component<PassedProps, State> { @@ -44,7 +45,9 @@ export const withDaemonStatusCheck = <PassedProps: {}>( }; render() { - if (this.state.isRunning) { + const { isRunning } = this.state; + + if (isRunning) { return <WrappedComponent {...this.props} {...this.state} />; } diff --git a/app/components/zcash-logo.js b/app/components/zcash-logo.js new file mode 100644 index 0000000..d41b5d7 --- /dev/null +++ b/app/components/zcash-logo.js @@ -0,0 +1,15 @@ +// @flow +import React from 'react'; + +export const ZCashLogo = () => ( + <svg xmlns='http://www.w3.org/2000/svg' viewBox='-75 -10 175 175'> + <defs> + <style>{'.a{ fill:#040508; }'}</style> + </defs> + <path + className='a' + d='M541.425,662.318v4.555h-7.678v5.678H545.5l-11.751,16v4.261h7.678v4.665h4.563v-4.665h7.577v-5.666H541.788l11.777-16v-4.273h-7.577v-4.555Z' + transform='translate(-533.747 -662.318)' + /> + </svg> +); diff --git a/app/constants/routes.js b/app/constants/routes.js index 5120961..8d72502 100644 --- a/app/constants/routes.js +++ b/app/constants/routes.js @@ -1,7 +1,8 @@ // @flow export const DASHBOARD_ROUTE = '/'; +export const CONSOLE_ROUTE = '/console'; export const SEND_ROUTE = '/send'; export const RECEIVE_ROUTE = '/receive'; +export const TRANSACTIONS_ROUTE = '/transactions'; export const SETTINGS_ROUTE = '/settings'; -export const CONSOLE_ROUTE = '/console'; diff --git a/app/constants/sidebar.js b/app/constants/sidebar.js index d8d2988..0684fa0 100644 --- a/app/constants/sidebar.js +++ b/app/constants/sidebar.js @@ -1,28 +1,57 @@ // @flow +import DashboardIcon from '../assets/images/dashboard_icon.svg'; +import DashboardIconActive from '../assets/images/dashboard_icon_active.svg'; +import ConsoleIcon from '../assets/images/console_icon.svg'; +import ConsoleIconActive from '../assets/images/console_icon_active.svg'; +import SendIcon from '../assets/images/send_icon.svg'; +import SendIconActive from '../assets/images/send_icon_active.svg'; +import ReceiveIcon from '../assets/images/receive_icon.svg'; +import ReceiveIconActive from '../assets/images/receive_icon_active.svg'; +import TransactionsIcon from '../assets/images/transactions_icon.svg'; +import TransactionsIconActive from '../assets/images/transactions_icon_active.svg'; +import SettingsIcon from '../assets/images/settings_icon.svg'; +import SettingsIconActive from '../assets/images/settings_icon_active.svg'; import { - DASHBOARD_ROUTE, SEND_ROUTE, RECEIVE_ROUTE, SETTINGS_ROUTE, CONSOLE_ROUTE, + DASHBOARD_ROUTE, + SEND_ROUTE, + RECEIVE_ROUTE, + SETTINGS_ROUTE, + CONSOLE_ROUTE, + TRANSACTIONS_ROUTE, } from './routes'; export const MENU_OPTIONS = [ { label: 'Dashboard', route: DASHBOARD_ROUTE, + // eslint-disable-next-line + icon: (isActive: boolean) => isActive ? DashboardIconActive : DashboardIcon, }, { label: 'Send', route: SEND_ROUTE, + icon: (isActive: boolean) => (isActive ? SendIconActive : SendIcon), }, { label: 'Receive', route: RECEIVE_ROUTE, + icon: (isActive: boolean) => (isActive ? ReceiveIconActive : ReceiveIcon), }, { - label: 'Console', - route: CONSOLE_ROUTE, + label: 'Transactions', + route: TRANSACTIONS_ROUTE, + // eslint-disable-next-line + icon: (isActive: boolean) => isActive ? TransactionsIconActive : TransactionsIcon, }, { label: 'Settings', route: SETTINGS_ROUTE, + icon: (isActive: boolean) => (isActive ? SettingsIconActive : SettingsIcon), + }, + { + label: 'Console', + route: CONSOLE_ROUTE, + icon: (isActive: boolean) => (isActive ? ConsoleIconActive : ConsoleIcon), }, ]; diff --git a/app/containers/dashboard.js b/app/containers/dashboard.js index 7c8c2e3..6b94c6f 100644 --- a/app/containers/dashboard.js +++ b/app/containers/dashboard.js @@ -2,9 +2,17 @@ import { connect } from 'react-redux'; import eres from 'eres'; +import flow from 'lodash.flow'; +import groupBy from 'lodash.groupby'; +import dateFns from 'date-fns'; import { DashboardView } from '../views/dashboard'; import rpc from '../../services/api'; -import { loadWalletSummary, loadWalletSummarySuccess, loadWalletSummaryError } from '../redux/modules/wallet'; +import store from '../../config/electron-store'; +import { + loadWalletSummary, + loadWalletSummarySuccess, + loadWalletSummaryError, +} from '../redux/modules/wallet'; import type { AppState } from '../types/app-state'; import type { Dispatch } from '../types/redux'; @@ -15,8 +23,9 @@ const mapStateToProps = ({ walletSummary }: AppState) => ({ transparent: walletSummary.transparent, error: walletSummary.error, isLoading: walletSummary.isLoading, - dollarValue: walletSummary.dollarValue, + zecPrice: walletSummary.zecPrice, addresses: walletSummary.addresses, + transactions: walletSummary.transactions, }); const mapDispatchToProps = (dispatch: Dispatch) => ({ @@ -29,14 +38,35 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ const [addressesErr, addresses] = await eres(rpc.z_listaddresses()); + // eslint-disable-next-line if (addressesErr) return dispatch(loadWalletSummaryError({ error: addressesErr.message })); + const [transactionsErr, transactions = []] = await eres( + rpc.listtransactions(), + ); + + if (transactionsErr) { + return dispatch( + loadWalletSummaryError({ error: transactionsErr.message }), + ); + } + dispatch( loadWalletSummarySuccess({ transparent: walletSummary.transparent, total: walletSummary.total, shielded: walletSummary.private, addresses, + transactions: flow([ + arr => arr.map(transaction => ({ + type: transaction.category, + date: new Date(transaction.time * 1000).toISOString(), + address: transaction.address, + amount: Math.abs(transaction.amount), + })), + arr => groupBy(arr, obj => dateFns.format(obj.date, 'MMM DD, YYYY')), + ])(transactions), + zecPrice: store.get('ZEC_DOLLAR_PRICE'), }), ); }, diff --git a/app/containers/sidebar.js b/app/containers/sidebar.js index d4dbc21..7fdd70b 100644 --- a/app/containers/sidebar.js +++ b/app/containers/sidebar.js @@ -1,17 +1,5 @@ // @flow -import { connect } from 'react-redux'; import { SidebarComponent } from '../components/sidebar'; -const mapStateToProps = (state: Object) => ({ - todos: state.todos, -}); - -// const mapDispatchToProps = (dispatch: Dispatch) => ({ -// addTodo: text => dispatch(addTodo(text)), -// }); - -export const SidebarContainer = connect( - mapStateToProps, - // mapDispatchToProps, -)(SidebarComponent); +export const SidebarContainer = SidebarComponent; diff --git a/app/redux/create.js b/app/redux/create.js index e2bf0a2..8e06402 100644 --- a/app/redux/create.js +++ b/app/redux/create.js @@ -9,7 +9,9 @@ import { createRootReducer } from './modules/reducer'; export const history = createBrowserHistory(); -const shouldEnableDevTools = (process.env.NODE_ENV !== 'production' || process.env.NODE_ENV !== 'staging') && window.devToolsExtension; +const shouldEnableDevTools = (process.env.NODE_ENV !== 'production' + || process.env.NODE_ENV !== 'staging') + && window.devToolsExtension; export const configureStore = (initialState: Object) => { const middleware = applyMiddleware(thunk, routerMiddleware(history)); diff --git a/app/redux/modules/wallet.js b/app/redux/modules/wallet.js index 53034d3..aec93d9 100644 --- a/app/redux/modules/wallet.js +++ b/app/redux/modules/wallet.js @@ -1,5 +1,6 @@ // @flow import type { Action } from '../../types/redux'; +import type { Transaction } from '../../components/transaction-item'; // Actions export const LOAD_WALLET_SUMMARY = 'LOAD_WALLET_SUMMARY'; @@ -17,11 +18,15 @@ export const loadWalletSummarySuccess = ({ shielded, transparent, addresses, + transactions, + zecPrice, }: { total: number, shielded: number, transparent: number, addresses: string[], + transactions: { [day: string]: Transaction[] }, + zecPrice: number, }) => ({ type: LOAD_WALLET_SUMMARY_SUCCESS, payload: { @@ -29,6 +34,8 @@ export const loadWalletSummarySuccess = ({ shielded, transparent, addresses, + transactions, + zecPrice, }, }); @@ -43,8 +50,9 @@ export type State = { transparent: number, error: string | null, isLoading: boolean, - dollarValue: number, + zecPrice: number, addresses: [], + transactions: { [day: string]: Transaction[] }, }; const initialState = { @@ -53,8 +61,9 @@ const initialState = { transparent: 0, error: null, isLoading: false, - dollarValue: 0, + zecPrice: 0, addresses: [], + transactions: {}, }; export default (state: State = initialState, action: Action) => { diff --git a/app/router/router.js b/app/router/router.js index 626c179..d3eaaff 100644 --- a/app/router/router.js +++ b/app/router/router.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Route, Switch, type Location } from 'react-router-dom'; import styled from 'styled-components'; import { ScrollTopComponent } from './scroll-top'; @@ -13,32 +13,56 @@ import { SettingsView } from '../views/settings'; import { NotFoundView } from '../views/not-found'; import { ConsoleView } from '../views/console'; import { LayoutComponent } from '../components/layout'; +import { HeaderComponent } from '../components/header'; import { - DASHBOARD_ROUTE, SEND_ROUTE, RECEIVE_ROUTE, SETTINGS_ROUTE, CONSOLE_ROUTE, + DASHBOARD_ROUTE, + SEND_ROUTE, + RECEIVE_ROUTE, + SETTINGS_ROUTE, + CONSOLE_ROUTE, } from '../constants/routes'; -const Wrapper = styled.div` +const FullWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const ContentWrapper = styled.div` display: flex; flex-direction: row; width: 100vw; `; -export const RouterComponent = () => ( - <Wrapper> - <SidebarContainer /> - <ScrollTopComponent> - {/* $FlowFixMe */} - <LayoutComponent> - <Switch> - <Route exact path={DASHBOARD_ROUTE} component={DashboardContainer} /> - <Route path={SEND_ROUTE} component={SendView} /> - <Route path={RECEIVE_ROUTE} component={ReceiveView} /> - <Route path={SETTINGS_ROUTE} component={SettingsView} /> - <Route path={CONSOLE_ROUTE} component={ConsoleView} /> - <Route component={NotFoundView} /> - </Switch> - </LayoutComponent> - </ScrollTopComponent> - </Wrapper> +const getTitle = (path: string) => { + if (path === '/') return 'Dashboard'; + + return path.replace('/', ''); +}; + +export const RouterComponent = ({ location }: { location: Location }) => ( + <FullWrapper> + <HeaderComponent title={getTitle(location.pathname)} /> + <ContentWrapper> + <SidebarContainer location={location} /> + <ScrollTopComponent> + {/* $FlowFixMe */} + <LayoutComponent> + <Switch> + <Route + exact + path={DASHBOARD_ROUTE} + component={DashboardContainer} + /> + <Route path={SEND_ROUTE} component={SendView} /> + <Route path={RECEIVE_ROUTE} component={ReceiveView} /> + <Route path={SETTINGS_ROUTE} component={SettingsView} /> + <Route path={CONSOLE_ROUTE} component={ConsoleView} /> + <Route component={NotFoundView} /> + </Switch> + </LayoutComponent> + </ScrollTopComponent> + </ContentWrapper> + </FullWrapper> ); diff --git a/app/theme.js b/app/theme.js index 88391d0..dc9dcd8 100644 --- a/app/theme.js +++ b/app/theme.js @@ -9,15 +9,32 @@ import { DARK } from './constants/themes'; const darkOne = '#7B00DD'; const lightOne = '#ffffff'; -const brandOne = '#624cda'; -const brandTwo = '#a6ede2'; +const brandOne = '#000'; +const brandTwo = '#3B3B3F'; const activeItem = '#F5CB00'; const text = '#FFF'; const cardBackgroundColor = '#000'; +const sidebarLogoGradientBegin = '#F4B728'; +const sidebarLogoGradientEnd = '#FFE240'; +const sidebarHoveredItem = '#1C1C1C'; +const sidebarHoveredItemLabel = '#969696'; +const background = '#212124'; +const transactionSent = '#FF6C6C'; +const transactionReceived = '#6AEAC0'; +const transactionsDate = '#777777'; const appTheme = { mode: DARK, fontFamily: 'PT Sans', + fontWeight: { + bold: 700, + default: 400, + }, + fontSize: { + title: 1.25, + text: 0.84375, + zecValueBase: 1.125, + }, colors: { primary: theme('mode', { light: lightOne, @@ -29,15 +46,24 @@ const appTheme = { }), sidebarBg: brandOne, sidebarItem: brandTwo, - sidebarItemActive: lightOne, + sidebarItemActive: activeItem, + sidebarHoveredItem, + sidebarHoveredItemLabel, cardBackgroundColor, text, activeItem, + inactiveItem: brandTwo, + sidebarLogoGradientBegin, + sidebarLogoGradientEnd, + background, + transactionSent, + transactionReceived, + transactionsDate, }, - size: { - title: 18, - paragraph: 12, - }, + sidebarWidth: '200px', + headerHeight: '60px', + layoutPaddingLeft: '50px', + layoutPaddingRight: '45px', }; export const GlobalStyle = createGlobalStyle` diff --git a/app/utils/formatNumber.js b/app/utils/formatNumber.js new file mode 100644 index 0000000..5d96753 --- /dev/null +++ b/app/utils/formatNumber.js @@ -0,0 +1,3 @@ +// @flow + +export default ({ value, append = '' }: { value: number, append?: string }) => `${append}${(value || 0).toLocaleString('de-DE')}`; diff --git a/app/utils/truncateAddress.js b/app/utils/truncateAddress.js new file mode 100644 index 0000000..3a78a3d --- /dev/null +++ b/app/utils/truncateAddress.js @@ -0,0 +1,6 @@ +// @flow + +export default (address: string = '') => `${address.substr(0, 20)}...${address.substr( + address.length - 10, + address.length, +)}`; diff --git a/app/views/console.js b/app/views/console.js index 7b53a4d..bf0d67b 100644 --- a/app/views/console.js +++ b/app/views/console.js @@ -24,10 +24,12 @@ export class ConsoleView extends Component<Props, State> { } render() { + const { log } = this.state; + return ( <div className='dashboard'> - {this.state.log - && this.state.log.split('\n').map(item => ( + {log + && log.split('\n').map(item => ( <Fragment key={`${item.slice(0, 10)}`}> {item} <br /> diff --git a/app/views/dashboard.js b/app/views/dashboard.js index 89fdd6a..a76b495 100644 --- a/app/views/dashboard.js +++ b/app/views/dashboard.js @@ -3,8 +3,11 @@ import React from 'react'; import { WalletSummaryComponent } from '../components/wallet-summary'; +import { TransactionDailyComponent } from '../components/transaction-daily'; import { withDaemonStatusCheck } from '../components/with-daemon-status-check'; +import type { Transaction } from '../components/transaction-item'; + type Props = { getSummary: () => void, total: number, @@ -12,32 +15,56 @@ type Props = { transparent: number, error: string | null, isLoading: boolean, - dollarValue: number, + zecPrice: number, addresses: string[], + transactions: { [day: string]: Transaction[] }, }; export class Dashboard extends React.Component<Props> { componentDidMount() { + /* eslint-disable-next-line */ this.props.getSummary(); } render() { - if (this.props.error) { - return this.props.error; + const { + error, + isLoading, + total, + shielded, + transparent, + zecPrice, + addresses, + transactions, + } = this.props; + + const days = Object.keys(transactions); + + if (error) { + return error; } return ( <div className='dashboard'> - {this.props.isLoading ? ( + {isLoading ? ( 'Loading' ) : ( - <WalletSummaryComponent - total={this.props.total} - shielded={this.props.shielded} - transparent={this.props.transparent} - dollarValue={this.props.dollarValue} - addresses={this.props.addresses} - /> + <div> + <WalletSummaryComponent + total={total} + shielded={shielded} + transparent={transparent} + zecPrice={zecPrice} + addresses={addresses} + /> + {days.map(day => ( + <TransactionDailyComponent + transactionsDate={day} + transactions={transactions[day]} + zecPrice={zecPrice} + /> + ))} + </div> )} </div> ); diff --git a/config/daemon/fetch-windows-params.js b/config/daemon/fetch-windows-params.js index e2e1548..dfba7d9 100644 --- a/config/daemon/fetch-windows-params.js +++ b/config/daemon/fetch-windows-params.js @@ -19,16 +19,36 @@ import log from './logger'; const queue = new Queue({ concurrency: 1, autoStart: false }); -const httpClient = got.extend({ baseUrl: 'https://z.cash/downloads/', retry: 3, useElectronNet: true }); +const httpClient = got.extend({ + baseUrl: 'https://z.cash/downloads/', + retry: 3, + useElectronNet: true, +}); const FILES: Array<{ name: string, hash: string }> = [ - { name: 'sprout-proving.key', hash: '8bc20a7f013b2b58970cddd2e7ea028975c88ae7ceb9259a5344a16bc2c0eef7' }, - { name: 'sprout-verifying.key', hash: '4bd498dae0aacfd8e98dc306338d017d9c08dd0918ead18172bd0aec2fc5df82' }, - { name: 'sapling-spend.params', hash: '8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13' }, - { name: 'sapling-output.params', hash: '2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4' }, - { name: 'sprout-groth16.params', hash: 'b685d700c60328498fbde589c8c7c484c722b788b265b72af448a5bf0ee55b50' }, + { + name: 'sprout-proving.key', + hash: '8bc20a7f013b2b58970cddd2e7ea028975c88ae7ceb9259a5344a16bc2c0eef7', + }, + { + name: 'sprout-verifying.key', + hash: '4bd498dae0aacfd8e98dc306338d017d9c08dd0918ead18172bd0aec2fc5df82', + }, + { + name: 'sapling-spend.params', + hash: '8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13', + }, + { + name: 'sapling-output.params', + hash: '2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4', + }, + { + name: 'sprout-groth16.params', + hash: 'b685d700c60328498fbde589c8c7c484c722b788b265b72af448a5bf0ee55b50', + }, ]; +// eslint-disable-next-line max-len const checkSha256 = (pathToFile: string, expectedHash: string) => new Promise((resolve, reject) => { fs.readFile(pathToFile, (err, file) => { if (err) return reject(new Error(err)); @@ -39,6 +59,7 @@ const checkSha256 = (pathToFile: string, expectedHash: string) => new Promise((r }); }); +// eslint-disable-next-line max-len const downloadFile = ({ file, pathToSave }): Promise<*> => new Promise((resolve, reject) => { log(`Downloading ${file.name}...`); @@ -50,7 +71,9 @@ const downloadFile = ({ file, pathToSave }): Promise<*> => new Promise((resolve, log(`SHA256 validation for file ${file.name} succeeded!`); resolve(file.name); } else { - reject(new Error(`SHA256 validation failed for file: ${file.name}`)); + reject( + new Error(`SHA256 validation failed for file: ${file.name}`), + ); } }); }) @@ -61,7 +84,9 @@ const downloadFile = ({ file, pathToSave }): Promise<*> => new Promise((resolve, let missingDownloadParam = false; export default (): Promise<*> => new Promise((resolve, reject) => { - const firstRunProcess = cp.spawn(path.join(getBinariesPath(), 'win', 'first-run.bat')); + const firstRunProcess = cp.spawn( + path.join(getBinariesPath(), 'win', 'first-run.bat'), + ); firstRunProcess.stdout.on('data', data => log(data.toString())); firstRunProcess.stderr.on('data', data => reject(data.toString())); @@ -70,20 +95,33 @@ export default (): Promise<*> => new Promise((resolve, reject) => { await Promise.all( FILES.map(async (file) => { - const pathToSave = path.join(app.getPath('userData'), '..', 'ZcashParams', file.name); + const pathToSave = path.join( + app.getPath('userData'), + '..', + 'ZcashParams', + file.name, + ); - const [cannotAccess] = await eres(util.promisify(fs.access)(pathToSave, fs.constants.F_OK)); + const [cannotAccess] = await eres( + util.promisify(fs.access)(pathToSave, fs.constants.F_OK), + ); if (cannotAccess) { missingDownloadParam = true; + // eslint-disable-next-line max-len queue.add(() => downloadFile({ file, pathToSave }).then(() => log(`Download ${file.name} finished!`))); } else { const isValid = await checkSha256(pathToSave, file.hash); if (isValid) { log(`${file.name} already is in ${pathToSave}...`); } else { - log(`File: ${file.name} failed in the SHASUM validation, downloading again...`); + log( + `File: ${ + file.name + } failed in the SHASUM validation, downloading again...`, + ); queue.add(() => { + // eslint-disable-next-line max-len downloadFile({ file, pathToSave }).then(() => log(`Download ${file.name} finished!`)); }); } diff --git a/config/daemon/get-binaries-path.js b/config/daemon/get-binaries-path.js index aa827b3..c5b8f0a 100644 --- a/config/daemon/get-binaries-path.js +++ b/config/daemon/get-binaries-path.js @@ -3,5 +3,8 @@ import path from 'path'; /* eslint-disable-next-line import/no-extraneous-dependencies */ import isDev from 'electron-is-dev'; -// $FlowFixMe -export default () => (isDev ? path.join(__dirname, '..', '..', './bin') : path.join(process.resourcesPath, 'bin')); +/* eslint-disable operator-linebreak */ +export default () => (isDev + ? path.join(__dirname, '..', '..', './bin') + : // $FlowFixMe + path.join(process.resourcesPath, 'bin')); diff --git a/config/daemon/run-fetch-params.js b/config/daemon/run-fetch-params.js index fac0e1d..1ef41aa 100644 --- a/config/daemon/run-fetch-params.js +++ b/config/daemon/run-fetch-params.js @@ -8,5 +8,7 @@ import runUnixFetchParams from './fetch-unix-params'; export default (): Promise<*> => { log('Fetching params'); - return os.platform() === 'win32' ? fetchWindowsParams() : runUnixFetchParams(); + return os.platform() === 'win32' + ? fetchWindowsParams() + : runUnixFetchParams(); }; diff --git a/config/daemon/zcashd-child-process.js b/config/daemon/zcashd-child-process.js index af09d67..34779b1 100644 --- a/config/daemon/zcashd-child-process.js +++ b/config/daemon/zcashd-child-process.js @@ -35,13 +35,20 @@ const getDaemonOptions = ({ username, password }) => { `-rpcuser=${username}`, `-rpcpassword=${password}`, ]; - return isDev ? defaultOptions.concat(['-testnet', '-addnode=testnet.z.cash']) : defaultOptions; + return isDev + ? defaultOptions.concat(['-testnet', '-addnode=testnet.z.cash']) + : defaultOptions; }; let resolved = false; +// eslint-disable-next-line const runDaemon: () => Promise<?ChildProcess> = () => new Promise(async (resolve, reject) => { - const processName = path.join(getBinariesPath(), getOsFolder(), getDaemonName()); + const processName = path.join( + getBinariesPath(), + getOsFolder(), + getDaemonName(), + ); const [err] = await eres(fetchParams()); @@ -76,9 +83,13 @@ const runDaemon: () => Promise<?ChildProcess> = () => new Promise(async (resolve store.set('rpcpassword', rpcCredentials.password); } - const childProcess = cp.spawn(processName, getDaemonOptions(rpcCredentials), { - stdio: ['ignore', 'pipe', 'pipe'], - }); + const childProcess = cp.spawn( + processName, + getDaemonOptions(rpcCredentials), + { + stdio: ['ignore', 'pipe', 'pipe'], + }, + ); childProcess.stdout.on('data', (data) => { if (mainWindow) mainWindow.webContents.send('zcashd-log', data.toString()); diff --git a/config/electron.js b/config/electron.js index 3c741d4..5f5816c 100644 --- a/config/electron.js +++ b/config/electron.js @@ -13,6 +13,8 @@ import eres from 'eres'; import { registerDebugShortcut } from '../utils/debug-shortcut'; import runDaemon from './daemon/zcashd-child-process'; import zcashLog from './daemon/logger'; +import getZecPrice from '../services/zec-price'; +import store from './electron-store'; let mainWindow: BrowserWindowType; let updateAvailable: boolean = false; @@ -37,9 +39,9 @@ const createWindow = () => { autoUpdater.on('download-progress', progress => showStatus( /* eslint-disable-next-line max-len */ - `Download speed: ${progress.bytesPerSecond} - Downloaded ${progress.percent}% (${progress.transferred}/${ - progress.total - })`, + `Download speed: ${progress.bytesPerSecond} - Downloaded ${ + progress.percent + }% (${progress.transferred}/${progress.total})`, )); autoUpdater.on('update-downloaded', () => { updateAvailable = true; @@ -58,10 +60,18 @@ const createWindow = () => { }, }); + getZecPrice().then((obj) => { + store.set('ZEC_DOLLAR_PRICE', obj.USD); + }); + mainWindow.setVisibleOnAllWorkspaces(true); registerDebugShortcut(app, mainWindow); - mainWindow.loadURL(isDev ? 'http://0.0.0.0:8080/' : `file://${path.join(__dirname, '../build/index.html')}`); + mainWindow.loadURL( + isDev + ? 'http://0.0.0.0:8080/' + : `file://${path.join(__dirname, '../build/index.html')}`, + ); exports.app = app; exports.mainWindow = mainWindow; diff --git a/package.json b/package.json index 97adb48..9972e63 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "css-loader": "^1.0.1", "docz": "^0.12.13", "docz-plugin-css": "^0.11.0", - "electron": "^3.0.10", "electron-builder": "^20.36.2", "electron-icon-maker": "^0.0.4", "electron-is-dev": "^1.0.1", @@ -88,17 +87,20 @@ "@babel/register": "^7.0.0", "autoprefixer": "^9.3.1", "connected-react-router": "^5.0.1", + "date-fns": "^1.30.1", + "electron": "^3.0.10", "electron-store": "^2.0.0", "eres": "^1.0.1", "got": "^9.3.2", "history": "^4.7.2", + "lodash.flow": "^3.5.0", + "lodash.groupby": "^4.6.0", "p-queue": "^3.0.0", "process-exists": "^3.1.0", "qrcode.react": "^0.8.0", "react": "^16.6.0", "react-click-outside": "tj/react-click-outside", "react-dom": "^16.6.0", - "react-icons": "^3.2.2", "react-popover": "^0.5.10", "react-redux": "^5.0.7", "react-router-dom": "^4.2.2", diff --git a/services/utils.js b/services/utils.js index 356a15b..20e7e09 100644 --- a/services/utils.js +++ b/services/utils.js @@ -745,8 +745,8 @@ export type APIMethods = { z_exportwallet: (filename: string) => Promise<string>, z_getbalance: (address: string, minconf?: number) => Promise<number>, z_getnewaddress: (type: string) => Promise<string>, - z_getoperationresult: (operationid: string) => Promise<Object[]>, - z_getoperationstatus: (operationid: string) => Promise<Object[]>, + z_getoperationresult: (operationid?: string[]) => Promise<Object[]>, + z_getoperationstatus: (operationid?: string[]) => Promise<Object[]>, z_gettotalbalance: ( minconf?: number, includeWatchonly?: boolean, diff --git a/services/zec-price.js b/services/zec-price.js new file mode 100644 index 0000000..7a92c0a --- /dev/null +++ b/services/zec-price.js @@ -0,0 +1,31 @@ +// @flow +import { net } from 'electron'; + +type Payload = { + [currency: string]: number, +}; + +/** + WARNING: + Just a super fast way to get the zec price +*/ +export default (currencies: string[] = ['USD']): Promise<Payload> => new Promise((resolve, reject) => { + const ENDPOINT = `https://min-api.cryptocompare.com/data/price?fsym=ZEC&tsyms=${currencies.join( + ',', + )}&api_key=b6162b068ff9f8fe2872070b791146b06d186e83d5e52e49dcaa42ef8d1d3875`; + + const request = net.request(ENDPOINT); + request.on('response', (response) => { + let data = ''; + /* eslint-disable-next-line no-return-assign */ + response.on('data', chunk => (data += chunk)); + response.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (err) { + reject(err); + } + }); + }); + request.end(); +}); diff --git a/yarn.lock b/yarn.lock index f113ea4..9ff9f73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4080,6 +4080,11 @@ date-fns@^1.23.0: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw== +date-fns@^1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" + integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== + date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -8866,11 +8871,21 @@ lodash.flattendepth@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.flattendepth/-/lodash.flattendepth-4.7.0.tgz#b4d2d14fc7d9c53deb96642eb616fff22a60932f" integrity sha1-tNLRT8fZxT3rlmQuthb/8ipgky8= +lodash.flow@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" + integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o= + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.groupby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1" + integrity sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E= + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -11653,11 +11668,6 @@ react-hot-loader@4.3.6: react-lifecycles-compat "^3.0.4" shallowequal "^1.0.2" -react-icons@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-3.2.2.tgz#7b1dc795004a119a35175e62ecef91e5c9c90962" - integrity sha512-0DiXTcECUNoMvze2zcFwYYrHAFgQ2rBM0+HIHfaECKRfE1NZYwfuUSbwyg9s1uVPuy7o/KqQKSwBGMWQFvdrhQ== - react-imported-component@^5.2.3: version "5.2.3" resolved "https://registry.yarnpkg.com/react-imported-component/-/react-imported-component-5.2.3.tgz#7771b72d2814c5ba2839489e2cdc5d033637c045"