diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dcf41de --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +ZEC_PRICE_API_KEY= \ No newline at end of file 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/.gitignore b/.gitignore index 1fa8533..564f475 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ flow-coverage build .docz coverage -flow-typed \ No newline at end of file +flow-typed +.env \ No newline at end of file diff --git a/__tests__/actions/Todo.test.js b/__tests__/actions/Todo.test.js deleted file mode 100644 index 7b84672..0000000 --- a/__tests__/actions/Todo.test.js +++ /dev/null @@ -1,29 +0,0 @@ -// @flow - -import configureStore from 'redux-mock-store'; - -import { ADD_TODO, addTodo } from '../../app/redux/modules/todo'; - -const store = configureStore()(); - -describe('Todo Actions', () => { - beforeEach(() => store.clearActions()); - - test('should create an action to add a new todo', () => { - const text = 'Hello World!'; - - store.dispatch(addTodo(text)); - - expect(store.getActions()[0]).toEqual( - expect.objectContaining({ - type: ADD_TODO, - payload: { - text, - id: expect.any(String), - editing: false, - createdAt: expect.any(Number), - }, - }), - ); - }); -}); diff --git a/__tests__/actions/wallet-summary.test.js b/__tests__/actions/wallet-summary.test.js new file mode 100644 index 0000000..0df55d5 --- /dev/null +++ b/__tests__/actions/wallet-summary.test.js @@ -0,0 +1,63 @@ +// @flow + +import configureStore from 'redux-mock-store'; + +import { + LOAD_WALLET_SUMMARY, + LOAD_WALLET_SUMMARY_SUCCESS, + LOAD_WALLET_SUMMARY_ERROR, + loadWalletSummary, + loadWalletSummarySuccess, + loadWalletSummaryError, +} from '../../app/redux/modules/wallet'; + +const store = configureStore()(); + +describe('WalletSummary Actions', () => { + beforeEach(() => store.clearActions()); + + test('should create an action to load wallet summary', () => { + store.dispatch(loadWalletSummary()); + + expect(store.getActions()[0]).toEqual( + expect.objectContaining({ + type: LOAD_WALLET_SUMMARY, + }), + ); + }); + + test('should create an action to load wallet summary', () => { + const payload = { + total: 5000, + transparent: 5000, + shielded: 5000, + addresses: [], + transactions: {}, + zecPrice: 50, + }; + + store.dispatch(loadWalletSummarySuccess(payload)); + + expect(store.getActions()[0]).toEqual( + expect.objectContaining({ + type: LOAD_WALLET_SUMMARY_SUCCESS, + payload, + }), + ); + }); + + test('should create an action to load wallet summary with error', () => { + const payload = { + error: 'Something went wrong!', + }; + + store.dispatch(loadWalletSummaryError(payload)); + + expect(store.getActions()[0]).toEqual( + expect.objectContaining({ + type: LOAD_WALLET_SUMMARY_ERROR, + 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__/e2e/startup.test.js b/__tests__/e2e/startup.test.js index d4a3de0..803ac51 100644 --- a/__tests__/e2e/startup.test.js +++ b/__tests__/e2e/startup.test.js @@ -2,7 +2,7 @@ import { getApp } from '../setup/utils'; -describe('ZCash', () => { +describe('Zcash', () => { const app = getApp(); beforeEach(() => app.start()); @@ -11,7 +11,8 @@ describe('ZCash', () => { }); test('should open the window', () => { - app.client.getWindowCount() + app.client + .getWindowCount() .then((count: number) => expect(count).toEqual(1)); }); }); diff --git a/__tests__/reducers/Todo.test.js b/__tests__/reducers/Todo.test.js deleted file mode 100644 index 560eb5c..0000000 --- a/__tests__/reducers/Todo.test.js +++ /dev/null @@ -1,30 +0,0 @@ -// @flow - -import todoReducer, { ADD_TODO } from '../../app/redux/modules/todo'; - -describe('Todo Reducer', () => { - test('should return the valid initial state', () => { - const initialState = []; - const action = { - type: 'UNKNOWN_ACTION', - payload: {}, - }; - - expect(todoReducer(undefined, action)).toEqual(initialState); - }); - - test('should add a new todo', () => { - const action = { - type: ADD_TODO, - payload: { - id: 'abc123', - text: 'Hello World!', - editing: false, - createdAt: new Date().getTime(), - }, - }; - const expectedState = [action.payload]; - - expect(todoReducer(undefined, action)).toEqual(expectedState); - }); -}); diff --git a/__tests__/reducers/wallet-summary.test.js b/__tests__/reducers/wallet-summary.test.js new file mode 100644 index 0000000..7095f5e --- /dev/null +++ b/__tests__/reducers/wallet-summary.test.js @@ -0,0 +1,80 @@ +// @flow +import walletSummaryReducer, { + LOAD_WALLET_SUMMARY, + LOAD_WALLET_SUMMARY_SUCCESS, + LOAD_WALLET_SUMMARY_ERROR, +} from '../../app/redux/modules/wallet'; + +describe('WalletSummary Reducer', () => { + test('should return the valid initial state', () => { + const initialState = { + total: 0, + shielded: 0, + transparent: 0, + error: null, + isLoading: false, + dollarValue: 0, + }; + const action = { + type: 'UNKNOWN_ACTION', + payload: {}, + }; + + expect(walletSummaryReducer(undefined, action)).toEqual(initialState); + }); + + test('should load the wallet summary', () => { + const action = { + type: LOAD_WALLET_SUMMARY, + payload: {}, + }; + const expectedState = { + total: 0, + shielded: 0, + transparent: 0, + error: null, + isLoading: true, + dollarValue: 0, + }; + + expect(walletSummaryReducer(undefined, action)).toEqual(expectedState); + }); + + test('should load the wallet summary with success', () => { + const action = { + type: LOAD_WALLET_SUMMARY_SUCCESS, + payload: { + total: 1000, + transparent: 1000, + shielded: 1000, + }, + }; + const expectedState = { + ...action.payload, + error: null, + isLoading: false, + dollarValue: 0, + }; + + expect(walletSummaryReducer(undefined, action)).toEqual(expectedState); + }); + + test('should load the wallet summary with error', () => { + const action = { + type: LOAD_WALLET_SUMMARY_ERROR, + payload: { + error: 'Something went wrong', + }, + }; + const expectedState = { + total: 0, + shielded: 0, + transparent: 0, + error: action.payload.error, + isLoading: false, + dollarValue: 0, + }; + + expect(walletSummaryReducer(undefined, action)).toEqual(expectedState); + }); +}); diff --git a/app/app.js b/app/app.js index 157fd9f..82c51e7 100644 --- a/app/app.js +++ b/app/app.js @@ -13,13 +13,15 @@ const store = configureStore({}); export default () => ( - - - - - - - - + + + + + + + + + + ); 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/Button.mdx b/app/components/button.mdx similarity index 100% rename from app/components/Button.mdx rename to app/components/button.mdx 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 new file mode 100644 index 0000000..59c9101 --- /dev/null +++ b/app/components/dropdown.js @@ -0,0 +1,109 @@ +// @flow +import React, { type Node, Component } from 'react'; +import styled from 'styled-components'; +/* eslint-disable import/no-extraneous-dependencies */ +// $FlowFixMe +import { darken } from 'polished'; +import Popover from 'react-popover'; +import ClickOutside from 'react-click-outside'; + +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})`}; + padding: 10px 20px; + border-radius: 10px; + margin-left: -10px; +`; + +const MenuItem = styled.button` + outline: none; + background-color: transparent; + border: none; + border-bottom-style: solid; + border-bottom-color: ${props => props.theme.colors.text}; + border-bottom-width: 1px; + padding: 15px 0; + cursor: pointer; + width: 100%; + text-align: left; + + &:hover { + opacity: 0.9; + } + + &:disabled { + cursor: default; + + &:hover { + opacity: 1; + } + } +`; + +const PopoverWithStyle = styled(Popover)` + & > .Popover-tip { + fill: ${props => props.theme.colors.activeItem}; + } +`; + +type Props = { + renderTrigger: (toggleVisibility: () => void, isOpen: boolean) => Node, + options: Array<{ label: string, onClick: () => void }>, + label?: string | null, +}; + +type State = { + isOpen: boolean, +}; + +export class DropdownComponent extends Component { + state = { + isOpen: false, + }; + + static defaultProps = { + label: null, + }; + + render() { + const { isOpen } = this.state; + const { label, options, renderTrigger } = this.props; + + return ( + this.setState(() => ({ isOpen: false }))} + > + + {label && ( + + + + )} + {options.map(({ label: optionLabel, onClick }) => ( + + + + ))} + + , + ]} + tipSize={7} + > + {renderTrigger( + () => this.setState(state => ({ isOpen: !state.isOpen })), + isOpen, + )} + + ); + } +} diff --git a/app/components/dropdown.mdx b/app/components/dropdown.mdx new file mode 100644 index 0000000..87af250 --- /dev/null +++ b/app/components/dropdown.mdx @@ -0,0 +1,31 @@ +--- +name: DropDown +--- + +import { Playground, PropsTable } from 'docz' + +import { DropdownComponent } from './dropdown.js' +import { DoczWrapper } from '../theme.js' + +# DropDown + + + +## Basic usage + + + + {() => ( +
+ } + /> +
+ )} +
+
diff --git a/app/components/header.js b/app/components/header.js new file mode 100644 index 0000000..4e3589a --- /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/Input.mdx b/app/components/input.mdx similarity index 51% rename from app/components/Input.mdx rename to app/components/input.mdx index 5111f62..985c806 100644 --- a/app/components/Input.mdx +++ b/app/components/input.mdx @@ -14,13 +14,22 @@ import { DoczWrapper } from '../theme.js' ## Text Input <Playground> - <DoczWrapper>{() => <InputComponent inputType="input" value="Hello World!" onChange={console.log} />}</DoczWrapper> + <DoczWrapper> + {() => <InputComponent inputType="input" value="Hello World!" onChange={console.log} />} + </DoczWrapper> </Playground> ## Textarea <Playground> <DoczWrapper> - {() => <InputComponent inputType="textarea" value="I'm ZCash Electron Wallet" onChange={console.log} rows={10} />} + {() => ( + <InputComponent + inputType="textarea" + value="I'm Zcash Electron Wallet" + onChange={console.log} + rows={10} + /> + )} </DoczWrapper> </Playground> diff --git a/app/components/layout.js b/app/components/layout.js index ed67ae4..47c0b28 100644 --- a/app/components/layout.js +++ b/app/components/layout.js @@ -1,17 +1,15 @@ // @flow - import React from 'react'; import styled from 'styled-components'; const Layout = styled.div` display: flex; flex-direction: column; - position: absolute; - width: calc(100vw - 200px); - left: 200px; - top: 0; - 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/QRCode.mdx b/app/components/qrcode.mdx similarity index 100% rename from app/components/QRCode.mdx rename to app/components/qrcode.mdx diff --git a/app/components/row.js b/app/components/row.js new file mode 100644 index 0000000..83a483c --- /dev/null +++ b/app/components/row.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: row; + align-items: ${props => props.alignItems}; + justify-content: ${props => props.justifyContent}; +`; + +type Props = { + alignItems?: string, + justifyContent?: string, + className?: string, + children: Node, +}; + +export const RowComponent = ({ children, ...props }: Props) => ( + <Flex {...props}>{React.Children.map(children, ch => ch)}</Flex> +); + +RowComponent.defaultProps = { + alignItems: 'flex-start', + justifyContent: 'flex-start', + className: '', +}; diff --git a/app/components/sidebar.js b/app/components/sidebar.js index 9307af1..4295d52 100644 --- a/app/components/sidebar.js +++ b/app/components/sidebar.js @@ -2,46 +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; - position: absolute; - left: 0; - top: 0; - 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/Sidebar.mdx b/app/components/sidebar.mdx similarity index 100% rename from app/components/Sidebar.mdx rename to app/components/sidebar.mdx diff --git a/app/components/text.js b/app/components/text.js new file mode 100644 index 0000000..2e110fc --- /dev/null +++ b/app/components/text.js @@ -0,0 +1,53 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; + +import theme from '../theme'; + +const Text = styled.p` + font-family: ${props => props.theme.fontFamily}; + font-size: ${props => props.size}; + color: ${props => props.color || props.theme.colors.text}; + margin: 0; + padding: 0; + font-weight: ${props => (props.isBold + ? props.theme.fontWeight.bold + : props.theme.fontWeight.default)}; + text-align: ${props => props.align}; +`; + +type Props = { + value: string, + isBold?: boolean, + color?: string, + className?: string, + size?: string | number, + align?: string, +}; + +export const TextComponent = ({ + value, + isBold, + color, + className, + size, + align, +}: Props) => ( + <Text + className={className} + isBold={isBold} + color={color} + size={`${String(size)}em`} + align={align} + > + {value} + </Text> +); + +TextComponent.defaultProps = { + className: '', + isBold: false, + color: theme.colors.text, + size: theme.fontSize.text, + align: 'left', +}; diff --git a/app/components/todo-edit-input.js b/app/components/todo-edit-input.js deleted file mode 100644 index dcc1122..0000000 --- a/app/components/todo-edit-input.js +++ /dev/null @@ -1,68 +0,0 @@ -// @flow - -import React, { Component } from 'react'; -import type { TodoType } from '../types/todo'; - -type Props = { - updateTodo: Function, - todo: TodoType, - cancelUpdateTodo: Function, -}; - -type State = { - value: string, -}; - -export default class TodoEditInput extends Component<Props, State> { - constructor(props: Props) { - super(props); - - this.state = { - value: props.todo.text || '', - }; - } - - handleSubmit = (event: SyntheticInputEvent<HTMLInputElement>, id: string) => { - const { value } = this.state; - const { updateTodo } = this.props; - const trimValue = value.trim(); - - event.preventDefault(); - - if (trimValue !== '') { - updateTodo(id, trimValue); - this.setState({ value: '' }); - } - }; - - handleCancel = (id: string) => { - const { cancelUpdateTodo } = this.props; - cancelUpdateTodo(id); - }; - - handleInputChange = (event: SyntheticInputEvent<HTMLInputElement>) => { - const { - target: { value }, - } = event; - this.setState({ value }); - }; - - render() { - const { value } = this.state; - const { todo } = this.props; - - return ( - <div className='todo-item__view todo-item__view--edit'> - <form className='todo-item__input' onSubmit={e => this.handleSubmit(e, todo.id)}> - <input value={value} onChange={this.handleInputChange} className='todo-item__input-field' autoFocus /> - <button type='submit' className='todo-item__input-button'> - Update - </button> - </form> - <button type='button' className='todo-item__input-cancel' onClick={() => this.handleCancel(todo.id)}> - Cancel - </button> - </div> - ); - } -} diff --git a/app/components/todo-input.js b/app/components/todo-input.js deleted file mode 100644 index ae71f0e..0000000 --- a/app/components/todo-input.js +++ /dev/null @@ -1,50 +0,0 @@ -// @flow - -import React, { Component } from 'react'; - -type Props = { - addTodo: Function, -}; - -type State = { - value: string, -}; - -export default class TodoInput extends Component<Props, State> { - state = { - value: '', - }; - - handleSubmit = (event: SyntheticInputEvent<HTMLInputElement>) => { - const { value } = this.state; - const { addTodo } = this.props; - const trimValue = value.trim(); - - event.preventDefault(); - - if (trimValue !== '') { - addTodo(trimValue); - this.setState({ value: '' }); - } - }; - - handleInputChange = (event: SyntheticInputEvent<HTMLInputElement>) => { - const { - target: { value }, - } = event; - this.setState({ value }); - }; - - render() { - const { value } = this.state; - - return ( - <form className='todo__input' onSubmit={this.handleSubmit}> - <input value={value} onChange={this.handleInputChange} className='todo__input-field' /> - <button type='submit' className='todo__input-button'> - Submit - </button> - </form> - ); - } -} diff --git a/app/components/todo-list-item.js b/app/components/todo-list-item.js deleted file mode 100644 index d668f40..0000000 --- a/app/components/todo-list-item.js +++ /dev/null @@ -1,55 +0,0 @@ -// @flow - -import React, { PureComponent } from 'react'; -import type { TodoType } from '../types/todo'; - -type Props = { - todo: TodoType, - deleteTodo: Function, - toggleEdit: Function, -}; - -export default class TodoListItem extends PureComponent<Props> { - handleDelete = (id: string) => { - if (!id) return; - - const { deleteTodo } = this.props; - deleteTodo(id); - }; - - handleEditToggle = (id: string) => { - if (!id) return; - - const { toggleEdit } = this.props; - toggleEdit(id); - }; - - render() { - const { todo } = this.props; - - return ( - <div className='todo-item__view todo-item__view--view'> - <span className='todo-item__text'>{todo.text}</span> - <div className='todo-item__buttons'> - <button type='button' onClick={() => this.handleEditToggle(todo.id)} className='todo-item__button'> - <svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 528.899 528.899'> - <path - className='todo-item__svg' - d='M328.883 89.125l107.59 107.589-272.34 272.34L56.604 361.465l272.279-272.34zm189.23-25.948l-47.981-47.981c-18.543-18.543-48.653-18.543-67.259 0l-45.961 45.961 107.59 107.59 53.611-53.611c14.382-14.383 14.382-37.577 0-51.959zM.3 512.69c-1.958 8.812 5.998 16.708 14.811 14.565l119.891-29.069L27.473 390.597.3 512.69z' - /> - </svg> - </button> - <button type='button' onClick={() => this.handleDelete(todo.id)} className='todo-item__button'> - <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 512 512'> - <path - className='todo-item__svg' - fill='#1D1D1B' - d='M459.232 60.687h-71.955c-1.121-17.642-15.631-31.657-33.553-31.657H161.669c-17.921 0-32.441 14.015-33.553 31.657H64.579c-18.647 0-33.767 15.12-33.767 33.768v8.442c0 18.648 15.12 33.768 33.767 33.768h21.04v342.113c0 13.784 11.179 24.963 24.963 24.963h308.996c13.784 0 24.964-11.179 24.964-24.963V136.665h14.691c18.663 0 33.768-15.12 33.768-33.768v-8.442c-.001-18.648-15.105-33.768-33.769-33.768zM196.674 443.725c0 12.58-10.197 22.803-22.802 22.803-12.598 0-22.803-10.223-22.803-22.803v-284.9c0-12.597 10.205-22.802 22.803-22.802 12.605 0 22.802 10.206 22.802 22.802v284.9zm91.213 0c0 12.58-10.205 22.803-22.803 22.803s-22.803-10.223-22.803-22.803v-284.9c0-12.597 10.205-22.802 22.803-22.802s22.803 10.206 22.803 22.802v284.9zm91.212 0c0 12.58-10.205 22.803-22.803 22.803-12.613 0-22.803-10.223-22.803-22.803v-284.9c0-12.597 10.189-22.802 22.803-22.802 12.598 0 22.803 10.206 22.803 22.802v284.9z' - /> - </svg> - </button> - </div> - </div> - ); - } -} diff --git a/app/components/todo-list.js b/app/components/todo-list.js deleted file mode 100644 index 244a6c2..0000000 --- a/app/components/todo-list.js +++ /dev/null @@ -1,52 +0,0 @@ -// @flow - -import React, { PureComponent } from 'react'; -import TodoEditInput from './todo-edit-input'; -import TodoListItem from './todo-list-item'; -import type { TodoType } from '../types/todo'; - -type Props = { - todos: Array<TodoType>, - deleteTodo: Function, - toggleEdit: Function, - updateTodo: Function, - cancelUpdateTodo: Function, -}; - -export default class TodoList extends PureComponent<Props> { - renderTodoView = (todo: TodoType) => { - const { deleteTodo, toggleEdit } = this.props; - - return <TodoListItem todo={todo} deleteTodo={deleteTodo} toggleEdit={toggleEdit} />; - }; - - renderEditView = (todo: TodoType) => { - const { updateTodo, cancelUpdateTodo } = this.props; - - return <TodoEditInput todo={todo} updateTodo={updateTodo} cancelUpdateTodo={cancelUpdateTodo} />; - }; - - renderList = () => { - const { todos } = this.props; - const sortTodosByTime = todos.sort((a, b) => b.createdAt - a.createdAt); - - return ( - <ul className='todo__list'> - {sortTodosByTime.map(todo => ( - <li key={todo.id} className='todo__list-item todo-item'> - {todo.editing ? this.renderEditView(todo) : this.renderTodoView(todo)} - </li> - ))} - </ul> - ); - }; - - renderEmptyState = () => <p className='todo__list todo__list--empty'>No todos right now</p>; - - render() { - const { todos } = this.props; - const hasTodos = todos.length; - - return hasTodos ? this.renderList() : this.renderEmptyState(); - } -} 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 new file mode 100644 index 0000000..63816e0 --- /dev/null +++ b/app/components/wallet-summary.js @@ -0,0 +1,146 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; + +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'; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + background-color: ${props => props.theme.colors.cardBackgroundColor}; + border-radius: 5px; + padding: 37px 45px; + margin-top: 20px; + position: relative; +`; + +const AllAddresses = styled(TextComponent)` + margin-bottom: 2.5px; + font-size: ${props => `${props.theme.fontSize.text}em`}; +`; + +const ValueBox = styled.div` + margin-bottom: 15px; + margin-right: 25px; +`; + +const Label = styled(TextComponent)` + margin-top: 10px; + margin-bottom: 5px; + margin-left: -7.5px; +`; + +const USDValue = styled(TextComponent)` + opacity: 0.5; +`; + +const ShieldedValue = styled(Label)` + color: ${props => props.theme.colors.activeItem}; +`; + +const SeeMoreButton = styled.button` + display: flex; + 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.inactiveItem)}; + background-color: transparent; + padding: 5px; + cursor: pointer; + position: absolute; + right: 10px; + top: 10px; + + &:hover { + border-color: ${props => props.theme.colors.activeItem}; + } +`; + +const SeeMoreIcon = styled.img` + width: 25px; + height: 25px; +`; + +type Props = { + total: number, + shielded: number, + transparent: number, + zecPrice: number, + addresses: string[], +}; + +export const WalletSummaryComponent = ({ + total, + shielded, + transparent, + zecPrice, + addresses, +}: Props) => ( + <Wrapper> + <DropdownComponent + label='All Addresses' + renderTrigger={(toggleVisibility, isOpen) => ( + <SeeMoreButton onClick={toggleVisibility} isOpen={isOpen}> + <SeeMoreIcon src={MenuIcon} alt='Menu Icon' /> + </SeeMoreButton> + )} + options={addresses.map(addr => ({ label: addr, onClick: x => x }))} + /> + <AllAddresses value='ALL ADDRESSES' isBold /> + <ValueBox> + <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={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={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 new file mode 100644 index 0000000..abe53de --- /dev/null +++ b/app/components/wallet-summary.mdx @@ -0,0 +1,30 @@ +--- +name: Wallet Summary +--- + +import { Playground, PropsTable } from 'docz' + +import { WalletSummaryComponent } from './wallet-summary.js' +import { DoczWrapper } from '../theme.js' + +# Wallet Summary + +<PropsTable of={WalletSummaryComponent} /> + +## Basic usage + +<Playground> + <DoczWrapper> + {() => ( + <div style={{ width: '700px' }}> + <WalletSummaryComponent + total={5000} + shielded={2500} + transparent={2500} + dollarValue={56} + addresses={['12345678asdaas9', '98asdasd765asd4sad321']} + /> + </div> + )} + </DoczWrapper> +</Playground> diff --git a/app/components/with-daemon-status-check.js b/app/components/with-daemon-status-check.js new file mode 100644 index 0000000..e587b7d --- /dev/null +++ b/app/components/with-daemon-status-check.js @@ -0,0 +1,56 @@ +// @flow +import React, { type ComponentType, Component } from 'react'; + +import rpc from '../../services/api'; + +type State = { + isRunning: boolean, +}; + +type Props = {}; + +/* eslint-disable max-len */ +export const withDaemonStatusCheck = <PassedProps: {}>( + WrappedComponent: ComponentType<PassedProps>, +): ComponentType<$Diff<PassedProps, Props>> => class extends Component<PassedProps, State> { + timer: ?IntervalID = null; + + state = { + isRunning: false, + }; + + componentDidMount() { + this.runTest(); + + this.timer = setInterval(this.runTest, 2000); + } + + componentWillUnmount() { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + runTest = () => { + rpc.getinfo().then((response) => { + if (response) { + this.setState(() => ({ isRunning: true })); + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + }); + }; + + render() { + const { isRunning } = this.state; + + if (isRunning) { + return <WrappedComponent {...this.props} {...this.state} />; + } + + return 'Daemon is starting...'; + } + }; diff --git a/app/components/zcash-logo.js b/app/components/zcash-logo.js new file mode 100644 index 0000000..32ed582 --- /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 new file mode 100644 index 0000000..6b94c6f --- /dev/null +++ b/app/containers/dashboard.js @@ -0,0 +1,78 @@ +// @flow + +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 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'; + +const mapStateToProps = ({ walletSummary }: AppState) => ({ + total: walletSummary.total, + shielded: walletSummary.shielded, + transparent: walletSummary.transparent, + error: walletSummary.error, + isLoading: walletSummary.isLoading, + zecPrice: walletSummary.zecPrice, + addresses: walletSummary.addresses, + transactions: walletSummary.transactions, +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + getSummary: async () => { + dispatch(loadWalletSummary()); + + const [err, walletSummary] = await eres(rpc.z_gettotalbalance()); + + if (err) return dispatch(loadWalletSummaryError({ error: err.message })); + + 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'), + }), + ); + }, +}); + +export const DashboardContainer = connect( + mapStateToProps, + mapDispatchToProps, +)(DashboardView); 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/containers/todo.js b/app/containers/todo.js deleted file mode 100644 index 6d32347..0000000 --- a/app/containers/todo.js +++ /dev/null @@ -1,27 +0,0 @@ -// @flow - -import { connect } from 'react-redux'; -import TodoView from '../views/todo'; -import { - addTodo, deleteTodo, toggleEdit, updateTodo, cancelUpdateTodo, -} from '../redux/modules/todo'; - -import type { AppState } from '../types/app-state'; -import type { Dispatch } from '../types/redux'; - -const mapStateToProps = (state: AppState) => ({ - todos: state.todos, -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - addTodo: text => dispatch(addTodo(text)), - deleteTodo: id => dispatch(deleteTodo(id)), - toggleEdit: id => dispatch(toggleEdit(id)), - updateTodo: (id, text) => dispatch(updateTodo(id, text)), - cancelUpdateTodo: id => dispatch(cancelUpdateTodo(id)), -}); - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(TodoView); 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/reducer.js b/app/redux/modules/reducer.js index b5b8c89..87ad85a 100644 --- a/app/redux/modules/reducer.js +++ b/app/redux/modules/reducer.js @@ -4,9 +4,9 @@ import { combineReducers } from 'redux'; import { connectRouter } from 'connected-react-router'; import type { RouterHistory } from 'react-router-dom'; -import todoReducer from './todo'; +import wallet from './wallet'; export const createRootReducer = (history: RouterHistory) => combineReducers({ - todos: todoReducer, + walletSummary: wallet, router: connectRouter(history), }); diff --git a/app/redux/modules/todo.js b/app/redux/modules/todo.js deleted file mode 100644 index fea3072..0000000 --- a/app/redux/modules/todo.js +++ /dev/null @@ -1,78 +0,0 @@ -// @flow - -import UUID from 'uuid/v4'; - -import { getTimestamp } from '../../utils/timestamp'; - -import type { Action } from '../../types/redux'; -import type { TodoType } from '../../types/todo'; - -// Actions -export const ADD_TODO = 'ADD_TODO'; -export const DELETE_TODO = 'DELETE_TODO'; -export const UPDATE_TODO = 'UPDATE_TODO'; -export const TOGGLE_EDIT_TODO = 'TOGGLE_EDIT_TODO'; -export const CANCEL_UPDATE_TODO = 'CANCEL_UPDATE_TODO'; - -// Actions Creators -export const addTodo = (text: string) => ({ - type: ADD_TODO, - payload: { - text, - id: UUID(), - editing: false, - createdAt: getTimestamp(), - }, -}); - -export const cancelUpdateTodo = (id: string) => ({ - type: CANCEL_UPDATE_TODO, - payload: { id }, -}); - -export const deleteTodo = (id: string) => ({ - type: DELETE_TODO, - payload: { id }, -}); - -export const toggleEdit = (id: string) => ({ - type: TOGGLE_EDIT_TODO, - payload: { id }, -}); - -export const updateTodo = (id: string, text: string) => ({ - type: UPDATE_TODO, - payload: { - text, - id, - }, -}); - -// Initial State -const initialState = []; - -// Reducers -export default (state: Array<TodoType> = initialState, action: Action): Array<TodoType> => { - switch (action.type) { - case ADD_TODO: - return [...state, action.payload]; - case DELETE_TODO: - return state.filter((todo: TodoType) => todo.id !== action.payload.id); - case TOGGLE_EDIT_TODO: { - const { id } = action.payload; - return state.map(todo => (todo.id === id ? { ...todo, editing: true } : todo)); - } - case UPDATE_TODO: { - const { id, text } = action.payload; - return state.map(todo => (todo.id === id ? { ...todo, editing: false, text } : todo)); - } - case CANCEL_UPDATE_TODO: { - const { id } = action.payload; - return state.map(todo => (todo.id === id ? { ...todo, editing: false } : todo)); - } - default: - return state; - } -}; - -// SideEffects diff --git a/app/redux/modules/wallet.js b/app/redux/modules/wallet.js new file mode 100644 index 0000000..aec93d9 --- /dev/null +++ b/app/redux/modules/wallet.js @@ -0,0 +1,86 @@ +// @flow +import type { Action } from '../../types/redux'; +import type { Transaction } from '../../components/transaction-item'; + +// Actions +export const LOAD_WALLET_SUMMARY = 'LOAD_WALLET_SUMMARY'; +export const LOAD_WALLET_SUMMARY_SUCCESS = 'LOAD_WALLET_SUMMARY_SUCCESS'; +export const LOAD_WALLET_SUMMARY_ERROR = 'LOAD_WALLET_SUMMARY_ERROR'; + +// Actions Creators +export const loadWalletSummary = () => ({ + type: LOAD_WALLET_SUMMARY, + payload: {}, +}); + +export const loadWalletSummarySuccess = ({ + total, + 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: { + total, + shielded, + transparent, + addresses, + transactions, + zecPrice, + }, +}); + +export const loadWalletSummaryError = ({ error }: { error: string }) => ({ + type: LOAD_WALLET_SUMMARY_ERROR, + payload: { error }, +}); + +export type State = { + total: number, + shielded: number, + transparent: number, + error: string | null, + isLoading: boolean, + zecPrice: number, + addresses: [], + transactions: { [day: string]: Transaction[] }, +}; + +const initialState = { + total: 0, + shielded: 0, + transparent: 0, + error: null, + isLoading: false, + zecPrice: 0, + addresses: [], + transactions: {}, +}; + +export default (state: State = initialState, action: Action) => { + switch (action.type) { + case LOAD_WALLET_SUMMARY: + return { ...state, isLoading: true }; + case LOAD_WALLET_SUMMARY_SUCCESS: + // TODO: Get zec in dollars + return { + ...state, + ...action.payload, + isLoading: false, + error: null, + }; + case LOAD_WALLET_SUMMARY_ERROR: + return { ...state, isLoading: false, error: action.payload.error }; + default: + return state; + } +}; diff --git a/app/router/container.js b/app/router/container.js index e03f647..1c71cb1 100644 --- a/app/router/container.js +++ b/app/router/container.js @@ -1,18 +1,6 @@ // @flow import { compose } from 'redux'; -import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { RouterComponent } from './router'; -import type { AppState } from '../types/app-state'; -const mapStateToProps = (state: AppState) => ({ - todos: state.todos, -}); - -export const Router = compose( - withRouter, - connect( - mapStateToProps, - null, - ), -)(RouterComponent); +export const Router = compose(withRouter)(RouterComponent); diff --git a/app/router/router.js b/app/router/router.js index 99756e5..d3eaaff 100644 --- a/app/router/router.js +++ b/app/router/router.js @@ -1,37 +1,68 @@ // @flow -import React, { Fragment } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import React from 'react'; +import { Route, Switch, type Location } from 'react-router-dom'; +import styled from 'styled-components'; import { ScrollTopComponent } from './scroll-top'; import { SidebarContainer } from '../containers/sidebar'; -import { DashboardView } from '../views/dashboard'; +import { DashboardContainer } from '../containers/dashboard'; import { SendView } from '../views/send'; import { ReceiveView } from '../views/receive'; 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'; -export const RouterComponent = () => ( - <Fragment> - <SidebarContainer /> - <ScrollTopComponent> - {/* $FlowFixMe */} - <LayoutComponent> - <Switch> - <Route exact path={DASHBOARD_ROUTE} component={DashboardView} /> - <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> - </Fragment> +const FullWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const ContentWrapper = styled.div` + display: flex; + flex-direction: row; + width: 100vw; +`; + +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 376d9ac..dc9dcd8 100644 --- a/app/theme.js +++ b/app/theme.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { Fragment } from 'react'; import theme from 'styled-theming'; import { ThemeProvider, createGlobalStyle } from 'styled-components'; // $FlowFixMe @@ -9,12 +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, @@ -26,18 +46,43 @@ const appTheme = { }), sidebarBg: brandOne, sidebarItem: brandTwo, - sidebarItemActive: lightOne, - }, - size: { - title: 18, - paragraph: 12, + sidebarItemActive: activeItem, + sidebarHoveredItem, + sidebarHoveredItemLabel, + cardBackgroundColor, + text, + activeItem, + inactiveItem: brandTwo, + sidebarLogoGradientBegin, + sidebarLogoGradientEnd, + background, + transactionSent, + transactionReceived, + transactionsDate, }, + sidebarWidth: '200px', + headerHeight: '60px', + layoutPaddingLeft: '50px', + layoutPaddingRight: '45px', }; +export const GlobalStyle = createGlobalStyle` + ${normalize()} + + * { + box-sizing: border-box; + } +`; + /* eslint-disable react/prop-types */ // $FlowFixMe -export const DoczWrapper = ({ children }) => <ThemeProvider theme={appTheme}>{children()}</ThemeProvider>; - -export const GlobalStyle = createGlobalStyle`${normalize()}`; +export const DoczWrapper = ({ children }) => ( + <ThemeProvider theme={appTheme}> + <Fragment> + <GlobalStyle /> + {children()} + </Fragment> + </ThemeProvider> +); export default appTheme; diff --git a/app/types/app-state.js b/app/types/app-state.js index 445bc75..684a66c 100644 --- a/app/types/app-state.js +++ b/app/types/app-state.js @@ -1,7 +1,7 @@ // @flow -import type { TodoType } from './todo'; +import type { State as WalletSummaryState } from '../redux/modules/wallet'; export type AppState = { - todos: Array<TodoType>, + walletSummary: WalletSummaryState, }; 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/config/daemon/generate-random-string.js b/app/utils/generate-random-string.js similarity index 100% rename from config/daemon/generate-random-string.js rename to app/utils/generate-random-string.js 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..804ef3d 100644 --- a/app/views/console.js +++ b/app/views/console.js @@ -1,39 +1,54 @@ // @flow import React, { Component, Fragment } from 'react'; -/* eslint-disable-next-line import/no-extraneous-dependencies */ import { ipcRenderer } from 'electron'; +import styled from 'styled-components'; +import generateRandomString from '../utils/generate-random-string'; + +const Wrapper = styled.div` + max-height: 100%; + overflow-y: auto; +`; type Props = {}; type State = { - log: string | null, + log: string, }; export class ConsoleView extends Component<Props, State> { + scrollView = React.createRef(); + state = { - log: null, + log: '', }; componentDidMount() { ipcRenderer.on('zcashd-log', (event, message) => { - this.setState(() => ({ - log: message, + this.setState(state => ({ + log: `${state.log}\n${message}`, })); + + if (this.scrollView && this.scrollView.current) { + // eslint-disable-next-line + this.scrollView.current.scrollTop = this.scrollView.current.scrollHeight; + } }); } render() { + const { log } = this.state; + return ( - <div className='dashboard'> - {this.state.log - && this.state.log.split('\n').map(item => ( - <Fragment key={`${item.slice(0, 10)}`}> + <Wrapper ref={this.scrollView}> + {log + && log.split('\n').map(item => ( + <Fragment key={generateRandomString()}> {item} <br /> </Fragment> ))} - </div> + </Wrapper> ); } } diff --git a/app/views/dashboard.js b/app/views/dashboard.js index eef6379..a76b495 100644 --- a/app/views/dashboard.js +++ b/app/views/dashboard.js @@ -2,4 +2,73 @@ import React from 'react'; -export const DashboardView = () => <div className='dashboard'>dashboard</div>; +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, + shielded: number, + transparent: number, + error: string | null, + isLoading: boolean, + zecPrice: number, + addresses: string[], + transactions: { [day: string]: Transaction[] }, +}; + +export class Dashboard extends React.Component<Props> { + componentDidMount() { + /* eslint-disable-next-line */ + this.props.getSummary(); + } + + render() { + const { + error, + isLoading, + total, + shielded, + transparent, + zecPrice, + addresses, + transactions, + } = this.props; + + const days = Object.keys(transactions); + + if (error) { + return error; + } + + return ( + <div className='dashboard'> + {isLoading ? ( + 'Loading' + ) : ( + <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> + ); + } +} + +export const DashboardView = withDaemonStatusCheck(Dashboard); diff --git a/app/views/todo.js b/app/views/todo.js deleted file mode 100644 index a6dedd9..0000000 --- a/app/views/todo.js +++ /dev/null @@ -1,42 +0,0 @@ -// @flow - -import React from 'react'; - -import TodoInput from '../components/todo-input'; -import TodoList from '../components/todo-list'; - -import type { TodoType } from '../types/todo'; - -import checklist from '../assets/images/checklist.svg'; - -type Props = { - addTodo: Function, - deleteTodo: Function, - toggleEdit: Function, - todos: Array<TodoType>, - updateTodo: Function, - cancelUpdateTodo: Function, -}; - -export default (props: Props) => { - const { - addTodo, todos, deleteTodo, toggleEdit, updateTodo, cancelUpdateTodo, - } = props; - - return ( - <div className='todo'> - <div className='todo__heading'> - <img src={checklist} alt='Testing File Loader' className='todo__image' /> - <h1 className='todo__header'>Todo List App</h1> - </div> - <TodoInput addTodo={addTodo} /> - <TodoList - todos={todos} - deleteTodo={deleteTodo} - toggleEdit={toggleEdit} - updateTodo={updateTodo} - cancelUpdateTodo={cancelUpdateTodo} - /> - </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/logger.js b/config/daemon/logger.js index d5da6d2..4806e3f 100644 --- a/config/daemon/logger.js +++ b/config/daemon/logger.js @@ -1,4 +1,4 @@ // @flow /* eslint-disable-next-line no-console */ -export default (...message: Array<*>) => console.log('[ZCash Daemon]', ...message); +export default (...message: Array<*>) => console.log('[Zcash Daemon]', ...message); 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..379294c 100644 --- a/config/daemon/zcashd-child-process.js +++ b/config/daemon/zcashd-child-process.js @@ -16,7 +16,7 @@ import getDaemonName from './get-daemon-name'; import fetchParams from './run-fetch-params'; import log from './logger'; import store from '../electron-store'; -import generateRandomString from './generate-random-string'; +import generateRandomString from '../../app/utils/generate-random-string'; const getDaemonOptions = ({ username, password }) => { /* @@ -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..7edd495 100644 --- a/config/electron.js +++ b/config/electron.js @@ -1,5 +1,6 @@ // @flow import '@babel/polyfill'; +import dotenv from 'dotenv'; import path from 'path'; @@ -13,6 +14,10 @@ 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'; + +dotenv.config(); let mainWindow: BrowserWindowType; let updateAvailable: boolean = false; @@ -37,9 +42,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 +63,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; @@ -75,7 +88,7 @@ app.on('ready', async () => { if (err || !proc) return zcashLog(err); /* eslint-disable-next-line */ - zcashLog(`ZCash Daemon running. PID: ${proc.pid}`); + zcashLog(`Zcash Daemon running. PID: ${proc.pid}`); zcashDaemon = proc; }); @@ -87,7 +100,7 @@ app.on('window-all-closed', () => { }); app.on('before-quit', () => { if (zcashDaemon) { - zcashLog('Graceful shutdown ZCash Daemon, this may take a few seconds.'); + zcashLog('Graceful shutdown Zcash Daemon, this may take a few seconds.'); zcashDaemon.kill('SIGINT'); } }); diff --git a/package.json b/package.json index 49cc144..785d093 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,15 +87,22 @@ "@babel/register": "^7.0.0", "autoprefixer": "^9.3.1", "connected-react-router": "^5.0.1", + "date-fns": "^1.30.1", + "dotenv": "^6.2.0", + "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-popover": "^0.5.10", "react-redux": "^5.0.7", "react-router-dom": "^4.2.2", "redux": "^4.0.1", diff --git a/services/utils.js b/services/utils.js index 356a15b..3b90312 100644 --- a/services/utils.js +++ b/services/utils.js @@ -1,4 +1,5 @@ // @flow +/* eslint-disable max-len */ export const METHODS = [ 'getbestblockhash', @@ -745,8 +746,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..f54d191 --- /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=${String(process.env.ZEC_PRICE_API_KEY)}`; + + 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 86e3868..7df910d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3898,6 +3898,13 @@ css-value@~0.0.1: resolved "https://registry.yarnpkg.com/css-value/-/css-value-0.0.1.tgz#5efd6c2eea5ea1fd6b6ac57ec0427b18452424ea" integrity sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo= +css-vendor@^0.3.1: + version "0.3.8" + resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-0.3.8.tgz#6421cfd3034ce664fe7673972fd0119fc28941fa" + integrity sha1-ZCHP0wNM5mT+dnOXL9ARn8KJQfo= + dependencies: + is-in-browser "^1.0.2" + css-what@2.1, css-what@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d" @@ -4073,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" @@ -4800,6 +4812,11 @@ dotenv@^6.0.0, dotenv@^6.1.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.1.0.tgz#9853b6ca98292acb7dec67a95018fa40bccff42c" integrity sha512-/veDn2ztgRlB7gKmE3i9f6CmDIyXAy6d5nBq+whO9SLX+Zs1sXEgFLPi+aSuWqUuusMfbi84fT8j34fs1HaYUw== +dotenv@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064" + integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w== + duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -7542,6 +7559,11 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835" integrity sha512-but/G3sapV3MNyqiDBLrOi4x8uCIw0RY3o/Vb5GT0sMFHrVV7731wFSVy41T5FO1og7G0gXLJh0MkgPRouko/A== +is-in-browser@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" + integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= + is-installed-globally@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" @@ -8827,6 +8849,13 @@ lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.debounce@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-3.1.1.tgz#812211c378a94cc29d5aa4e3346cf0bfce3a7df5" + integrity sha1-gSIRw3ipTMKdWqTjNGzwv846ffU= + dependencies: + lodash._getnative "^3.0.0" + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -8847,11 +8876,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" @@ -8906,6 +8945,13 @@ lodash.tail@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= +lodash.throttle@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-3.0.4.tgz#bc4f471fb328e4d6fdc6df2b3d3caf113f0f89c9" + integrity sha1-vE9HH7Mo5Nb9xt8rPTyvET8Pick= + dependencies: + lodash.debounce "^3.0.0" + lodash.throttle@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" @@ -11470,6 +11516,10 @@ re-resizable@^4.10.0: resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-4.10.0.tgz#e5a4d5e0c79e9c94a1d68fda77aee91526eaff3e" integrity sha512-g5Q5IswKX7LM+MtYFnuzaQrTEGr/kpserqGV8V6HYkjwbV60XnJv00VlKugLHEwlQ5pgrV08spm0TjyyYVbWmQ== +react-click-outside@tj/react-click-outside: + version "1.1.1" + resolved "https://codeload.github.com/tj/react-click-outside/tar.gz/a833ddc5be47490307f9fcc6ed09d8c353108510" + react-codemirror2@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-5.1.0.tgz#62de4460178adea40eb52eabf7491669bf3794b8" @@ -11664,6 +11714,16 @@ react-perfect-scrollbar@^1.4.2: perfect-scrollbar "^1.4.0" prop-types "^15.6.1" +react-popover@^0.5.10: + version "0.5.10" + resolved "https://registry.yarnpkg.com/react-popover/-/react-popover-0.5.10.tgz#40d5e854300a96722ffc1620e49d840cb1ad5db6" + integrity sha512-5SYDTfncywSH00I70oHd4gFRUR8V0rJ4sRADSI/P6G0RVXp9jUgaWloJ0Bk+SFnjpLPuipTKuzQNNd2CTs5Hrw== + dependencies: + css-vendor "^0.3.1" + debug "^2.6.8" + lodash.throttle "^3.0.3" + prop-types "^15.5.10" + react-powerplug@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/react-powerplug/-/react-powerplug-1.0.0.tgz#f9c10a761ece115661b8fd10920c4e573ea95909"