Merge pull request #53 from andrerfneves/develop

Release v0.3
This commit is contained in:
André Neves 2019-01-25 17:55:23 -05:00 committed by GitHub
commit 03c4eacb7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
169 changed files with 25806 additions and 8360 deletions

View File

@ -1,11 +1,10 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-flow"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread"
]
}
{
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"],
"plugins": [
"@babel/plugin-transform-regenerator",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-syntax-dynamic-import"
]
}

48
.circleci/config.yml Normal file
View File

@ -0,0 +1,48 @@
version: 2
jobs:
test:
docker:
- image: electronuserland/builder:wine-chrome
steps:
- checkout
- run: apt-get -y update
- run: apt install -y software-properties-common
- run: add-apt-repository ppa:jonathonf/ffmpeg-4
- run: apt-get -y update
- run: apt-get -y install libusb-1.0-0-dev graphicsmagick libudev-dev
- run: apt-get -y install tmux xvfb libxtst6 libxss1 libgtk2.0-0 libnss3 libasound2 libgconf-2-4 ffmpeg frei0r-plugins
# TODO: Just a quick try
- run: rm yarn.lock
- run: yarn install
- run:
name: Run Webpack
command: yarn dev
background: true
- run:
name: Run Mock RPC API
command: yarn e2e:serve
background: true
- run: yarn wait-on http://localhost:8080 && yarn wait-on http://localhost:18232
- run:
command: Xvfb :44 -auth /tmp/xvfb.auth -ac -screen 0 1024x768x24 -listen tcp
background: true
- run:
command: ffmpeg -y -f x11grab -video_size 1024x768 -i :44 -codec:v libx264 -r 12 /tmp/e2e-record.mp4
background: true
- run: DISPLAY=:44 yarn e2e:run
- run: |
kill -s SIGINT $(pgrep ffmpeg)
sleep 10
kill -s SIGTERM $(pgrep Xvfb)
- store_artifacts:
path: /tmp/e2e-record.mp4
destination: e2e-record.mp4
workflows:
version: 2
test:
jobs:
- test:
filters:
branches:
only: feature/e2e

1
.env.example Normal file
View File

@ -0,0 +1 @@
ZEC_PRICE_API_KEY=

103
.eslintrc
View File

@ -1,55 +1,48 @@
{
"parser": "babel-eslint",
"extends": [
"airbnb",
"plugin:flowtype/recommended"
],
"env": {
"browser": true,
"node": true,
"mocha": true
},
"plugins": ["flowtype"],
"settings": {
"flowtype": {
"onlyFilesWithFlowAnnotation": true
}
},
"rules": {
"jsx-quotes": ["error", "prefer-single"],
"import/prefer-default-export": [
"off"
],
"react/jsx-filename-extension": [
1,
{ "extensions": [".js"] }
],
"jsx-a11y/anchor-is-valid": [
"error",
{
"components": [
"Link"
],
"specialLink": [
"to",
"hrefLeft",
"hrefRight"
],
"aspects": [
"noHref",
"invalidHref",
"preferButton"
]
}
],
"jsx-a11y/no-autofocus": [ 0, {
"ignoreNonDOM": true
}],
"max-len": ["error", {
"ignoreUrls": true,
"ignoreComments": true,
"ignoreStrings": true,
"ignorePattern": "<p[^>]*>.*?</p>"
}]
}
}
{
"parser": "babel-eslint",
"extends": ["airbnb", "plugin:flowtype/recommended"],
"env": {
"browser": true,
"node": true,
"mocha": true,
"jest/globals": true
},
"plugins": ["flowtype", "jest"],
"settings": {
"flowtype": {
"onlyFilesWithFlowAnnotation": true
}
},
"rules": {
"jsx-quotes": ["error", "prefer-single"],
"import/prefer-default-export": ["off"],
"react/jsx-filename-extension": [1, { "extensions": [".js"] }],
"jsx-a11y/anchor-is-valid": [
"error",
{
"components": ["Link"],
"specialLink": ["to", "hrefLeft", "hrefRight"],
"aspects": ["noHref", "invalidHref", "preferButton"]
}
],
"jsx-a11y/no-autofocus": [
0,
{
"ignoreNonDOM": true
}
],
"max-len": [
"error",
{
"code": 100,
"tabWidth": 2,
"ignoreUrls": true,
"ignoreComments": true,
"ignoreStrings": true,
"ignorePattern": "<p[^>]*>.*?</p>",
"ignoreTrailingComments": true
}
],
"consistent-return": 0
}
}

View File

@ -1,11 +1,15 @@
[ignore]
.*/node_modules/polished/.*
./__tests__
[include]
[libs]
flow-typed
[lints]
[options]
esproposal.optional_chaining=enable
[strict]

5
.gitignore vendored
View File

@ -3,3 +3,8 @@ node_modules
dist
.DS_Store
flow-coverage
build
.docz
coverage
flow-typed
.env

75
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,75 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of
experience, nationality, personal appearance, race, religion, or sexual identity
and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening,
offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at kent+coc@doddsfamily.us. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an
incident. Further details of specific enforcement policies may be posted
separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Zcash Foundation
Copyright (c) 2019 Zcash Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

72
NODE.md
View File

@ -1,24 +1,26 @@
# Zcash Node Setup
## Installation
The original `zcashd` daemon built by the Zcash Company, is built entirely in for Linux systems. If you're running a Linux distro, you should look [here](https://github.com/zcash/zcash) for instructions on how to install it.
Download the attached files, verify checksum of the archive and extract
For macOS users, please follow [this guide](https://github.com/kozyilmaz/zcash-apple) to build the binaries locally on your machine and then install it. After downloading the tarball and the hash, use the steps below to kickstart your node and connect it to the network.
## Installing
Download the attached files from Zcash Apple repository, verify the checksum of the archive and then extract its contents:
```bash
shasum -a 256 -c zcash-macos-v2.0.1a.tar.bz2.hash
tar -xvf zcash-macos-v2.0.1a.tar.bz2
```
## How to use
## Running
When launching Zcash on MacOS for the first time, certain initialization steps should be completed.
Please run the commands below once for the first time
When launching Zcash on macOS for the first time, certain initialization steps should be completed. Please run the commands below once for the first time
```
cd zcash-macos-v2.0.1a/usr/local/bin
./zcash-fetch-params
./zcash-init
./zcashd
```
You can just run Zcash by launching the daemon afterwards:
@ -27,4 +29,60 @@ You can just run Zcash by launching the daemon afterwards:
./zcashd
```
You can refer to NODE_COMMANDS for basic usage
## Basic Commands
Along with `zcashd`, the daemon provides the `zcash-cli` utility which allows you to run RPC commands to your node, and receive the output data.
Some useful commands can be found below:
### General
> List information for a command
```bash
./zcash-cli help <command>
```
> List all shielded addresses
```bash
./zcash-cli z_listaddresses
```
> Create new shielded address
```bash
./zcash-cli z_getnewaddress
```
> Get total balance
```bash
./zcash-cli z_gettotalbalance
```
### Sending Funds
```bash
export ZADDR='zcNeXiyD3JkhKTrU38xM9C6HQGy9aP5qqVFH25qFzQGnmdwYZ2Dr53Jy7iRp64D4CzkMZdmKagN6mmtu3jVKHuZ8xZp8fw3'
export FRIEND='zcfZJW3qLHpSc7q7W1SXRGdVjgM6Q6kRwdkz1DHW5sP2EqcMHf5RCp3Frpf2qnb81j9K6upzRN4HoVxfboVwLTRaZ7bKn8b'
```
> Send from shielded address to shielded address (with memo and fee)
```bash
./zcash-cli z_sendmany "$ZADDR" "[{\"address\": \"$FRIEND\", \"amount\": 0.05, \"memo\": \"9876543210\"}]" 1 0.002
```
> Get send result
```bash
./zcash-cli z_getoperationresult [\"$OPID\"]
```
> List amounts received by shielded address
```bash
./zcash-cli z_listreceivedbyaddress "$ZADDR"
```
## Helpful Resources
* [Zcash Documentation (ReadTheDocs)](https://zcash.readthedocs.io/en/latest/rtd_pages/user_guide.html)
* [Zcash Apple Daemon](https://github.com/kozyilmaz/zcash-apple)
* [Zcash Community Forum](https://forum.zcashcommunity.com/)
* [Zcash Foundation](https://z.cash.foundation/)
* [Zcash Daemon](https://github.com/zcash/zcash)
* [Zcash Payment API Docs](https://github.com/zcash/zcash/blob/master/doc/payment-api.md)

View File

@ -1,59 +0,0 @@
# for detailed information use the guides below
https://github.com/zcash/zcash/wiki/1.0-User-Guide
https://github.com/zcash/zcash/blob/master/doc/payment-api.md
https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_calls_list
# list info for a command
$ ./zcash-cli help <command>
# list all accounts (therefore all t-addr's)
$ ./zcash-cli listreceivedbyaddress 0 true
# list of t-addr's for default account (again all t-addr's)
$ ./zcash-cli getaddressesbyaccount ""
# list all unspent transaction outputs (t-addr UTXO's)
$ ./zcash-cli listunspent
# create new t-addr's
$ ./zcash-cli getnewaddress
# list all z-addr's
$ ./zcash-cli z_listaddresses
# create new z-addr's
$ ./zcash-cli z_getnewaddress
# get total balance
$ ./zcash-cli z_gettotalbalance
# send funds
$ export TADDR='t1TGVDzsEK2qbG1N8FJQFSAzV1bWWHMGGCS'
$ export ZADDR='zcNeXiyD3JkhKTrU38xM9C6HQGy9aP5qqVFH25qFzQGnmdwYZ2Dr53Jy7iRp64D4CzkMZdmKagN6mmtu3jVKHuZ8xZp8fw3'
$ export FRIEND='zcfZJW3qLHpSc7q7W1SXRGdVjgM6Q6kRwdkz1DHW5sP2EqcMHf5RCp3Frpf2qnb81j9K6upzRN4HoVxfboVwLTRaZ7bKn8b'
# send from t-addr to z-addr (with memo and fee)
$ ./zcash-cli z_sendmany "$TADDR" "[{\"address\": \"$ZADDR\", \"amount\": 0.1, \"memo\": \"0123456789\"}]" 1 0.002
# send from t-addr to t-addr (with fee)
$ ./zcash-cli z_sendmany "$TADDR1" "[{\"address\": \"$TADDR2\", \"amount\": 0.09}]" 1 0.002
# send from z-addr to z-addr (with memo and fee)
$ ./zcash-cli z_sendmany "$ZADDR" "[{\"address\": \"$FRIEND\", \"amount\": 0.05, \"memo\": \"9876543210\"}]" 1 0.002
# get send result
$ ./zcash-cli z_getoperationresult [\"$OPID\"]
# list amounts received by z-addr
$ ./zcash-cli z_listreceivedbyaddress "$ZADDR"
# list balance both for t-addr and z-addr
$ ./zcash-cli z_getbalance "$TADDR"

View File

@ -12,6 +12,7 @@ Reference Wallet for the Zcash Network
- [Babel](http://babeljs.io/): ES7/JSX transpilling
- [ESLint](http://eslint.org/): code rules and linting
- [React Router](https://github.com/reactjs/react-router): routing solution for react
- [Styled Components](https://www.styled-components.com/): visual primitives for theming applications
## Installation
@ -21,12 +22,18 @@ yarn install
## Development
To run the application on port 8080
To run the application you simply need to run
```bash
yarn dev
yarn start
```
This will kickstart the webpack development server and serve the app on port 8080, as well as launch the Electron wrapper for the application, which houses the `zcashd` daemon process.
## Buiding & Distribution
TBD
## License
© Zcash Foundation 2018
© Zcash Foundation 2018

View File

@ -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,
}),
);
});
});

View File

@ -0,0 +1,23 @@
// @flow
import { getApp } from '../setup/utils';
const app = getApp();
beforeAll(async () => {
await app.start();
await app.client.waitUntilWindowLoaded();
await app.client.waitUntilTextExists('#sidebar', 'Console');
});
afterAll(() => app.stop());
describe('Console', () => {
test('should load "Console Page"', async () => {
await app.client.element('#sidebar a:nth-child(6)').click();
expect(app.client.getText('#header p:first-child')).resolves.toEqual('Console');
expect(app.client.element('#console-wrapper img').getAttribute('src')).resolves.toEqual(
expect.stringContaining('/assets/console_zcash.png'),
);
});
});

157
__tests__/e2e/send.test.js Normal file
View File

@ -0,0 +1,157 @@
// @flow
import { getApp } from '../setup/utils';
const app = getApp();
beforeEach(async () => {
await app.start();
await app.client.waitUntilWindowLoaded();
await app.client.waitUntilTextExists('#sidebar', 'Send');
await app.client.element('#sidebar a:nth-child(2)').click();
});
afterEach(() => app.stop());
describe('Send', () => {
test('should load "Send Page"', async () => {
expect(app.client.element('#send-wrapper').isVisible()).resolves.toEqual(true);
});
test('should show Additional Options click', async () => {
expect(app.client.element('#send-wrapper #send-fee-wrapper').isVisible()).resolves.toEqual(
false,
);
await app.client.element('#send-show-additional-options-button').click();
expect(app.client.element('#send-wrapper #send-fee-wrapper').isVisible()).resolves.toEqual(
true,
);
});
test('should disable send button if required fields are empty', async () => {
expect(app.client.element('#send-submit-button').getAttribute('disabled')).resolves.toEqual(
true,
);
});
test('should enable send button if required fields are filled', async () => {
await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.element('#sidebar a:nth-child(2)').click();
await app.client.element('#send-wrapper #select-component').click();
await app.client
.element('#send-wrapper #select-component #t3Pnbg7XjP7FGPBUuz75H65aczphHgkpoJW')
.click();
await app.client.element('#send-wrapper input[name=amount]').setValue('0.484');
await app.client
.element('#send-wrapper input[name=to]')
.setValue('t14oHp2v54vfmdgQ3v3SNuQga8JKHTNi2a1');
expect(
expect(app.client.element('#send-submit-button').getAttribute('disabled')).resolves.toEqual(
false,
),
);
});
test('should show confirm transaction modal', async () => {
await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.element('#sidebar a:nth-child(2)').click();
await app.client.element('#send-wrapper #select-component').click();
await app.client
.element('#send-wrapper #select-component #t3Pnbg7XjP7FGPBUuz75H65aczphHgkpoJW')
.click();
await app.client.element('#send-wrapper input[name=amount]').setValue('0.484');
await app.client
.element('#send-wrapper input[name=to]')
.setValue('tmMEBdrnRRRMKSUUC9SWdSova7V8NmHBqET');
await app.client.element('#send-submit-button').click();
expect(app.client.element('#send-confirm-transaction-modal').isVisible()).resolves.toEqual(
true,
);
});
test('should display a load indicator while the transaction is processed', async () => {
await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.element('#sidebar a:nth-child(2)').click();
await app.client.element('#send-wrapper #select-component').click();
await app.client
.element('#send-wrapper #select-component #t3Pnbg7XjP7FGPBUuz75H65aczphHgkpoJW')
.click();
await app.client.element('#send-wrapper input[name=amount]').setValue('0.484');
await app.client
.element('#send-wrapper input[name=to]')
.setValue('tmMEBdrnRRRMKSUUC9SWdSova7V8NmHBqET');
await app.client.element('#send-submit-button').click();
await app.client.element('#confirm-modal-button').click();
expect(app.client.getAttribute('#send-confirm-transaction-modal img', 'src')).resolves.toEqual(
expect.stringContaining('/assets/sync_icon.png'),
);
expect(app.client.getText('#send-confirm-transaction-modal p')).resolves.toEqual(
'Processing transaction...',
);
expect(app.client.element('#confirm-modal-button').isVisible()).resolves.toEqual(false);
});
test('should show an error in invalid transaction', async () => {
expect(app.client.element('#send-error-text').isVisible()).resolves.toEqual(false);
await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.element('#sidebar a:nth-child(2)').click();
await app.client.element('#send-wrapper #select-component').click();
await app.client
.element('#send-wrapper #select-component #t3Pnbg7XjP7FGPBUuz75H65aczphHgkpoJW')
.click();
await app.client.element('#send-wrapper input[name=amount]').setValue('-500');
await app.client
.element('#send-wrapper input[name=to]')
.setValue('tmMEBdrnRRRMKSUUC9SWdSova7V8NmHBqET');
await app.client.element('#send-submit-button').click();
await app.client.element('#confirm-modal-button').click();
expect(
app.client
.element('#send-error-message')
.waitForVisible()
.isVisible(),
).resolves.toEqual(true);
});
test('should show a success screen after transaction and show a transaction item', async () => {
expect(app.client.element('#send-success-wrapper').isVisible()).resolves.toEqual(false);
await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.element('#sidebar a:nth-child(2)').click();
await app.client.element('#send-wrapper #select-component').click();
await app.client
.element('#send-wrapper #select-component #t3Pnbg7XjP7FGPBUuz75H65aczphHgkpoJW')
.click();
await app.client.element('#send-wrapper input[name=amount]').setValue('0.484');
await app.client
.element('#send-wrapper input[name=to]')
.setValue('t3Pnbg7XjP7FGasduz75HadsdzphHgkadW');
await app.client.element('#send-submit-button').click();
await app.client.element('#confirm-modal-button').click();
await app.client
.waitForVisible('#send-success-wrapper')
.element('#send-confirm-transaction-modal button')
.click();
await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.waitUntilTextExists('#transaction-item-operation-id-1', 'Send');
expect(await app.client.element('#transaction-item-operation-id-1 img').isVisible()).toEqual(
true,
);
expect(
await app.client.element('#transaction-item-operation-id-1 img').getAttribute('src'),
).toEndWith('/assets/transaction_sent_icon.svg');
});
});

View File

@ -0,0 +1,98 @@
// @flow
import { getApp } from '../setup/utils';
const app = getApp();
beforeAll(async () => {
await app.start();
await app.client.waitUntilWindowLoaded();
await app.client.waitUntilTextExists('#sidebar', 'Dashboard');
});
afterAll(() => app.stop());
describe('Sidebar', () => {
test('should see the active dashboard route', async () => {
await app.client.element('#sidebar a:nth-child(1)').click();
expect(await app.client.getUrl()).toEndWith('/');
expect(await app.client.element('#sidebar a:nth-child(1)').getHTML()).toEqual(
expect.stringContaining('Dashboard'),
);
expect(await app.client.element('#sidebar a:nth-child(1) img').getAttribute('src')).toEqual(
expect.stringContaining('/assets/dashboard_icon_active.svg'),
);
});
test('should see the active send route', async () => {
await app.client.element('#sidebar a:nth-child(2)').click();
expect(await app.client.getUrl()).toEndWith('/send');
expect(await app.client.element('#sidebar a:nth-child(2)').getHTML()).toEqual(
expect.stringContaining('Send'),
);
expect(await app.client.element('#sidebar a:nth-child(2) img').getAttribute('src')).toEqual(
expect.stringContaining('/assets/send_icon_active.svg'),
);
});
test('should see the active receive route', async () => {
await app.client.element('#sidebar a:nth-child(3)').click();
expect(await app.client.getUrl()).toEndWith('/receive');
expect(await app.client.element('#sidebar a:nth-child(3)').getHTML()).toEqual(
expect.stringContaining('Receive'),
);
expect(await app.client.element('#sidebar a:nth-child(3) img').getAttribute('src')).toEqual(
expect.stringContaining('/assets/receive_icon_active.svg'),
);
});
test('should see the active transactions route', async () => {
await app.client.element('#sidebar a:nth-child(4)').click();
expect(await app.client.getUrl()).toEndWith('/transactions');
expect(await app.client.element('#sidebar a:nth-child(4)').getHTML()).toEqual(
expect.stringContaining('Transactions'),
);
expect(await app.client.element('#sidebar a:nth-child(4) img').getAttribute('src')).toEqual(
expect.stringContaining('/assets/transactions_icon_active.svg'),
);
});
test('should see the active settings route', async () => {
await app.client.element('#sidebar a:nth-child(5)').click();
expect(await app.client.getUrl()).toEndWith('/settings');
expect(await app.client.element('#sidebar a:nth-child(5)').getHTML()).toEqual(
expect.stringContaining('Settings'),
);
expect(await app.client.element('#sidebar a:nth-child(5) img').getAttribute('src')).toEqual(
expect.stringContaining('/assets/settings_icon_active.svg'),
);
});
test('should see the active console route', async () => {
await app.client.element('#sidebar a:nth-child(6)').click();
expect(await app.client.getUrl()).toEndWith('/console');
expect(await app.client.element('#sidebar a:nth-child(6)').getHTML()).toEqual(
expect.stringContaining('Console'),
);
expect(await app.client.element('#sidebar a:nth-child(6) img').getAttribute('src')).toEqual(
expect.stringContaining('/assets/console_icon_active.svg'),
);
});
});

View File

@ -0,0 +1,31 @@
// @flow
import { getApp } from '../setup/utils';
const app = getApp();
beforeAll(async () => {
await app.start();
await app.client.waitUntilWindowLoaded();
});
afterAll(() => app.stop());
describe('Startup', () => {
test('should open the window', () => expect(app.client.getWindowCount()).resolves.toEqual(1));
test('should have the right title', () => {
expect(app.client.getTitle()).resolves.toEqual('ZEC Wallet');
});
test('should show the text "ZEC Wallet Starting" in loading screen', async () => expect(app.client.element('#loading-screen:first-child p').getHTML()).resolves.toEqual(
expect.stringContaining('ZEC Wallet Starting'),
));
test('should show the zcash logo in loading screen', () => expect(app.client.getAttribute('#loading-screen:first-child img', 'src')).resolves.toEqual(
expect.stringContaining('/assets/zcash-simple-icon.svg'),
));
test('should show the loading circle in loading screen', () => {
expect(app.client.element('#loading-screen svg').isExisting()).resolves.toEqual(true);
});
});

View File

@ -0,0 +1,18 @@
// @flow
import { getApp } from '../setup/utils';
const app = getApp();
beforeAll(async () => {
await app.start();
await app.client.waitUntilWindowLoaded();
await app.client.waitUntilTextExists('#sidebar', 'Dashboard');
});
afterAll(() => app.stop());
describe('Status Pill', () => {
test('should show status pill in the header', async () => expect(
app.client.waitUntilTextExists('#status-pill', '50.00%').getText('#status-pill'),
).resolves.toEqual(expect.stringContaining('50.00%')));
});

View File

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

7
__tests__/setup/jest.js Normal file
View File

@ -0,0 +1,7 @@
// @flow
// eslint-disable-next-line import/no-unresolved
require('jest-extended');
// $FlowFixMe
jest.DEFAULT_TIMEOUT_INTERVAL = 120000;
jest.setTimeout(120000);

101
__tests__/setup/mockAPI.js Normal file
View File

@ -0,0 +1,101 @@
// @flow
// eslint-disable-next-line import/no-extraneous-dependencies
import 'babel-polyfill';
import createTestServer from 'create-test-server';
const transactions = [];
const sleep = (time: number) => new Promise(resolve => setTimeout(resolve, time));
createTestServer({
httpPort: '18232',
}).then(async (server) => {
console.log('[MOCK RPC API]', server.url);
server.get('/', (req, res) => {
res.send('Zcash RPC');
});
server.post('/', async (req, res) => {
const { method } = req.body;
switch (method) {
case 'getinfo':
sleep(1500).then(() => res.send({ result: { version: 1.0 } }));
break;
case 'getblockchaininfo':
return res.send({ result: { verificationprogress: 0.5 } });
case 'z_gettotalbalance':
return res.send({
result: { transparent: 2.5, private: 3.5, total: 6 },
});
case 'z_listaddresses':
return res.send({
result: ['zs1z7rejlpsa98s2rrrfkwmaxu53e4ue0ulcrw0h4x5g8jl04tak0d3mm47vdtahatqrlkngh9sly'],
});
case 'getaddressesbyaccount':
return res.send({
result: ['t3Pnbg7XjP7FGPBUuz75H65aczphHgkpoJW'],
});
case 'listtransactions':
return res.send({
result: transactions,
});
case 'z_sendmany':
// eslint-disable-next-line
sleep(2000).then(() => {
const [, [obj], amount, fee] = req.body.params;
if ((obj.address[0] === 'z' || obj.address[0] === 't') && amount > 0) {
transactions.push({
account: '',
address: obj.address,
category: 'send',
amount: obj.amount,
vout: 0,
fee,
confirmations: 10,
blockhash: 20,
blockindex: 10,
txid: `operation-id-${transactions.length + 1}`,
time: Date.now(),
timereceived: Date.now(),
comment: '',
otheraccount: '',
size: 10,
});
return res.send({ result: 'operation-id-1' });
}
return res.status(500).send({ error: { message: 'Invalid address!' } });
});
break;
case 'z_validateaddress':
// eslint-disable-next-line
const [zAd] = req.body.params;
if (zAd[0] === 'z' || zAd[0] === 't') {
return res.send({ result: { isvalid: true } });
}
return res.send({ result: { isvalid: false } });
case 'validateaddress':
// eslint-disable-next-line
const [tAd] = req.body.params;
if (tAd[0] === 'z' || tAd[0] === 't') {
return res.send({ result: { isvalid: true } });
}
return res.send({ result: { isvalid: false } });
case 'z_getoperationstatus':
return res.send({
result: [{ id: 'operation-id-1', status: 'success', result: { txid: 'txid-1' } }],
});
default:
return null;
}
});
});

17
__tests__/setup/utils.js Normal file
View File

@ -0,0 +1,17 @@
// @flow
/* eslint-disable import/no-extraneous-dependencies */
import electron from 'electron';
import { Application } from 'spectron';
export const TIMEOUT = 20000;
export const getApp = () => new Application({
path: electron,
args: ['.'],
startTimeout: TIMEOUT,
waitTimeout: TIMEOUT,
quitTimeout: TIMEOUT,
env: {
NODE_ENV: 'test',
},
});

View File

@ -1,15 +0,0 @@
// @flow
import uuidv4 from 'uuid/v4';
import { ADD_TODO } from '../constants/actions';
import { getTimestamp } from '../utils/timestamp';
export const addTodo = (text: string) => ({
type: ADD_TODO,
payload: {
text,
id: uuidv4(),
editing: false,
createdAt: getTimestamp(),
},
});

View File

@ -1,8 +0,0 @@
// @flow
import { CANCEL_UPDATE_TODO } from '../constants/actions';
export const cancelUpdateTodo = (id: string) => ({
type: CANCEL_UPDATE_TODO,
payload: { id },
});

View File

@ -1,8 +0,0 @@
// @flow
import { DELETE_TODO } from '../constants/actions';
export const deleteTodo = (id: string) => ({
type: DELETE_TODO,
payload: { id },
});

View File

@ -1,8 +0,0 @@
// @flow
import { TOGGLE_EDIT_TODO } from '../constants/actions';
export const toggleEdit = (id: string) => ({
type: TOGGLE_EDIT_TODO,
payload: { id },
});

View File

@ -1,11 +0,0 @@
// @flow
import { UPDATE_TODO } from '../constants/actions';
export const updateTodo = (id: string, text: string) => ({
type: UPDATE_TODO,
payload: {
text,
id,
},
});

View File

@ -1,17 +1,26 @@
// @flow
import React from 'react';
import React, { Fragment } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import configureStore from './store/configure';
import Router from './router';
import { ConnectedRouter } from 'connected-react-router';
import { ThemeProvider } from 'styled-components';
import { configureStore, history } from './redux/create';
import { Router } from './router/container';
import theme, { GlobalStyle } from './theme';
const store = configureStore({});
export default () => (
<BrowserRouter>
<Provider store={store}>
<Router />
</Provider>
</BrowserRouter>
<ThemeProvider theme={theme}>
<Fragment>
<GlobalStyle />
<Provider store={store}>
<ConnectedRouter history={history}>
{/* $FlowFixMe */}
<Router />
</ConnectedRouter>
</Provider>
</Fragment>
</ThemeProvider>
);

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M0 7.33l2.829-2.83 9.175 9.339 9.167-9.339 2.829 2.83-11.996 12.17z"/></svg>

After

Width:  |  Height:  |  Size: 181 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M0 16.67l2.829 2.83 9.175-9.339 9.167 9.339 2.829-2.83-11.996-12.17z"/></svg>

After

Width:  |  Height:  |  Size: 182 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 24 24" height="24px" id="Layer_1" version="1.1" viewBox="0 0 24 24" width="24px" xml:space="preserve"><path xmlns="http://www.w3.org/2000/svg" fill="#777777" d="M22.245,4.015c0.313,0.313,0.313,0.826,0,1.139l-6.276,6.27c-0.313,0.312-0.313,0.826,0,1.14l6.273,6.272 c0.313,0.313,0.313,0.826,0,1.14l-2.285,2.277c-0.314,0.312-0.828,0.312-1.142,0l-6.271-6.271c-0.313-0.313-0.828-0.313-1.141,0 l-6.276,6.267c-0.313,0.313-0.828,0.313-1.141,0l-2.282-2.28c-0.313-0.313-0.313-0.826,0-1.14l6.278-6.269 c0.313-0.312,0.313-0.826,0-1.14L1.709,5.147c-0.314-0.313-0.314-0.827,0-1.14l2.284-2.278C4.308,1.417,4.821,1.417,5.135,1.73 L11.405,8c0.314,0.314,0.828,0.314,1.141,0.001l6.276-6.267c0.312-0.312,0.826-0.312,1.141,0L22.245,4.015z"/></svg>

After

Width:  |  Height:  |  Size: 839 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22.891 10.875"><defs><style>.a{fill:#3b3b3f;stroke:#3b3b3f;stroke-width:0.5px;}</style></defs><g transform="translate(0.283 0.286)"><g transform="translate(-0.001 0.004)"><path class="a" d="M2.269,5.239c1.185,1.183,2.35,2.345,3.513,3.508a.938.938,0,1,1-1.307,1.342c-.786-.777-1.564-1.561-2.346-2.342q-.89-.89-1.779-1.78A.943.943,0,0,1,.345,4.5Q2.4,2.446,4.449.395A.938.938,0,0,1,5.441.115a.891.891,0,0,1,.638.7.941.941,0,0,1-.307.917q-1.654,1.649-3.3,3.3C2.408,5.091,2.349,5.156,2.269,5.239Z" transform="translate(0.001 -0.067)"/><path class="a" d="M297.98,5.214c-1.2-1.2-2.36-2.357-3.522-3.512a1.015,1.015,0,0,1-.336-.674.944.944,0,0,1,.536-.9.932.932,0,0,1,1.071.195c.592.582,1.177,1.173,1.765,1.76l2.344,2.344a.959.959,0,0,1,.01,1.534q-2.027,2.028-4.054,4.057a.948.948,0,0,1-1.053.283.933.933,0,0,1-.307-1.576q1.655-1.664,3.319-3.319C297.815,5.341,297.888,5.29,297.98,5.214Z" transform="translate(-277.88 -0.034)"/><path class="a" d="M142.53,9.378a3.646,3.646,0,0,1,.149-.453q1.983-4.169,3.976-8.332a.939.939,0,1,1,1.7.8q-1.995,4.19-4,8.375a.93.93,0,0,1-1.1.533A1,1,0,0,1,142.53,9.378Z" transform="translate(-134.629 -0.004)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22.891 10.875"><defs><style>.a{fill:#f4b728;stroke:#3b3b3f;stroke-width:0.5px;}</style></defs><g transform="translate(0.283 0.286)"><g transform="translate(-0.001 0.004)"><path class="a" d="M2.269,5.239c1.185,1.183,2.35,2.345,3.513,3.508a.938.938,0,1,1-1.307,1.342c-.786-.777-1.564-1.561-2.346-2.342q-.89-.89-1.779-1.78A.943.943,0,0,1,.345,4.5Q2.4,2.446,4.449.395A.938.938,0,0,1,5.441.115a.891.891,0,0,1,.638.7.941.941,0,0,1-.307.917q-1.654,1.649-3.3,3.3C2.408,5.091,2.349,5.156,2.269,5.239Z" transform="translate(0.001 -0.067)"/><path class="a" d="M297.98,5.214c-1.2-1.2-2.36-2.357-3.522-3.512a1.015,1.015,0,0,1-.336-.674.944.944,0,0,1,.536-.9.932.932,0,0,1,1.071.195c.592.582,1.177,1.173,1.765,1.76l2.344,2.344a.959.959,0,0,1,.01,1.534q-2.027,2.028-4.054,4.057a.948.948,0,0,1-1.053.283.933.933,0,0,1-.307-1.576q1.655-1.664,3.319-3.319C297.815,5.341,297.888,5.29,297.98,5.214Z" transform="translate(-277.88 -0.034)"/><path class="a" d="M142.53,9.378a3.646,3.646,0,0,1,.149-.453q1.983-4.169,3.976-8.332a.939.939,0,1,1,1.7.8q-1.995,4.19-4,8.375a.93.93,0,0,1-1.1.533A1,1,0,0,1,142.53,9.378Z" transform="translate(-134.629 -0.004)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 19.091"><defs><style>.a{fill:#3b3b3f;}</style></defs><rect class="a" width="9.091" height="9.091" rx="2"/><rect class="a" width="20" height="8.182" rx="2" transform="translate(0 10.909)"/><rect class="a" width="9.091" height="9.091" rx="2" transform="translate(10.909)"/></svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 19.091"><defs><style>.a{fill:#f4b728;}</style></defs><rect class="a" width="9.091" height="9.091" rx="2"/><rect class="a" width="20" height="8.182" rx="2" transform="translate(0 10.909)"/><rect class="a" width="9.091" height="9.091" rx="2" transform="translate(10.909)"/></svg>

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
app/assets/images/eye.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg enable-background="new 0 0 256 256" height="256px" id="Layer_1" version="1.1" viewBox="0 0 256 256" width="256px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle fill="white" cx="57.6" cy="128" r="20"/>
<circle fill="white" cx="128" cy="128" r="20"/>
<circle fill="white" cx="198.4" cy="128" r="20"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.28 19.854"><defs><style>.a{fill:#3b3b3f;}</style></defs><g transform="translate(0)"><path class="a" d="M6.612,39.045h0a.9.9,0,0,1-.9.9h-3.9v9.638H5.959c.5,0,.675.176.675.682,0,.768,0,1.536,0,2.3,0,.424.191.619.617.62q2.387,0,4.774,0c.436,0,.627-.2.628-.638,0-.784,0-1.568,0-2.352,0-.429.186-.616.616-.616q1.94,0,3.88,0h.3V39.956H13.569a.9.9,0,0,1-.9-.9v-.022a.9.9,0,0,1,.9-.9c1.227,0,3.415,0,4.657,0a.948.948,0,0,1,1.053,1.066q0,7.972,0,15.944A1.048,1.048,0,0,1,18.23,56.2H1.041A.931.931,0,0,1,0,55.152q0-8.007,0-16.015a.912.912,0,0,1,.951-1c1.293-.006,3.529,0,4.769.006A.9.9,0,0,1,6.612,39.045Z" transform="translate(0.002 -36.345)"/><path class="a" d="M112.724,5.946l.016-.016a1.125,1.125,0,0,1,1.6.005l1.14,1.154.06-.024V1.222A1.221,1.221,0,0,1,116.756,0h0a1.222,1.222,0,0,1,1.222,1.222V7.057l1.143-1.144a1.125,1.125,0,0,1,1.6,0l.04.041a1.125,1.125,0,0,1,0,1.586l-3.4,3.4a.86.86,0,0,1-1.216,0l-3.409-3.41A1.125,1.125,0,0,1,112.724,5.946Z" transform="translate(-107.108)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.28 19.854"><defs><style>.a{fill:#f4b728;}</style></defs><g transform="translate(0)"><path class="a" d="M6.612,39.045h0a.9.9,0,0,1-.9.9h-3.9v9.638H5.959c.5,0,.675.176.675.682,0,.768,0,1.536,0,2.3,0,.424.191.619.617.62q2.387,0,4.774,0c.436,0,.627-.2.628-.638,0-.784,0-1.568,0-2.352,0-.429.186-.616.616-.616q1.94,0,3.88,0h.3V39.956H13.569a.9.9,0,0,1-.9-.9v-.022a.9.9,0,0,1,.9-.9c1.227,0,3.415,0,4.657,0a.948.948,0,0,1,1.053,1.066q0,7.972,0,15.944A1.048,1.048,0,0,1,18.23,56.2H1.041A.931.931,0,0,1,0,55.152q0-8.007,0-16.015a.912.912,0,0,1,.951-1c1.293-.006,3.529,0,4.769.006A.9.9,0,0,1,6.612,39.045Z" transform="translate(0.002 -36.345)"/><path class="a" d="M112.724,5.946l.016-.016a1.125,1.125,0,0,1,1.6.005l1.14,1.154.06-.024V1.222A1.221,1.221,0,0,1,116.756,0h0a1.222,1.222,0,0,1,1.222,1.222V7.057l1.143-1.144a1.125,1.125,0,0,1,1.6,0l.04.041a1.125,1.125,0,0,1,0,1.586l-3.4,3.4a.86.86,0,0,1-1.216,0l-3.409-3.41A1.125,1.125,0,0,1,112.724,5.946Z" transform="translate(-107.108)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.28 19.283"><defs><style>.a{fill:#3b3b3f;}</style></defs><g transform="translate(0 0)"><path class="a" d="M7.454,19.286c-.055-.021-.111-.038-.163-.063a.657.657,0,0,1-.4-.613c-.005-1.236,0-2.472,0-3.708a.213.213,0,0,1,.049-.12q1.437-1.766,2.878-3.528l6.229-7.635a.277.277,0,0,0,.051-.085.5.5,0,0,0-.042.029L9.744,9.019q-2.5,2.166-5,4.333a.12.12,0,0,1-.148.015Q2.513,12.512.432,11.661a.683.683,0,0,1-.086-1.232L3.87,8.4,18.218.118a.666.666,0,0,1,.751,0,.663.663,0,0,1,.294.717q-.356,2.122-.708,4.245Q18.2,7.241,17.838,9.4q-.428,2.568-.858,5.136-.23,1.38-.458,2.763a.686.686,0,0,1-.969.55q-2.369-.966-4.734-1.937a.138.138,0,0,0-.193.048q-1.254,1.535-2.511,3.066a.778.778,0,0,1-.424.264C7.612,19.286,7.533,19.286,7.454,19.286Z" transform="translate(0.003 -0.005)"/></g></svg>

After

Width:  |  Height:  |  Size: 826 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.28 19.283"><defs><style>.a{fill:#f4b728;}</style></defs><g transform="translate(0 0)"><path class="a" d="M7.454,19.286c-.055-.021-.111-.038-.163-.063a.657.657,0,0,1-.4-.613c-.005-1.236,0-2.472,0-3.708a.213.213,0,0,1,.049-.12q1.437-1.766,2.878-3.528l6.229-7.635a.277.277,0,0,0,.051-.085.5.5,0,0,0-.042.029L9.744,9.019q-2.5,2.166-5,4.333a.12.12,0,0,1-.148.015Q2.513,12.512.432,11.661a.683.683,0,0,1-.086-1.232L3.87,8.4,18.218.118a.666.666,0,0,1,.751,0,.663.663,0,0,1,.294.717q-.356,2.122-.708,4.245Q18.2,7.241,17.838,9.4q-.428,2.568-.858,5.136-.23,1.38-.458,2.763a.686.686,0,0,1-.969.55q-2.369-.966-4.734-1.937a.138.138,0,0,0-.193.048q-1.254,1.535-2.511,3.066a.778.778,0,0,1-.424.264C7.612,19.286,7.533,19.286,7.454,19.286Z" transform="translate(0.003 -0.005)"/></g></svg>

After

Width:  |  Height:  |  Size: 826 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20.327 20.328"><defs><style>.a{fill:#3b3b3f;fill-rule:evenodd;}</style></defs><path class="a" d="M20.311,9.58a.868.868,0,0,0-.725-.865c-.067-.017-.134-.017-.2-.029-.58-.1-1.159-.2-1.763-.312-.218-.571-.434-1.136-.658-1.713.1-.134.186-.265.283-.39.253-.32.5-.65.772-.953A1.017,1.017,0,0,0,18,3.707c-.146-.143-.271-.306-.408-.457-.577-.638-.859-.679-1.588-.23-.484.3-.961.6-1.471.921A8.953,8.953,0,0,0,12,2.771c-.047-.245-.093-.463-.131-.685-.07-.425-.137-.851-.213-1.276a.874.874,0,0,0-.827-.778,6.587,6.587,0,0,0-1.291,0A.861.861,0,0,0,8.708.8c-.041.248-.067.5-.131.737a6.365,6.365,0,0,1-.242,1.194c-.749.315-1.436.6-2.153.9-.131-.087-.283-.184-.431-.285-.373-.262-.734-.539-1.113-.787A.881.881,0,0,0,3.4,2.658c-.268.245-.527.5-.772.772a.863.863,0,0,0-.09,1.229c.236.361.5.7.746,1.058.125.184.312.338.323.51-.312.752-.6,1.439-.891,2.141-.111.023-.218.047-.329.064-.536.093-1.078.172-1.611.277a.855.855,0,0,0-.76.836,12.027,12.027,0,0,0,0,1.3.828.828,0,0,0,.647.787c.151.047.312.052.466.087a11.385,11.385,0,0,1,1.626.329c.23.609.446,1.177.667,1.765-.189.236-.37.466-.554.7-.227.283-.466.559-.679.854A.888.888,0,0,0,2.2,16.493a10.568,10.568,0,0,0,.816.915.842.842,0,0,0,1.116.157c.181-.1.35-.21.524-.318l1.2-.743c.437.224.83.452,1.241.635s.842.32,1.261.478c.038.186.073.341.1.5.087.492.157.988.262,1.477a.809.809,0,0,0,.848.728c.431.017.865.015,1.3-.006a.781.781,0,0,0,.749-.591c.061-.192.079-.4.125-.6a13.164,13.164,0,0,1,.28-1.492l2.141-.88c.192.131.4.268.6.411.335.236.661.484,1,.708a.863.863,0,0,0,1.232-.082,10.682,10.682,0,0,0,.772-.769.9.9,0,0,0,.093-1.238c-.277-.417-.58-.813-.865-1.224-.1-.146-.184-.306-.245-.408.3-.728.583-1.4.9-2.15.274-.05.565-.1.854-.154.335-.058.676-.1,1.008-.166a.891.891,0,0,0,.813-.935C20.329,10.352,20.332,9.964,20.311,9.58ZM10.188,14a3.779,3.779,0,0,1-3.8-3.825,3.812,3.812,0,1,1,7.624.023A3.769,3.769,0,0,1,10.188,14Z" transform="translate(0 -0.001)"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20.327 20.328"><defs><style>.a{fill:#f4b728;fill-rule:evenodd;}</style></defs><path class="a" d="M20.311,9.58a.868.868,0,0,0-.725-.865c-.067-.017-.134-.017-.2-.029-.58-.1-1.159-.2-1.763-.312-.218-.571-.434-1.136-.658-1.713.1-.134.186-.265.283-.39.253-.32.5-.65.772-.953A1.017,1.017,0,0,0,18,3.707c-.146-.143-.271-.306-.408-.457-.577-.638-.859-.679-1.588-.23-.484.3-.961.6-1.471.921A8.953,8.953,0,0,0,12,2.771c-.047-.245-.093-.463-.131-.685-.07-.425-.137-.851-.213-1.276a.874.874,0,0,0-.827-.778,6.587,6.587,0,0,0-1.291,0A.861.861,0,0,0,8.708.8c-.041.248-.067.5-.131.737a6.365,6.365,0,0,1-.242,1.194c-.749.315-1.436.6-2.153.9-.131-.087-.283-.184-.431-.285-.373-.262-.734-.539-1.113-.787A.881.881,0,0,0,3.4,2.658c-.268.245-.527.5-.772.772a.863.863,0,0,0-.09,1.229c.236.361.5.7.746,1.058.125.184.312.338.323.51-.312.752-.6,1.439-.891,2.141-.111.023-.218.047-.329.064-.536.093-1.078.172-1.611.277a.855.855,0,0,0-.76.836,12.027,12.027,0,0,0,0,1.3.828.828,0,0,0,.647.787c.151.047.312.052.466.087a11.385,11.385,0,0,1,1.626.329c.23.609.446,1.177.667,1.765-.189.236-.37.466-.554.7-.227.283-.466.559-.679.854A.888.888,0,0,0,2.2,16.493a10.568,10.568,0,0,0,.816.915.842.842,0,0,0,1.116.157c.181-.1.35-.21.524-.318l1.2-.743c.437.224.83.452,1.241.635s.842.32,1.261.478c.038.186.073.341.1.5.087.492.157.988.262,1.477a.809.809,0,0,0,.848.728c.431.017.865.015,1.3-.006a.781.781,0,0,0,.749-.591c.061-.192.079-.4.125-.6a13.164,13.164,0,0,1,.28-1.492l2.141-.88c.192.131.4.268.6.411.335.236.661.484,1,.708a.863.863,0,0,0,1.232-.082,10.682,10.682,0,0,0,.772-.769.9.9,0,0,0,.093-1.238c-.277-.417-.58-.813-.865-1.224-.1-.146-.184-.306-.245-.408.3-.728.583-1.4.9-2.15.274-.05.565-.1.854-.154.335-.058.676-.1,1.008-.166a.891.891,0,0,0,.813-.935C20.329,10.352,20.332,9.964,20.311,9.58ZM10.188,14a3.779,3.779,0,0,1-3.8-3.825,3.812,3.812,0,1,1,7.624.023A3.769,3.769,0,0,1,10.188,14Z" transform="translate(0 -0.001)"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.342 18.853"><defs><style>.a{fill:#6aeac0;}</style></defs><g transform="translate(34.55 55.562) rotate(180)"><g transform="translate(22.05 41.044)"><path class="a" d="M2.822,2.39h.817l3.889-.011c.69,0,1.381,0,2.071,0a.836.836,0,0,1,.1,1.668,1.591,1.591,0,0,1-.187.007q-4.277,0-8.55,0a1.1,1.1,0,0,1-.511-.093A.811.811,0,0,1,.228,2.67Q1.426,1.444,2.642.233A.8.8,0,0,1,3.777.24a.8.8,0,0,1,.015,1.127c-.295.31-.6.612-.907.918a.4.4,0,0,1-.1.056A.407.407,0,0,1,2.822,2.39Z" transform="translate(6.505 0) rotate(90)"/><path class="a" d="M2.822,1.664h.817l3.889.011c.69,0,1.381,0,2.071,0A.836.836,0,0,0,9.7.011,1.591,1.591,0,0,0,9.517,0Q5.24,0,.967,0A1.1,1.1,0,0,0,.455.093.811.811,0,0,0,.228,1.385q1.2,1.226,2.415,2.437A.808.808,0,0,0,3.792,2.687c-.295-.31-.6-.612-.907-.918a.4.4,0,0,0-.1-.056A.407.407,0,0,0,2.822,1.664Z" transform="translate(4.054 0) rotate(90)"/></g><g transform="translate(16.208 36.709)"><g transform="translate(0 0)"><path class="a" d="M9.568,0c.192.018.384.033.572.054a8.642,8.642,0,0,1,3.97,1.44,9.218,9.218,0,0,1,4.013,5.978,9.494,9.494,0,0,1-.923,6.481.923.923,0,0,0-.047.1.812.812,0,0,1-.919.5.847.847,0,0,1-.666-.85.325.325,0,0,1,.029-.13c.159-.38.322-.76.478-1.143a7.694,7.694,0,0,0,.59-3.456,7.687,7.687,0,0,0-2.4-5.236,7.093,7.093,0,0,0-4.071-1.979A7.2,7.2,0,0,0,4.249,3.579a7.447,7.447,0,0,0-2.5,4.614,7.65,7.65,0,0,0,1.925,6.492,7.156,7.156,0,0,0,4.429,2.377,7.222,7.222,0,0,0,5.453-1.39.387.387,0,0,0,.058-.051.8.8,0,0,1,.9-.17.871.871,0,0,1,.5.8.711.711,0,0,1-.275.572,8.448,8.448,0,0,1-4.48,1.954,8.725,8.725,0,0,1-6.846-2A9.249,9.249,0,0,1,.182,11.337a9.4,9.4,0,0,1-.127-3A9.343,9.343,0,0,1,2.36,3.108,8.962,8.962,0,0,1,7.234.214a8.521,8.521,0,0,1,1.419-.2A1.044,1.044,0,0,0,8.758,0C9.029,0,9.3,0,9.568,0Z" transform="translate(0.01)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.342 18.853"><defs><style>.a{fill:#ff6c6c;}</style></defs><g transform="translate(-16.208 -36.709)"><g transform="translate(22.05 41.044)"><path class="a" d="M2.822,2.39h.817l3.889-.011c.69,0,1.381,0,2.071,0a.836.836,0,0,1,.1,1.668,1.591,1.591,0,0,1-.187.007q-4.277,0-8.55,0a1.1,1.1,0,0,1-.511-.093A.811.811,0,0,1,.228,2.67Q1.426,1.444,2.642.233A.8.8,0,0,1,3.777.24a.8.8,0,0,1,.015,1.127c-.295.31-.6.612-.907.918a.4.4,0,0,1-.1.056A.407.407,0,0,1,2.822,2.39Z" transform="translate(6.505 0) rotate(90)"/><path class="a" d="M2.822,1.664h.817l3.889.011c.69,0,1.381,0,2.071,0A.836.836,0,0,0,9.7.011,1.591,1.591,0,0,0,9.517,0Q5.24,0,.967,0A1.1,1.1,0,0,0,.455.093.811.811,0,0,0,.228,1.385q1.2,1.226,2.415,2.437A.808.808,0,0,0,3.792,2.687c-.295-.31-.6-.612-.907-.918a.4.4,0,0,0-.1-.056A.407.407,0,0,0,2.822,1.664Z" transform="translate(4.054 0) rotate(90)"/></g><g transform="translate(16.208 36.709)"><g transform="translate(0 0)"><path class="a" d="M9.568,0c.192.018.384.033.572.054a8.642,8.642,0,0,1,3.97,1.44,9.218,9.218,0,0,1,4.013,5.978,9.494,9.494,0,0,1-.923,6.481.923.923,0,0,0-.047.1.812.812,0,0,1-.919.5.847.847,0,0,1-.666-.85.325.325,0,0,1,.029-.13c.159-.38.322-.76.478-1.143a7.694,7.694,0,0,0,.59-3.456,7.687,7.687,0,0,0-2.4-5.236,7.093,7.093,0,0,0-4.071-1.979A7.2,7.2,0,0,0,4.249,3.579a7.447,7.447,0,0,0-2.5,4.614,7.65,7.65,0,0,0,1.925,6.492,7.156,7.156,0,0,0,4.429,2.377,7.222,7.222,0,0,0,5.453-1.39.387.387,0,0,0,.058-.051.8.8,0,0,1,.9-.17.871.871,0,0,1,.5.8.711.711,0,0,1-.275.572,8.448,8.448,0,0,1-4.48,1.954,8.725,8.725,0,0,1-6.846-2A9.249,9.249,0,0,1,.182,11.337a9.4,9.4,0,0,1-.127-3A9.343,9.343,0,0,1,2.36,3.108,8.962,8.962,0,0,1,7.234.214a8.521,8.521,0,0,1,1.419-.2A1.044,1.044,0,0,0,8.758,0C9.029,0,9.3,0,9.568,0Z" transform="translate(0.01)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20.82 21.4"><defs><style>.a{fill:#3b3b3f;}</style></defs><g transform="translate(0)"><path class="a" d="M10.862,0c.218.021.435.037.649.062A9.81,9.81,0,0,1,16.017,1.7a10.464,10.464,0,0,1,4.555,6.786,10.777,10.777,0,0,1-1.047,7.356,1.048,1.048,0,0,0-.053.119.921.921,0,0,1-1.043.567.961.961,0,0,1-.756-.965.369.369,0,0,1,.033-.148c.181-.431.366-.863.542-1.3a8.733,8.733,0,0,0,.67-3.923,8.725,8.725,0,0,0-2.723-5.944A8.051,8.051,0,0,0,11.573,2,8.176,8.176,0,0,0,4.824,4.062,8.452,8.452,0,0,0,1.99,9.3a8.683,8.683,0,0,0,2.185,7.369,8.123,8.123,0,0,0,5.028,2.7,8.2,8.2,0,0,0,6.19-1.577.44.44,0,0,0,.066-.058.909.909,0,0,1,1.027-.193.988.988,0,0,1,.571.908.807.807,0,0,1-.312.649,9.59,9.59,0,0,1-5.085,2.218,9.9,9.9,0,0,1-7.771-2.271,10.5,10.5,0,0,1-3.68-6.174A10.675,10.675,0,0,1,.064,9.46,10.6,10.6,0,0,1,2.68,3.528,10.173,10.173,0,0,1,8.213.242,9.672,9.672,0,0,1,9.823.021,1.185,1.185,0,0,0,9.942,0C10.25,0,10.554,0,10.862,0Z" transform="translate(0.01)"/><path class="a" d="M14.3,17.106h.9l4.28-.012c.76,0,1.52,0,2.28,0a.92.92,0,0,1,.115,1.836,1.751,1.751,0,0,1-.205.008q-4.707,0-9.41,0a1.214,1.214,0,0,1-.563-.1.893.893,0,0,1-.251-1.421q1.318-1.349,2.658-2.682a.889.889,0,0,1,1.265,1.249c-.324.341-.661.674-1,1.01a.436.436,0,0,1-.115.062A.448.448,0,0,1,14.3,17.106Z" transform="translate(-6.589 -8.53)"/><path class="a" d="M19.609,29.83h-.14q-3.654.006-7.3.008a.932.932,0,0,1-.928-.641A.915.915,0,0,1,12.1,28c1.109-.008,2.218,0,3.331,0h6.334a.908.908,0,0,1,.949.858.881.881,0,0,1-.283.694c-.32.32-.641.645-.961.969l-1.631,1.643A.892.892,0,0,1,18.7,32.3a.877.877,0,0,1-.164-1.339c.32-.341.657-.67.99-1a1.143,1.143,0,0,1,.111-.086A.094.094,0,0,1,19.609,29.83Z" transform="translate(-6.592 -16.498)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20.82 21.4"><defs><style>.a{fill:#f4b728;}</style></defs><path class="a" d="M10.862,0c.218.021.435.037.649.062A9.81,9.81,0,0,1,16.017,1.7a10.464,10.464,0,0,1,4.555,6.786,10.777,10.777,0,0,1-1.047,7.356,1.048,1.048,0,0,0-.053.119.921.921,0,0,1-1.043.567.961.961,0,0,1-.756-.965.369.369,0,0,1,.033-.148c.181-.431.366-.863.542-1.3a8.733,8.733,0,0,0,.67-3.923,8.725,8.725,0,0,0-2.723-5.944A8.051,8.051,0,0,0,11.573,2,8.176,8.176,0,0,0,4.824,4.062,8.452,8.452,0,0,0,1.99,9.3a8.683,8.683,0,0,0,2.185,7.369,8.123,8.123,0,0,0,5.028,2.7,8.2,8.2,0,0,0,6.19-1.577.44.44,0,0,0,.066-.058.909.909,0,0,1,1.027-.193.988.988,0,0,1,.571.908.807.807,0,0,1-.312.649,9.59,9.59,0,0,1-5.085,2.218,9.9,9.9,0,0,1-7.771-2.271,10.5,10.5,0,0,1-3.68-6.174A10.675,10.675,0,0,1,.064,9.46,10.6,10.6,0,0,1,2.68,3.528,10.173,10.173,0,0,1,8.213.242,9.672,9.672,0,0,1,9.823.021,1.185,1.185,0,0,0,9.942,0C10.25,0,10.554,0,10.862,0Z" transform="translate(0.01)"/><path class="a" d="M14.3,17.106h.9l4.28-.012c.76,0,1.52,0,2.28,0a.92.92,0,0,1,.115,1.836,1.751,1.751,0,0,1-.205.008q-4.707,0-9.41,0a1.214,1.214,0,0,1-.563-.1.893.893,0,0,1-.251-1.421q1.318-1.349,2.658-2.682a.889.889,0,0,1,1.265,1.249c-.324.341-.661.674-1,1.01a.436.436,0,0,1-.115.062A.448.448,0,0,1,14.3,17.106Z" transform="translate(-6.589 -8.53)"/><path class="a" d="M19.609,29.83h-.14q-3.654.006-7.3.008a.932.932,0,0,1-.928-.641A.915.915,0,0,1,12.1,28c1.109-.008,2.218,0,3.331,0h6.334a.908.908,0,0,1,.949.858.881.881,0,0,1-.283.694c-.32.32-.641.645-.961.969l-1.631,1.643A.892.892,0,0,1,18.7,32.3a.877.877,0,0,1-.164-1.339c.32-.341.657-.67.99-1a1.143,1.143,0,0,1,.111-.086A.094.094,0,0,1,19.609,29.83Z" transform="translate(-6.592 -16.498)"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19.818" height="35.161" viewBox="0 0 19.818 35.161"><defs><style>.a{fill:#FCD639;}</style></defs><path class="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>

After

Width:  |  Height:  |  Size: 349 B

View File

@ -0,0 +1,8 @@
---
name: Home
route: /
---
# UI Components
Use the sidebar menu to browse the styleguide of React components available for the ZEC Wallet application.

126
app/components/button.js Normal file
View File

@ -0,0 +1,126 @@
// @flow
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';
import type { ElementProps } from 'react';
const DefaultButton = styled.button`
align-items: center;
display: flex;
justify-content: center;
padding: 10px 30px;
font-family: ${props => props.theme.fontFamily};
font-weight: ${props => props.theme.fontWeight.bold};
font-size: ${props => `${props.theme.fontSize.regular}em`};
cursor: pointer;
outline: none;
min-width: 100px;
border-radius: 100px;
transition: background-color 0.1s ${props => props.theme.colors.transitionEase};
`;
const Primary = styled(DefaultButton)`
background-color: ${props => props.theme.colors.primary};
color: ${props => props.theme.colors.secondary};
border: none;
&:hover {
background-color: ${props => darken(0.1, props.theme.colors.primary(props))};
}
&:disabled {
background-color: ${props => props.theme.colors.buttonBorderColor};
cursor: not-allowed;
opacity: 0.8;
}
`;
const Secondary = styled(DefaultButton)`
background-color: transparent;
color: ${props => props.theme.colors.secondary};
border: 2px solid ${props => props.theme.colors.buttonBorderColor};
&:hover {
border-color: ${props => props.theme.colors.primary};
}
&:disabled {
background-color: Transparent;
cursor: not-allowed;
color: ${props => props.theme.colors.buttonBorderColor};
&:hover {
border-color: ${props => props.theme.colors.buttonBorderColor};
}
}
`;
const Icon = styled.img`
height: 12px;
width: 12px;
margin-right: 10px;
`;
type Props = {
...ElementProps<'button'>,
label: string,
onClick?: () => void,
to?: ?string,
variant?: 'primary' | 'secondary',
disabled?: boolean,
icon?: string | null,
className?: string,
isLoading?: boolean,
};
export const Button = ({
onClick,
label,
to,
variant,
disabled,
icon,
className,
isLoading,
id,
}: Props) => {
if (to && onClick) throw new Error('Should define either "to" or "onClick"');
const props = {
onClick,
disabled: disabled || isLoading,
icon: null,
className,
id,
};
const buttonLabel = isLoading ? 'Loading...' : label;
const component = variant === 'primary' ? (
<Primary {...props}>
{icon ? <Icon src={icon} /> : null}
{buttonLabel}
</Primary>
) : (
<Secondary {...props}>
{icon ? <Icon src={icon} /> : null}
{buttonLabel}
</Secondary>
);
return to ? <Link to={String(to)}>{component}</Link> : component;
};
Button.defaultProps = {
to: '',
icon: null,
variant: 'primary',
onClick: () => {},
disabled: false,
className: '',
isLoading: false,
};

90
app/components/button.mdx Normal file
View File

@ -0,0 +1,90 @@
---
name: Button
---
import { Playground, PropsTable } from 'docz'
import { Button } from './button.js'
import { DoczWrapper } from '../theme.js'
# Button
<PropsTable of={Button} />
## Primary
<Playground>
<div style={{ backgroundColor: '#000', padding: '20px' }}>
<DoczWrapper>
{() => (
<Button
label='Click me!'
onClick={() => alert('Clicked')}
/>
)}
</DoczWrapper>
</div>
</Playground>
## Secondary
<Playground>
<div style={{ backgroundColor: '#000', padding: '20px' }}>
<DoczWrapper>
{() => (
<Button
label='Click me!'
onClick={() => alert('Clicked')}
variant='secondary'
/>
)}
</DoczWrapper>
</div>
</Playground>
## Primary Disabled
<Playground>
<div style={{ backgroundColor: '#000', padding: '20px' }}>
<DoczWrapper>
{() => (
<Button
label='Cannot click'
disabled
/>
)}
</DoczWrapper>
</div>
</Playground>
## Secondary Disabled
<Playground>
<div style={{ backgroundColor: '#000', padding: '20px' }}>
<DoczWrapper>
{() => (
<Button
label='Cannot click'
disabled
variant='secondary'
/>
)}
</DoczWrapper>
</div>
</Playground>
## Link Button
<Playground>
<div style={{ backgroundColor: '#000', padding: '20px' }}>
<DoczWrapper>
{() => (
<Button
label='Click me!'
isLink
to='/my-route'
/>
)}
</DoczWrapper>
</div>
</Playground>

View File

@ -0,0 +1,50 @@
// @flow
import React, { PureComponent } from 'react';
import { Button } from './button';
type Props = {
text: string,
className?: string,
};
type State = { copied: boolean };
export class Clipboard extends PureComponent<Props, State> {
static defaultProps = {
className: '',
};
state = {
copied: false,
};
handleClick = () => {
const { text } = this.props;
const el = document.createElement('textarea');
el.value = text;
if (document.body) document.body.appendChild(el);
el.select();
document.execCommand('copy');
if (document.body) document.body.removeChild(el);
this.setState({ copied: true });
};
render() {
const { className } = this.props;
const { copied } = this.state;
return (
<Button
label={copied ? 'Copied!' : 'Copy!'}
className={className}
onClick={this.handleClick}
disabled={copied}
/>
);
}
}

33
app/components/column.js Normal file
View File

@ -0,0 +1,33 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import type { Node, ElementProps } from 'react';
const Flex = styled.div`
display: flex;
flex-direction: column;
align-items: ${props => props.alignItems};
justify-content: ${props => props.justifyContent};
${props => props.width && `width: ${props.width};`}
`;
type Props = {
...ElementProps<'div'>,
alignItems?: string,
justifyContent?: string,
className?: string,
children: Node,
width?: string,
};
export const ColumnComponent = ({ children, ...props }: Props) => (
<Flex {...props}>{React.Children.map(children, ch => ch)}</Flex>
);
ColumnComponent.defaultProps = {
alignItems: 'flex-start',
justifyContent: 'flex-start',
className: '',
width: '',
};

View File

@ -0,0 +1,119 @@
// @flow
import React, { type Element } from 'react';
import styled from 'styled-components';
import { TextComponent } from './text';
import { Button } from './button';
import { Divider } from './divider';
import { ModalComponent } from './modal';
import CloseIcon from '../assets/images/close_icon.svg';
const Wrapper = styled.div`
display: flex;
width: ${props => `${props.width}px`};
background-color: ${props => props.theme.colors.background};
flex-direction: column;
align-items: center;
border-radius: 6px;
box-shadow: 0px 0px 30px 0px black;
position: relative;
`;
const CloseIconWrapper = styled.div`
display: flex;
width: 100%;
align-items: flex-end;
justify-content: flex-end;
position: absolute;
`;
const TitleWrapper = styled.div`
margin-top: 20px;
margin-bottom: 20px;
`;
const CloseIconImg = styled.img`
width: 16px;
height: 16px;
margin-top: 12px;
margin-right: 12px;
cursor: pointer;
`;
const Btn = styled(Button)`
width: 95%;
margin-bottom: 10px;
`;
type Props = {
renderTrigger: (() => void) => Element<*>,
title: string,
onConfirm: () => void,
onClose?: () => void,
showButtons?: boolean,
width?: number,
isLoading?: boolean,
children: (() => void) => Element<*>,
};
export const ConfirmDialogComponent = ({
children,
title,
onConfirm,
onClose,
renderTrigger,
showButtons,
isLoading,
width,
}: Props) => {
const handleClose = toggle => () => {
toggle();
if (onClose) onClose();
};
return (
<ModalComponent
id='confirm-dialog-modal-wrapper'
renderTrigger={renderTrigger}
closeOnBackdropClick={false}
closeOnEsc={false}
>
{toggle => (
<Wrapper width={width}>
<CloseIconWrapper>
<CloseIconImg src={CloseIcon} onClick={handleClose(toggle)} />
</CloseIconWrapper>
<TitleWrapper>
<TextComponent value={title} align='center' />
</TitleWrapper>
<Divider opacity={0.3} />
{children(handleClose(toggle))}
{showButtons && (
<>
<Btn
id='confirm-modal-button'
label='Confirm'
onClick={onConfirm}
isLoading={isLoading}
/>
<Btn
label='Cancel'
onClick={handleClose(toggle)}
variant='secondary'
disabled={isLoading}
/>
</>
)}
</Wrapper>
)}
</ModalComponent>
);
};
ConfirmDialogComponent.defaultProps = {
showButtons: true,
width: 460,
isLoading: false,
onClose: () => {},
};

View File

@ -0,0 +1,31 @@
---
name: Confirm Dialog
---
import { Playground, PropsTable } from 'docz'
import { Button } from './button.js'
import { ConfirmDialogComponent } from './confirm-dialog.js'
import { DoczWrapper } from '../theme.js'
# Confirm Dialog
## Properties
<PropsTable of={ConfirmDialogComponent} />
## Basic Usage
<Playground>
<DoczWrapper>
{() => (
<ConfirmDialogComponent
title="Confirm example"
onConfirm={() => alert('Confirm')}
renderTrigger={toggle => <button onClick={toggle}> Open! </button>}
>
{toggle => <div>Confirm content</div>}
</ConfirmDialogComponent>
)}
</DoczWrapper>
</Playground>

11
app/components/divider.js Normal file
View File

@ -0,0 +1,11 @@
// @flow
import styled from 'styled-components';
export const Divider = styled.div`
width: 100%;
height: 1px;
background-color: ${props => props.color || props.theme.colors.text};
opacity: ${props => props.opacity || 1};
margin-bottom: ${props => props.marginBottom || 0};
margin-top: ${props => props.marginTop || 0};
`;

136
app/components/dropdown.js Normal file
View File

@ -0,0 +1,136 @@
// @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';
import truncateAddress from '../utils/truncateAddress';
/* 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})`};
border-radius: ${props => props.theme.boxBorderRadius};
margin-left: -10px;
max-width: 400px;
overflow: hidden;
`;
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;
cursor: pointer;
font-weight: 700;
overflow: hidden;
width: 100%;
text-align: left;
&:hover {
opacity: 1;
}
&:disabled {
cursor: default;
&:hover {
opacity: 1;
}
}
&:last-child {
border-bottom: none;
}
`;
const OptionItem = styled(MenuItem)`
&:hover {
background-color: #F9D114;
}
`;
const Option = styled(TextComponent)`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
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,
truncate?: boolean,
};
type State = {
isOpen: boolean,
};
export class DropdownComponent extends Component<Props, State> {
state = {
isOpen: false,
};
static defaultProps = {
label: null,
truncate: false,
};
render() {
const { isOpen } = this.state;
const {
label, options, truncate, renderTrigger,
} = this.props;
const body = [
<ClickOutside
onClickOutside={() => this.setState(() => ({ isOpen: false }))}
>
<MenuWrapper>
{label && (
<MenuItem disabled isGroupLabel>
<TextComponent value={label} isBold />
</MenuItem>
)}
{options.map(({ label: optionLabel, onClick }) => (
<OptionItem onClick={onClick} key={optionLabel}>
<Option value={truncate ? truncateAddress(optionLabel) : optionLabel} />
</OptionItem>
))}
</MenuWrapper>
</ClickOutside>,
];
return (
<PopoverWithStyle
isOpen={isOpen}
preferPlace='below'
enterExitTransitionDurationMs={0}
tipSize={7}
body={body}
>
{renderTrigger(
() => this.setState(state => ({ isOpen: !state.isOpen })),
isOpen,
)}
</PopoverWithStyle>
);
}
}

View File

@ -0,0 +1,39 @@
---
name: Dropdown
---
import { Playground, PropsTable } from 'docz'
import { Button } from './button.js'
import { DropdownComponent } from './dropdown.js'
import { DoczWrapper } from '../theme.js'
# Dropdown
## Properties
<PropsTable of={DropdownComponent} />
## Basic Usage
<Playground>
<DoczWrapper>
{() => (
<div style={{ height: '500px' }}>
<DropdownComponent
label='Addresses'
renderTrigger={toggleVisibility => (
<Button
label='Show Dropdown'
onClick={toggleVisibility}
/>
)}
options={[
{ label: 'asbh1yeasbdh23848asdasd', onClick: console.log },
{ label: 'urtyruhjr374hbfdjdhuh', onClick: console.log },
]}
/>
</div>
)}
</DoczWrapper>
</Playground>

View File

@ -0,0 +1,19 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import { TextComponent } from './text';
const Wrapper = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 30px 0;
`;
export const EmptyTransactionsComponent = () => (
<Wrapper>
<TextComponent value='No transactions!' />
</Wrapper>
);

75
app/components/header.js Normal file
View File

@ -0,0 +1,75 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import { ZcashLogo } from './zcash-logo';
import { TextComponent } from './text';
import { Divider } from './divider';
import { RowComponent } from './row';
import { StatusPill } from './status-pill';
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 TitleRow = styled(RowComponent)`
align-items: center;
justify-content: space-between;
width: 100%;
`;
const Title = styled(TextComponent)`
font-size: ${props => `${props.theme.fontSize.large}em`};
margin-top: 10px;
margin-bottom: 10px;
text-transform: capitalize;
letter-spacing: 0.25px;
font-weight: ${props => props.theme.fontWeight.bold};
`;
type Props = {
title: string,
};
export const HeaderComponent = ({ title }: Props) => (
<Wrapper id='header'>
<LogoWrapper>
<ZcashLogo />
</LogoWrapper>
<TitleWrapper>
<TitleRow alignItems='center' justifyContent='space-around'>
<Title value={title} />
<StatusPill />
</TitleRow>
<Divider opacity={0.1} />
</TitleWrapper>
</Wrapper>
);

View File

@ -0,0 +1,9 @@
// @flow
import styled from 'styled-components';
import { TextComponent } from './text';
export const InputLabelComponent = styled(TextComponent)`
margin: ${props => props.marginTop || '20px'} 0 8.5px 0;
font-weight: ${props => props.theme.fontWeight.bold};
`;

96
app/components/input.js Normal file
View File

@ -0,0 +1,96 @@
// @flow
import React, { type Element } from 'react';
import styled from 'styled-components';
import theme from '../theme';
const getDefaultStyles = t => styled[t]`
border-radius: ${props => props.theme.boxBorderRadius};
border: none;
background-color: ${props => props.bgColor || props.theme.colors.inputBackground};
color: ${props => props.theme.colors.text};
padding: 15px;
padding-right: ${props => (props.withRightElement ? '85px' : '15px')};
width: 100%;
outline: none;
font-family: ${props => props.theme.fontFamily};
::placeholder {
opacity: 0.5;
}
`;
const Wrapper = styled.div`
position: relative;
`;
const RightElement = styled.div`
position: absolute;
top: 15px;
right: 15px;
`;
const Input = getDefaultStyles('input');
const Textarea = getDefaultStyles('textarea');
type Props = {
inputType?: 'input' | 'textarea',
value: string,
onChange?: string => void,
onFocus?: (SyntheticFocusEvent<HTMLInputElement>) => void,
rows?: number,
disabled?: boolean,
type?: string,
step?: number,
name?: string,
renderRight?: () => Element<*> | null,
bgColor?: string,
};
export const InputComponent = ({
inputType,
bgColor,
onChange = () => {},
renderRight = () => null,
...props
}: Props) => {
const rightElement = renderRight();
const inputTypes = {
input: () => (
<Input
onChange={evt => onChange(evt.target.value)}
withRightElement={Boolean(rightElement)}
bgColor={bgColor}
{...props}
/>
),
textarea: () => (
<Textarea onChange={evt => onChange(evt.target.value)} bgColor={bgColor} {...props} />
),
};
if (!Object.keys(inputTypes).find(key => key === inputType)) {
throw new Error(`Invalid input type: ${String(inputType)}`);
}
return (
<Wrapper>
{inputTypes[inputType || 'input']()}
{rightElement && <RightElement>{rightElement}</RightElement>}
</Wrapper>
);
};
InputComponent.defaultProps = {
inputType: 'input',
rows: 4,
disabled: false,
type: 'text',
name: '',
renderRight: () => null,
onChange: () => {},
onFocus: () => {},
step: 1,
bgColor: theme.colors.inputBackground,
};

43
app/components/input.mdx Normal file
View File

@ -0,0 +1,43 @@
---
name: Input
---
import { Playground, PropsTable } from 'docz'
import { InputComponent } from './input.js'
import { DoczWrapper } from '../theme.js'
# Input
## Properties
<PropsTable of={InputComponent} />
## Text Input
<Playground>
<DoczWrapper>
{() => (
<InputComponent
inputType='input'
value='Hello World!'
onChange={console.log}
/>
)}
</DoczWrapper>
</Playground>
## Textarea
<Playground>
<DoczWrapper>
{() => (
<InputComponent
inputType='textarea'
value='I am Zcash Electron Wallet'
onChange={console.log}
rows={10}
/>
)}
</DoczWrapper>
</Playground>

25
app/components/layout.js Normal file
View File

@ -0,0 +1,25 @@
// @flow
import React from 'react';
import styled from 'styled-components';
const Layout = styled.div`
display: flex;
flex-direction: column;
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};
overflow: auto;
`;
type Props = {
chidren: any, // eslint-disable-line
};
export const LayoutComponent = (props: Props) => {
// $FlowFixMe
const { children } = props; // eslint-disable-line
return <Layout id='layout'>{children}</Layout>;
};

View File

@ -0,0 +1,95 @@
// @flow
import React, { PureComponent } from 'react';
import styled from 'styled-components';
import { Transition, animated } from 'react-spring';
import CircleProgressComponent from 'react-circle';
import { TextComponent } from './text';
import zcashLogo from '../assets/images/zcash-simple-icon.svg';
import theme from '../theme';
const Wrapper = styled.div`
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: ${props => props.theme.colors.cardBackgroundColor};
`;
const CircleWrapper = styled.div`
width: 125px;
height: 125px;
position: relative;
margin-bottom: 25px;
`;
const Logo = styled.img`
z-index: 10;
position: absolute;
width: 50px;
height: 50px;
top: calc(50% - 25px);
left: calc(50% - 25px);
`;
type Props = {
progress: number,
};
type State = {
start: boolean,
};
const TIME_DELAY_ANIM = 100;
export class LoadingScreen extends PureComponent<Props, State> {
state = { start: false };
componentDidMount() {
setTimeout(() => {
this.setState(() => ({ start: true }));
}, TIME_DELAY_ANIM);
}
render() {
const { start } = this.state;
const { progress } = this.props;
return (
<Wrapper>
<Transition
native
items={start}
enter={[{ height: 'auto', opacity: 1 }]}
leave={{ height: 0, opacity: 0 }}
from={{
position: 'absolute',
overflow: 'hidden',
height: 0,
opacity: 0,
}}
>
{() => props => (
<animated.div style={props} id='loading-screen'>
<CircleWrapper>
<Logo src={zcashLogo} alt='Zcash logo' />
<CircleProgressComponent
progress={progress}
responsive
showPercentage={false}
progressColor={theme.colors.activeItem}
bgColor={theme.colors.inactiveItem}
/>
</CircleWrapper>
<TextComponent value='ZEC Wallet Starting' />
</animated.div>
)}
</Transition>
</Wrapper>
);
}
}

117
app/components/modal.js Normal file
View File

@ -0,0 +1,117 @@
// @flow
import React, { PureComponent, Fragment, type Element } from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
const ModalWrapper = styled.div`
width: 100vw;
height: 100vh;
position: fixed;
display: flex;
align-items: center;
justify-content: center;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.5);
`;
const ChildrenWrapper = styled.div`
z-index: 90;
`;
type Props = {
renderTrigger: (() => void) => Element<*>,
children: (() => void) => Element<*>,
closeOnBackdropClick?: boolean,
closeOnEsc?: boolean,
};
type State = {
isVisible: boolean,
};
const modalRoot = document.getElementById('modal-root');
export class ModalComponent extends PureComponent<Props, State> {
element = document.createElement('div');
static defaultProps = {
closeOnBackdropClick: true,
closeOnEsc: true,
};
state = {
isVisible: false,
};
componentDidMount() {
const { closeOnEsc } = this.props;
if (closeOnEsc) {
window.addEventListener('keydown', this.handleEscPress);
}
}
componentWillUnmount() {
const { closeOnEsc } = this.props;
if (closeOnEsc) {
window.removeEventListener('keydown', this.handleEscPress);
}
}
handleEscPress = (event: Object) => {
const { isVisible } = this.state;
if (event.key === 'Escape' && isVisible) {
this.close();
}
};
open = () => {
this.setState(
() => ({ isVisible: true }),
() => {
if (modalRoot) modalRoot.appendChild(this.element);
},
);
};
close = () => {
this.setState(
() => ({ isVisible: false }),
() => {
if (modalRoot) modalRoot.removeChild(this.element);
},
);
};
render() {
const { renderTrigger, children, closeOnBackdropClick } = this.props;
const { isVisible } = this.state;
const toggleVisibility = isVisible ? this.close : this.open;
return (
<Fragment>
{renderTrigger(toggleVisibility)}
{isVisible
? createPortal(
<ModalWrapper
id='modal-portal-wrapper'
onClick={(event) => {
if (
closeOnBackdropClick
&& event.target.id === 'modal-portal-wrapper'
) this.close();
}}
>
<ChildrenWrapper>{children(toggleVisibility)}</ChildrenWrapper>
</ModalWrapper>,
this.element,
)
: null}
</Fragment>
);
}
}

57
app/components/modal.mdx Normal file
View File

@ -0,0 +1,57 @@
---
name: Modal
---
import { Playground, PropsTable } from 'docz'
import { ModalComponent } from './modal.js'
# Modal
## Properties
<PropsTable of={ModalComponent} />
## Basic usage
<Playground>
<ModalComponent
renderTrigger={toggleVisibility => (
<button type="button" onClick={toggleVisibility}>
Open Modal
</button>
)}
>
{toggleVisibility => (
<div style={{ padding: '50px', backgroundColor: 'white' }}>
Modal Content
<button type="button" onClick={toggleVisibility}>
Close Modal
</button>
</div>
)}
</ModalComponent>
</Playground>
## Don't close with ESC or backdrop click
<Playground>
<ModalComponent
closeOnEsc={false}
closeOnBackdropClick={false}
renderTrigger={toggleVisibility => (
<button type="button" onClick={toggleVisibility}>
Open Modal
</button>
)}
>
{toggleVisibility => (
<div style={{ padding: '50px', backgroundColor: 'white' }}>
Modal Content
<button type="button" onClick={toggleVisibility}>
Close Modal
</button>
</div>
)}
</ModalComponent>
</Playground>

17
app/components/qrcode.js Normal file
View File

@ -0,0 +1,17 @@
// @flow
import React from 'react';
import QR from 'qrcode.react';
type Props = {
value: string,
size?: number,
};
export const QRCode = ({ value, size }: Props) => (
<QR value={value} size={size} />
);
QRCode.defaultProps = {
size: 128,
};

28
app/components/qrcode.mdx Normal file
View File

@ -0,0 +1,28 @@
---
name: QRCode
---
import { Playground, PropsTable } from 'docz'
import { QRCode } from './qrcode.js'
# QRCode
## Properties
<PropsTable of={QRCode} />
## Basic usage
<Playground>
<QRCode value='https://z.cash.foundation' />
</Playground>
## Custom size
<Playground>
<QRCode
value='https://z.cash.foundation'
size={500}
/>
</Playground>

30
app/components/row.js Normal file
View File

@ -0,0 +1,30 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import type { Node, ElementProps } from 'react';
const Flex = styled.div`
display: flex;
flex-direction: row;
align-items: ${props => props.alignItems};
justify-content: ${props => props.justifyContent};
`;
type Props = {
...ElementProps<'div'>,
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: '',
};

192
app/components/select.js Normal file
View File

@ -0,0 +1,192 @@
// @flow
import React, { PureComponent } from 'react';
import styled from 'styled-components';
import { TextComponent } from './text';
import ChevronUp from '../assets/images/chevron-up.svg';
import ChevronDown from '../assets/images/chevron-down.svg';
import theme from '../theme';
/* eslint-disable max-len */
const SelectWrapper = styled.div`
align-items: center;
display: flex;
flex-direction: row;
border-radius: ${props => props.theme.boxBorderRadius};
border: none;
background-color: ${props => props.bgColor || props.theme.colors.inputBackground};
color: ${props => props.theme.colors.text};
width: 100%;
outline: none;
font-family: ${props => props.theme.fontFamily};
cursor: pointer;
position: relative;
${props => props.isOpen
&& `border-${props.placement}-left-radius: 0; border-${props.placement}-right-radius: 0;`}
`;
/* eslint-enable max-len */
const ValueWrapper = styled.div`
width: 95%;
padding: 13px;
opacity: ${props => (props.hasValue ? '1' : '0.2')};
text-transform: capitalize;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const SelectMenuButtonWrapper = styled.button`
cursor: pointer;
outline: none;
border: none;
background-color: transparent;
width: 50px;
height: 100%;
padding: 13px;
border-left: ${props => `1px solid ${props.theme.colors.background}`};
`;
/* eslint-disable max-len */
const SelectMenuButton = styled.button`
padding: 3px 7px;
outline: none;
background-color: transparent;
border: 1px solid ${props => (props.isOpen ? props.theme.colors.primary : '#29292D')};
border-radius: 100%;
`;
/* eslint-enable max-len */
const Icon = styled.img`
width: 10px;
height: 10px;
`;
const OptionsWrapper = styled.div`
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
${props => `${props.placement}: ${`-${props.optionsAmount * 40}px`}`};
overflow-y: auto;
`;
const Option = styled.button`
border: none;
background: none;
height: 40px;
background-color: #5d5d5d;
cursor: pointer;
z-index: 99;
text-transform: capitalize;
padding: 5px 10px;
border-bottom: 1px solid #4e4b4b;
&:hover {
background-color: ${props => props.theme.colors.background};
}
&:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: 1px solid transparent;
}
`;
type Props = {
value: string,
options: { value: string, label: string }[],
placeholder?: string,
onChange: string => void,
placement?: 'top' | 'bottom',
bgColor?: string,
};
type State = {
isOpen: boolean,
};
export class SelectComponent extends PureComponent<Props, State> {
state = {
isOpen: false,
};
static defaultProps = {
placeholder: '',
placement: 'bottom',
bgColor: theme.colors.inputBackground,
};
onSelect = (value: string) => {
const { onChange } = this.props;
this.setState(() => ({ isOpen: false }), () => onChange(value));
};
handleClickOutside = (event: Object) => {
const { isOpen } = this.state;
if (isOpen && event.target.id !== 'select-options-wrapper') this.setState(() => ({ isOpen: false }));
};
getSelectedLabel = (value: string) => {
const { options } = this.props;
const option = options.find(opt => opt.value === value);
if (option) return option.label;
};
getSelectIcon = () => {
const { placement } = this.props;
const { isOpen } = this.state;
if (placement === 'top') {
return !isOpen ? ChevronUp : ChevronDown;
}
return !isOpen ? ChevronDown : ChevronUp;
};
render() {
const {
value, options, placeholder, placement, bgColor,
} = this.props;
const { isOpen } = this.state;
return (
<SelectWrapper
id='select-component'
isOpen={isOpen}
placement={placement}
onClick={() => this.setState(() => ({ isOpen: !isOpen }))}
bgColor={bgColor}
>
<ValueWrapper hasValue={Boolean(value)}>
{this.getSelectedLabel(value) || placeholder}
</ValueWrapper>
<SelectMenuButtonWrapper>
<SelectMenuButton isOpen={isOpen}>
<Icon src={this.getSelectIcon()} />
</SelectMenuButton>
</SelectMenuButtonWrapper>
{isOpen && (
<OptionsWrapper
id='select-options-wrapper'
optionsAmount={options.length}
placement={placement}
>
{options.map(({ label, value: optionValue }) => (
<Option
id={optionValue}
key={label + optionValue}
onClick={() => this.onSelect(optionValue)}
bgColor={bgColor}
>
<TextComponent value={label} />
</Option>
))}
</OptionsWrapper>
)}
</SelectWrapper>
);
}
}

93
app/components/sidebar.js Normal file
View File

@ -0,0 +1,93 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import { Link, type Location } from 'react-router-dom';
import { MENU_OPTIONS } from '../constants/sidebar';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
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-top: 15px;
position: relative;
`;
const StyledLink = styled(Link)`
color: ${props => (props.isActive
? props.theme.colors.sidebarItemActive
: props.theme.colors.sidebarItem)};
font-size: ${props => `${props.theme.fontSize.regular}em`};
text-decoration: none;
font-weight: ${props => props.theme.fontWeight.bold};
background-color: ${props => (props.isActive
? `${props.theme.colors.sidebarHoveredItem}`
: 'transparent')};
letter-spacing: 0.25px;
padding: 25px 20px;
height: 35px;
width: 100%;
display: flex;
align-items: center;
outline: none;
border-right: ${props => (props.isActive
? `3px solid ${props.theme.colors.sidebarItemActive}`
: 'none')};
cursor: pointer;
transition: all 0.03s ${props => props.theme.colors.transitionEase};
&:hover {
color: ${
/* eslint-disable-next-line max-len */
props => (props.isActive ? props.theme.colors.sidebarItemActive : '#ddd')
}
background-color: ${props => props.theme.colors.sidebarHoveredItem};
}
`;
const Icon = styled.img`
width: 17px;
height: 17px;
margin-right: 13px;
${StyledLink}:hover & {
filter: ${props => (props.isActive ? 'none' : 'brightness(300%)')};
}
`;
type MenuItem = {
route: string,
label: string,
icon: (isActive: boolean) => string,
};
type Props = {
options?: MenuItem[],
location: Location,
};
export const SidebarComponent = ({ options, location }: Props) => (
<Wrapper id='sidebar'>
{(options || []).map((item) => {
const isActive = location.pathname === item.route;
return (
<StyledLink isActive={isActive} key={item.route} to={item.route}>
<Icon
isActive={isActive}
src={item.icon(isActive)}
alt={`${item.route}`}
/>
{item.label}
</StyledLink>
);
})}
</Wrapper>
);
SidebarComponent.defaultProps = {
options: MENU_OPTIONS,
};

View File

@ -0,0 +1,26 @@
---
name: Sidebar
---
import { Playground, PropsTable } from 'docz'
import { SidebarComponent } from './sidebar.js'
import { DoczWrapper } from '../theme.js'
# Sidebar
## General
Sidebar component does not behave correctly inside Docz Styleguide as it is tighly coupled with the router.
## Properties
<PropsTable of={SidebarComponent} />
## Basic Usage
<Playground>
<DoczWrapper>
{() => <SidebarComponent location={{}} />}
</DoczWrapper>
</Playground>

View File

@ -0,0 +1,115 @@
// @flow
import React, { Component } from 'react';
import styled, { keyframes } from 'styled-components';
import eres from 'eres';
import { TextComponent } from './text';
import rpc from '../../services/api';
import readyIcon from '../assets/images/green_check.png';
import syncIcon from '../assets/images/sync_icon.png';
import errorIcon from '../assets/images/error_icon.png';
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const Wrapper = styled.div`
align-items: center;
display: flex;
background-color: #000;
border-radius: 27px;
padding: 8px 16px;
`;
const Icon = styled.img`
width: 12px;
height: 12px;
margin-right: 8px;
animation: 2s linear infinite;
animation-name: ${props => (props.animated ? rotate : 'none')};
`;
const StatusPillLabel = styled(TextComponent)`
color: ${props => props.theme.colors.statusPillLabel};
font-weight: ${props => props.theme.fontWeight.bold};
text-transform: uppercase;
font-size: 10px;
padding-top: 1px;
user-select: none;
`;
type Props = {};
type State = {
type: string,
icon: string,
progress: number,
isSyncing: boolean,
};
export class StatusPill extends Component<Props, State> {
timer: ?IntervalID = null;
state = {
type: 'syncing',
icon: syncIcon,
progress: 0,
isSyncing: true,
};
componentDidMount() {
this.timer = setInterval(() => {
this.getBlockchainStatus();
}, 2000);
}
componentWillUnmount() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
getBlockchainStatus = async () => {
const [blockchainErr, blockchaininfo] = await eres(rpc.getblockchaininfo());
const newProgress = blockchaininfo.verificationprogress * 100;
this.setState({
progress: newProgress,
...(newProgress > 99.99
? {
type: 'ready',
icon: readyIcon,
isSyncing: false,
}
: {}),
});
if (blockchainErr) {
this.setState(() => ({ type: 'error', icon: errorIcon }));
}
};
render() {
const {
type, icon, progress, isSyncing,
} = this.state;
const showPercent = isSyncing ? `(${progress.toFixed(2)}%)` : '';
return (
<Wrapper id='status-pill'>
<Icon src={icon} animated={isSyncing} />
<StatusPillLabel value={`${type} ${showPercent}`} />
</Wrapper>
);
}
}

View File

@ -0,0 +1,22 @@
---
name: StatusPill
---
import { Playground, PropsTable } from 'docz'
import { StatusPill } from './status-pill.js'
import { DoczWrapper } from '../theme.js'
# StatusPill
## Properties
<PropsTable of={StatusPill} />
## Basic Usage
<Playground>
<DoczWrapper>
{() => <StatusPill percent={83.} />}
</DoczWrapper>
</Playground>

50
app/components/text.js Normal file
View File

@ -0,0 +1,50 @@
// @flow
/* eslint-disable max-len */
import React from 'react';
import styled from 'styled-components';
import type { ElementProps } from 'react';
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 = {
...ElementProps<'p'>,
value: string,
isBold?: boolean,
color?: string,
className?: string,
size?: string | number,
align?: string,
};
export const TextComponent = ({
value, isBold, color, className, size, align, id,
}: Props) => (
<Text
id={id}
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.regular,
align: 'left',
};

View File

@ -1,81 +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>
);
}
}

View File

@ -1,58 +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>
);
}
}

View File

@ -1,56 +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>
);
}
}

View File

@ -1,73 +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();
}
}

View File

@ -0,0 +1,64 @@
// @flow
import React, { Fragment } from 'react';
import styled from 'styled-components';
import { TransactionItemComponent, type Transaction } from './transaction-item';
import { TextComponent } from './text';
import { Divider } from './divider';
const Wrapper = styled.div`
margin-top: 20px;
`;
const TransactionsWrapper = styled.div`
border-radius: ${props => props.theme.boxBorderRadius};
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.regular * 0.9}em`};
font-weight: ${props => props.theme.fontWeight.bold};
margin-bottom: 5px;
`;
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, transactionId,
}, idx) => (
<Fragment key={`${address}-${type}-${amount}-${date}`}>
<TransactionItemComponent
transactionId={transactionId}
type={type}
date={date}
address={address || ''}
amount={amount}
zecPrice={zecPrice}
/>
{idx < transactions.length - 1 && <Divider />}
</Fragment>
),
)}
</TransactionsWrapper>
</Wrapper>
);

View File

@ -0,0 +1,40 @@
---
name: Transaction Daily
---
import { Playground, PropsTable } from 'docz'
import { TransactionDailyComponent } from './transaction-daily.js'
import { DoczWrapper } from '../theme.js'
# Transaction Item
## Properties
<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>

View File

@ -0,0 +1,205 @@
// @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 CloseIcon from '../assets/images/close_icon.svg';
import { TextComponent } from './text';
import { RowComponent } from './row';
import { ColumnComponent } from './column';
import theme from '../theme';
import formatNumber from '../utils/formatNumber';
import truncateAddress from '../utils/truncateAddress';
const Wrapper = styled.div`
width: 460px;
background-color: ${props => props.theme.colors.background};
display: flex;
flex-direction: column;
align-items: center;
border-radius: 6px;
box-shadow: 0px 0px 30px 0px black;
position: relative;
`;
const TitleWrapper = styled.div`
margin-top: 20px;
margin-bottom: 30px;
`;
const Icon = styled.img`
width: 40px;
height: 40px;
margin: 20px 0;
`;
const CloseIconWrapper = styled.div`
display: flex;
width: 100%;
align-items: flex-end;
justify-content: flex-end;
position: absolute;
`;
const CloseIconImg = styled.img`
width: 16px;
height: 16px;
margin-top: 12px;
margin-right: 12px;
cursor: pointer;
`;
const InfoRow = styled(RowComponent)`
justify-content: space-between;
align-items: center;
width: 100%;
height: 80px;
padding: 0 30px;
&first-child {
margin-top: 30px;
}
&:hover {
background: #1d1d1d;
}
`;
const DetailsWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 30px;
`;
const Divider = styled.div`
width: 100%;
background-color: #3a3a3a;
height: 1px;
opacity: 0.5;
`;
const Label = styled(TextComponent)`
font-weight: ${props => props.theme.fontWeight.bold};
color: ${props => props.theme.colors.transactionsDetailsLabel};
margin-bottom: 5px;
letter-spacing: 0.25px;
`;
const Ellipsis = styled(TextComponent)`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
`;
type Props = {
amount: number,
type: 'send' | 'receive',
zecPrice: number,
date: string,
transactionId: string,
from: string,
to: string,
handleClose: () => void,
};
export const TransactionDetailsComponent = ({
amount,
type,
zecPrice,
date,
transactionId,
from,
to,
handleClose,
}: Props) => {
const isReceived = type === 'receive';
return (
<Wrapper>
<CloseIconWrapper>
<CloseIconImg
src={CloseIcon}
onClick={handleClose}
/>
</CloseIconWrapper>
<TitleWrapper>
<TextComponent
value='Transaction Details'
align='center'
/>
</TitleWrapper>
<DetailsWrapper>
<Icon
src={isReceived ? ReceivedIcon : SentIcon}
alt='Transaction Type Icon'
/>
<TextComponent
isBold
size={2.625}
value={formatNumber({
append: `${isReceived ? '+' : '-'}ZEC `,
value: amount,
})}
color={
isReceived
? theme.colors.transactionReceived
: theme.colors.transactionSent
}
/>
<TextComponent
value={formatNumber({
append: `${isReceived ? '+' : '-'}USD `,
value: amount * zecPrice,
})}
size={1.5}
color={theme.colors.transactionsDetailsLabel}
/>
</DetailsWrapper>
<InfoRow>
<ColumnComponent>
<Label value='DATE' />
<TextComponent value={dateFns.format(date, 'MMMM D, YYYY HH:MMA')} />
</ColumnComponent>
<ColumnComponent>
<TextComponent
value='FEES'
isBold
color={theme.colors.transactionsDetailsLabel}
/>
<TextComponent
value={formatNumber({ value: amount * 0.1, append: 'ZEC ' })}
/>
</ColumnComponent>
</InfoRow>
<Divider />
<InfoRow>
<ColumnComponent width='100%'>
<Label value='TRANSACTION ID' />
<Ellipsis value={transactionId} />
</ColumnComponent>
</InfoRow>
<Divider />
<InfoRow>
<ColumnComponent width='100%'>
<Label value='FROM' />
<Ellipsis value={truncateAddress(from)} />
</ColumnComponent>
</InfoRow>
<Divider />
<InfoRow>
<ColumnComponent width='100%'>
<Label value='TO' />
<Ellipsis value={truncateAddress(to)} />
</ColumnComponent>
</InfoRow>
</Wrapper>
);
};

View File

@ -0,0 +1,32 @@
---
name: Transaction Details
---
import { Playground, PropsTable } from 'docz'
import { TransactionDetailsComponent } from './transaction-details.js'
import { DoczWrapper } from '../theme.js'
# Transaction Details
## Properties
<PropsTable of={TransactionDetailsComponent} />
## Basic Usage
<Playground>
<DoczWrapper>
{() => (
<TransactionDetailsComponent
type="receive"
amount={2851}
zecPrice={56}
transactionId="asdh8233uhd89as283dhuashd23-493iskdfdnhdufhs"
from="WKSJniasnxusiuhd898ns13umdoioj93287yhnjknxU"
to="WKSJniasnxusiuhd898ns13umdoioj93287yhnjknxU"
date={new Date().toISOString()}
/>
)}
</DoczWrapper>
</Playground>

View File

@ -0,0 +1,147 @@
// @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 { ModalComponent } from './modal';
import { TransactionDetailsComponent } from './transaction-details';
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;
cursor: pointer;
&:hover {
background-color: #0a0a0a;
}
`;
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 TransactionAddress = styled(TextComponent)`
color: #a7a7a7;
${Wrapper}:hover & {
color: #fff;
}
`;
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,
transactionId: string,
};
export const TransactionItemComponent = ({
type,
date,
address,
amount,
zecPrice,
transactionId,
}: 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 (
<ModalComponent
renderTrigger={toggleVisibility => (
<Wrapper
id={`transaction-item-${transactionId}`}
alignItems='center'
justifyContent='space-between'
onClick={toggleVisibility}
>
<RowComponent alignItems='center'>
<RowComponent alignItems='center'>
<Icon
src={isReceived ? ReceivedIcon : SentIcon}
alt='Transaction Type Icon'
/>
<TransactionColumn>
<TransactionTypeLabel
isReceived={isReceived}
value={type}
isBold
/>
<TransactionTime value={transactionTime} />
</TransactionColumn>
</RowComponent>
<TransactionAddress value={transactionAddress} />
</RowComponent>
<ColumnComponent alignItems='flex-end'>
<TextComponent
isBold
value={transactionValueInZec}
color={
isReceived
? theme.colors.transactionReceived
: theme.colors.transactionSent
}
/>
<TextComponent
value={transactionValueInUsd}
color={theme.colors.inactiveItem}
/>
</ColumnComponent>
</Wrapper>
)}
>
{toggleVisibility => (
<TransactionDetailsComponent
amount={amount}
date={date}
from={address}
to=''
transactionId={transactionId}
handleClose={toggleVisibility}
type={type}
zecPrice={zecPrice}
/>
)}
</ModalComponent>
);
};

View File

@ -0,0 +1,44 @@
---
name: Transaction Item
---
import { Playground, PropsTable } from 'docz'
import { TransactionItemComponent } from './transaction-item.js'
import { DoczWrapper } from '../theme.js'
# Transaction Item
## Properties
<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>

View File

@ -0,0 +1,154 @@
// @flow
import React, { PureComponent } from 'react';
import styled from 'styled-components';
import { Transition, animated } from 'react-spring';
import { ColumnComponent } from './column';
import { Button } from './button';
import { QRCode } from './qrcode';
import truncateAddress from '../utils/truncateAddress';
import eyeIcon from '../assets/images/eye.png';
const AddressWrapper = styled.div`
align-items: center;
display: flex;
background-color: #000;
border-radius: 6px;
padding: 7px 13px;
width: 100%;
`;
const Input = styled.input`
border-radius: ${props => props.theme.boxBorderRadius};
border: none;
background-color: ${props => props.theme.colors.inputBackground};
color: ${props => props.theme.colors.text};
padding: 15px;
width: 100%;
outline: none;
font-family: ${props => props.theme.fontFamily};
::placeholder {
opacity: 0.5;
}
`;
const Btn = styled(Button)`
border-width: 1px;
font-weight: ${props => props.theme.fontWeight.regular};
border-color: ${props => (props.isVisible
? props.theme.colors.primary : props.theme.colors.buttonBorderColor
)};
padding: 8px 10px;
min-width: 260px;
img {
height: 12px;
width: 20px;
}
`;
const QRCodeWrapper = styled.div`
align-items: center;
display: flex;
background-color: #000;
border-radius: 6px;
padding: 20px;
margin-top: 10px;
width: 100%;
`;
const RevealsMain = styled.div`
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: flex-start;
justify-content: flex-start;
height: ${props => (props.isVisible ? '178px' : 0)}
transition: all 0.25s ease-in-out;
& > div {
top: 0;
right: 0;
left: 0;
}
`;
type Props = {
address: string,
isVisible?: boolean,
};
type State = {
isVisible: boolean,
};
export class WalletAddress extends PureComponent<Props, State> {
static defaultProps = {
isVisible: false,
}
constructor(props: Props) {
super(props);
this.state = { isVisible: Boolean(props.isVisible) };
}
show = () => this.setState(() => ({ isVisible: true }));
hide = () => this.setState(() => ({ isVisible: false }));
render() {
const { address } = this.props;
const { isVisible } = this.state;
const toggleVisibility = isVisible ? this.hide : this.show;
const buttonLabel = `${isVisible ? 'Hide' : 'Show'} details and QR Code`;
return (
<ColumnComponent width='100%'>
<AddressWrapper>
<Input
value={isVisible ? address : truncateAddress(address)}
onChange={() => {}}
onFocus={event => event.currentTarget.select()}
/>
<Btn
icon={eyeIcon}
label={buttonLabel}
onClick={toggleVisibility}
variant='secondary'
isVisible={isVisible}
/>
</AddressWrapper>
<RevealsMain isVisible={isVisible}>
<Transition
native
items={isVisible}
enter={[{
height: 'auto',
opacity: 1,
}]}
leave={{ height: 0, opacity: 0 }}
from={{
position: 'absolute',
overflow: 'hidden',
opacity: 0,
height: 0,
}}
>
{show => show && (props => (
<animated.div style={props}>
<QRCodeWrapper>
<QRCode value={address} />
</QRCodeWrapper>
</animated.div>
))}
</Transition>
</RevealsMain>
</ColumnComponent>
);
}
}

View File

@ -0,0 +1,28 @@
---
name: Wallet Address
---
import { Playground, PropsTable } from 'docz'
import { WalletAddress } from './wallet-address.js'
import { DoczWrapper } from '../theme.js'
# Wallet Address
## Properties
<PropsTable of={WalletAddress} />
## Basic usage
<Playground>
<DoczWrapper>
{() => (
<div style={{ width: '700px' }}>
<WalletAddress
address='t14oHp2v54vfmdgQ3v3SNuQga8JKHTNi2a1'
/>
</div>
)}
</DoczWrapper>
</Playground>

View File

@ -0,0 +1,110 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import { TextComponent } from './text';
import { RowComponent } from './row';
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: ${props => props.theme.boxBorderRadius};
padding: 37px 45px;
min-height: 250px;
position: relative;
margin-top: ${props => props.theme.layoutContentPaddingTop};
`;
const AllAddresses = styled(TextComponent)`
margin-bottom: 2.5px;
font-size: ${props => `${props.theme.fontSize.small}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;
font-weight: ${props => props.theme.fontWeight.light};
`;
const ShieldedValue = styled(Label)`
color: ${props => props.theme.colors.activeItem};
`;
type Props = {
total: number,
shielded: number,
transparent: number,
zecPrice: number,
};
export const WalletSummaryComponent = ({
total,
shielded,
transparent,
zecPrice,
}: Props) => (
<Wrapper>
<AllAddresses
value='ALL ADDRESSES'
isBold
/>
<ValueBox>
<TextComponent
size={theme.fontSize.medium * 2.5}
value={`ZEC ${formatNumber({ value: total })}`}
isBold
/>
<USDValue
value={`USD $${formatNumber({ value: total * zecPrice })}`}
size={theme.fontSize.medium * 2}
/>
</ValueBox>
<RowComponent>
<ValueBox>
<ShieldedValue
value='&#9679; SHIELDED'
isBold
size={theme.fontSize.small}
/>
<TextComponent
value={`ZEC ${formatNumber({ value: shielded })}`}
isBold
size={theme.fontSize.medium}
/>
<USDValue
value={`USD $${formatNumber({ value: shielded * zecPrice })}`}
/>
</ValueBox>
<ValueBox>
<Label
value='&#9679; TRANSPARENT'
isBold
size={theme.fontSize.small}
/>
<TextComponent
value={`ZEC ${formatNumber({ value: transparent })}`}
isBold
size={theme.fontSize.medium}
/>
<USDValue
value={`USD $${formatNumber({ value: transparent * zecPrice })}`}
/>
</ValueBox>
</RowComponent>
</Wrapper>
);

View File

@ -0,0 +1,34 @@
---
name: Wallet Summary
---
import { Playground, PropsTable } from 'docz'
import { WalletSummaryComponent } from './wallet-summary.js'
import { DoczWrapper } from '../theme.js'
# Wallet Summary
## Properties
<PropsTable of={WalletSummaryComponent} />
## Basic usage
<Playground>
<DoczWrapper>
{() => (
<div style={{ width: '700px' }}>
<WalletSummaryComponent
total={5000}
shielded={2500}
transparent={2500}
dollarValue={56}
addresses={[
't14oHp2v54vfmdgQ3v3SNuQga8JKHTNi2a1', 't14oHp2v54vfAShh121t5uQga8JKHTNi2a1',
]}
/>
</div>
)}
</DoczWrapper>
</Playground>

View File

@ -0,0 +1,72 @@
// @flow
import React, { type ComponentType, Component } from 'react';
import { LoadingScreen } from './loading-screen';
import rpc from '../../services/api';
type Props = {};
type State = {
isRunning: boolean,
progress: number,
};
/* 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,
progress: 0,
};
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) {
setTimeout(() => {
this.setState(() => ({ isRunning: true }));
}, 500);
this.setState(() => ({ progress: 100 }));
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
})
.catch(() => {
this.setState((state) => {
const newProgress = state.progress > 70 ? state.progress + 2.5 : state.progress + 5;
return { progress: newProgress > 95 ? 95 : newProgress };
});
});
};
render() {
const { isRunning, progress } = this.state;
if (isRunning) {
return <WrappedComponent {...this.props} {...this.state} />;
}
return <LoadingScreen progress={progress} />;
}
};

View File

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

View File

@ -1,7 +0,0 @@
// @flow
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';

8
app/constants/fees.js Normal file
View File

@ -0,0 +1,8 @@
// @flow
export default {
LOW: 0.001,
MEDIUM: 0.005,
HIGH: 0.009,
CUSTOM: 'custom',
};

8
app/constants/routes.js Normal file
View File

@ -0,0 +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';

57
app/constants/sidebar.js Normal file
View File

@ -0,0 +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,
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: '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),
},
];

Some files were not shown because too many files have changed in this diff Show More