Merge pull request #22 from andrerfneves/feature/send-view
Feature/send view
This commit is contained in:
commit
e2b4bc5248
3
.babelrc
3
.babelrc
|
@ -3,6 +3,7 @@
|
|||
"plugins": [
|
||||
"@babel/plugin-transform-regenerator",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-proposal-optional-chaining"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ flow-typed
|
|||
[lints]
|
||||
|
||||
[options]
|
||||
esproposal.optional_chaining=enable
|
||||
|
||||
[strict]
|
|
@ -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 |
|
@ -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 |
|
@ -8,7 +8,7 @@ import { Link } from 'react-router-dom';
|
|||
// $FlowFixMe
|
||||
import { darken } from 'polished';
|
||||
|
||||
const defaultStyles = `
|
||||
const DefaultButton = styled.button`
|
||||
padding: 10px 30px;
|
||||
font-family: ${
|
||||
// $FlowFixMe
|
||||
|
@ -27,10 +27,10 @@ const defaultStyles = `
|
|||
min-width: 100px;
|
||||
border-radius: 100px;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Primary = styled.button`
|
||||
${defaultStyles};
|
||||
const Primary = styled(DefaultButton)`
|
||||
background-color: ${props => props.theme.colors.primary};
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
border: none;
|
||||
|
@ -46,9 +46,8 @@ const Primary = styled.button`
|
|||
}
|
||||
`;
|
||||
|
||||
const Secondary = styled.button`
|
||||
${defaultStyles};
|
||||
background-color: Transparent;
|
||||
const Secondary = styled(DefaultButton)`
|
||||
background-color: transparent;
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
border: 2px solid #3e3c42;
|
||||
|
||||
|
@ -73,21 +72,28 @@ type Props = {
|
|||
to?: string,
|
||||
variant?: 'primary' | 'secondary',
|
||||
disabled?: boolean,
|
||||
className?: string,
|
||||
isLoading?: boolean,
|
||||
};
|
||||
|
||||
export const Button = ({
|
||||
onClick, label, to, variant, disabled,
|
||||
onClick,
|
||||
label,
|
||||
to,
|
||||
variant,
|
||||
disabled,
|
||||
className,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
if (to && onClick) throw new Error('Should define either "to" or "onClick"');
|
||||
|
||||
const props = { onClick, disabled: disabled || isLoading, className };
|
||||
const buttonLabel = isLoading ? 'Loading...' : label;
|
||||
|
||||
const component = variant === 'primary' ? (
|
||||
<Primary onClick={onClick} disabled={disabled}>
|
||||
{label}
|
||||
</Primary>
|
||||
<Primary {...props}>{buttonLabel}</Primary>
|
||||
) : (
|
||||
<Secondary onClick={onClick} disabled={disabled}>
|
||||
{label}
|
||||
</Secondary>
|
||||
<Secondary {...props}>{buttonLabel}</Secondary>
|
||||
);
|
||||
|
||||
return to ? <Link to={String(to)}>{component}</Link> : component;
|
||||
|
@ -98,4 +104,6 @@ Button.defaultProps = {
|
|||
variant: 'primary',
|
||||
onClick: null,
|
||||
disabled: false,
|
||||
className: '',
|
||||
isLoading: false,
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ const Flex = styled.div`
|
|||
flex-direction: column;
|
||||
align-items: ${props => props.alignItems};
|
||||
justify-content: ${props => props.justifyContent};
|
||||
${props => props.width && `width: ${props.width};`}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
|
@ -16,6 +17,7 @@ type Props = {
|
|||
justifyContent?: string,
|
||||
className?: string,
|
||||
children: Node,
|
||||
width?: string,
|
||||
};
|
||||
|
||||
export const ColumnComponent = ({ children, ...props }: Props) => (
|
||||
|
@ -26,4 +28,5 @@ ColumnComponent.defaultProps = {
|
|||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
className: '',
|
||||
width: '',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// @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};
|
||||
`;
|
|
@ -15,8 +15,8 @@ const MenuWrapper = styled.div`
|
|||
0.05,
|
||||
props.theme.colors.activeItem,
|
||||
)}, ${props.theme.colors.activeItem})`};
|
||||
padding: 10px 0;
|
||||
border-radius: 10px;
|
||||
padding: 10px 20px;
|
||||
border-radius: ${props => props.theme.boxBorderRadius};
|
||||
margin-left: -10px;
|
||||
`;
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import styled from 'styled-components';
|
|||
|
||||
import { ZcashLogo } from './zcash-logo';
|
||||
import { TextComponent } from './text';
|
||||
import { Divider } from './divider';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: ${props => props.theme.headerHeight};
|
||||
|
@ -44,13 +45,6 @@ const Title = styled(TextComponent)`
|
|||
text-transform: capitalize;
|
||||
`;
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 100%;
|
||||
background-color: ${props => props.theme.colors.text};
|
||||
height: 1px;
|
||||
opacity: 0.1;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
};
|
||||
|
@ -62,7 +56,7 @@ export const HeaderComponent = ({ title }: Props) => (
|
|||
</LogoWrapper>
|
||||
<TitleWrapper>
|
||||
<Title value={title} />
|
||||
<Divider />
|
||||
<Divider opacity={0.1} />
|
||||
</TitleWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { TextComponent } from './text';
|
||||
|
||||
export const InputLabelComponent = styled(TextComponent)`
|
||||
margin: 20px 0 5px 0;
|
||||
font-weight: ${props => props.theme.fontWeight.bold};
|
||||
`;
|
|
@ -3,30 +3,30 @@ import React from 'react';
|
|||
|
||||
import styled from 'styled-components';
|
||||
|
||||
// TODO: Missing styles
|
||||
|
||||
const defaultStyles = `
|
||||
padding: 10px;
|
||||
const getDefaultStyles = t => styled[t]`
|
||||
border-radius: ${// $FlowFixMe
|
||||
props => props.theme.boxBorderRadius};
|
||||
border: none;
|
||||
background-color: ${// $FlowFixMe
|
||||
props => props.theme.colors.inputBackground};
|
||||
color: ${// $FlowFixMe
|
||||
props => props.theme.colors.text};
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
font-family: ${
|
||||
// $FlowFixMe
|
||||
props => props.theme.fontFamily
|
||||
}
|
||||
font-family: ${// $FlowFixMe
|
||||
props => props.theme.fontFamily};
|
||||
|
||||
::placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
const Input = styled.input.attrs({
|
||||
type: 'text',
|
||||
})`
|
||||
${defaultStyles};
|
||||
`;
|
||||
|
||||
const Textarea = styled.textarea`
|
||||
${defaultStyles};
|
||||
`;
|
||||
const Input = getDefaultStyles('input');
|
||||
const Textarea = getDefaultStyles('textarea');
|
||||
|
||||
type Props = {
|
||||
inputType?: 'input' | 'textarea' | 'dropdown',
|
||||
inputType?: 'input' | 'textarea',
|
||||
value: string,
|
||||
onChange: string => void,
|
||||
rows?: number,
|
||||
|
@ -42,7 +42,6 @@ export const InputComponent = ({ inputType, onChange, ...props }: Props) => {
|
|||
textarea: () => (
|
||||
<Textarea onChange={evt => onChange(evt.target.value)} {...props} />
|
||||
),
|
||||
dropdown: () => null,
|
||||
};
|
||||
|
||||
if (!Object.keys(inputTypes).find(key => key === inputType)) {
|
||||
|
|
|
@ -10,6 +10,7 @@ const Layout = styled.div`
|
|||
background-color: ${props => props.theme.colors.background};
|
||||
padding-left: ${props => props.theme.layoutPaddingLeft};
|
||||
padding-right: ${props => props.theme.layoutPaddingRight};
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
// @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';
|
||||
|
||||
const SelectWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-radius: ${props => props.theme.boxBorderRadius};
|
||||
border: none;
|
||||
background-color: ${// $FlowFixMe
|
||||
props => props.theme.colors.inputBackground};
|
||||
color: ${// $FlowFixMe
|
||||
props => props.theme.colors.text};
|
||||
width: 100%;
|
||||
outline: none;
|
||||
font-family: ${// $FlowFixMe
|
||||
props => props.theme.fontFamily};
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
${props => props.isOpen
|
||||
&& `border-${props.placement}-left-radius: 0; border-${
|
||||
props.placement
|
||||
}-right-radius: 0;`}
|
||||
`;
|
||||
|
||||
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`
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid ${props => props.theme.colors.text};
|
||||
border-radius: 100%;
|
||||
box-shadow: ${props => `0px 0px 10px 0px ${
|
||||
props.theme.colors.selectButtonShadow
|
||||
}, 0px 0px 10px 0px ${props.theme.colors.selectButtonShadow} inset`};
|
||||
`;
|
||||
/* 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 * 60}px`}`};
|
||||
`;
|
||||
|
||||
const Option = styled.button`
|
||||
border: none;
|
||||
background: none;
|
||||
height: 60px;
|
||||
background-color: ${props => props.theme.colors.inputBackground};
|
||||
cursor: pointer;
|
||||
z-index: 99;
|
||||
text-transform: capitalize;
|
||||
|
||||
&:hover {
|
||||
background-color: ${props => props.theme.colors.background};
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
value: string,
|
||||
options: { value: string, label: string }[],
|
||||
placeholder?: string,
|
||||
onChange: string => void,
|
||||
placement?: 'top' | 'bottom',
|
||||
};
|
||||
type State = {
|
||||
isOpen: boolean,
|
||||
};
|
||||
|
||||
export class SelectComponent extends PureComponent<Props, State> {
|
||||
state = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
placeholder: '',
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
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 === 'bottom') {
|
||||
return isOpen ? ChevronUp : ChevronDown;
|
||||
}
|
||||
|
||||
return isOpen ? ChevronDown : ChevronUp;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
value, options, placeholder, placement,
|
||||
} = this.props;
|
||||
const { isOpen } = this.state;
|
||||
|
||||
return (
|
||||
<SelectWrapper
|
||||
isOpen={isOpen}
|
||||
placement={placement}
|
||||
onClick={() => this.setState(() => ({ isOpen: !isOpen }))}
|
||||
>
|
||||
<ValueWrapper hasValue={Boolean(value)}>
|
||||
{this.getSelectedLabel(value) || placeholder}
|
||||
</ValueWrapper>
|
||||
<SelectMenuButtonWrapper>
|
||||
<SelectMenuButton>
|
||||
<Icon src={this.getSelectIcon()} />
|
||||
</SelectMenuButton>
|
||||
</SelectMenuButtonWrapper>
|
||||
{isOpen && (
|
||||
<OptionsWrapper
|
||||
id='select-options-wrapper'
|
||||
optionsAmount={options.length}
|
||||
placement={placement}
|
||||
>
|
||||
{options.map(({ label, value: optionValue }) => (
|
||||
<Option
|
||||
key={label + optionValue}
|
||||
onClick={() => this.onSelect(optionValue)}
|
||||
>
|
||||
<TextComponent value={label} />
|
||||
</Option>
|
||||
))}
|
||||
</OptionsWrapper>
|
||||
)}
|
||||
</SelectWrapper>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,17 @@
|
|||
// @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: 7.5px;
|
||||
border-radius: ${props => props.theme.boxBorderRadius};
|
||||
overflow: hidden;
|
||||
background-color: ${props => props.theme.colors.cardBackgroundColor};
|
||||
padding: 0;
|
||||
|
@ -26,12 +28,6 @@ const Day = styled(TextComponent)`
|
|||
margin-bottom: 5px;
|
||||
`;
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${props => props.theme.colors.inactiveItem};
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
transactionsDate: string,
|
||||
transactions: Transaction[],
|
||||
|
|
|
@ -15,10 +15,10 @@ const Wrapper = styled.div`
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: ${props => props.theme.colors.cardBackgroundColor};
|
||||
border-radius: 5px;
|
||||
border-radius: ${props => props.theme.boxBorderRadius};
|
||||
padding: 37px 45px;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
margin-top: ${props => props.theme.layoutContentPaddingTop};
|
||||
`;
|
||||
|
||||
const AllAddresses = styled(TextComponent)`
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// @flow
|
||||
|
||||
export default {
|
||||
LOW: 1,
|
||||
MEDIUM: 5,
|
||||
HIGH: 9,
|
||||
CUSTOM: 'custom',
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
// @flow
|
||||
import { connect } from 'react-redux';
|
||||
import eres from 'eres';
|
||||
|
||||
import rpc from '../../services/api';
|
||||
import { SendView } from '../views/send';
|
||||
|
||||
import {
|
||||
sendTransaction,
|
||||
sendTransactionSuccess,
|
||||
sendTransactionError,
|
||||
} from '../redux/modules/send';
|
||||
|
||||
import type { AppState } from '../types/app-state';
|
||||
import type { Dispatch } from '../types/redux';
|
||||
|
||||
export type SendTransactionInput = {
|
||||
from: string,
|
||||
to: string,
|
||||
amount: number,
|
||||
fee: number,
|
||||
memo: string,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ walletSummary, sendStatus }: AppState) => ({
|
||||
balance: walletSummary.total,
|
||||
zecPrice: walletSummary.zecPrice,
|
||||
addresses: walletSummary.addresses,
|
||||
error: sendStatus.error,
|
||||
isSending: sendStatus.isSending,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
sendTransaction: async ({
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
fee,
|
||||
memo,
|
||||
}: SendTransactionInput) => {
|
||||
dispatch(sendTransaction());
|
||||
|
||||
const [sendErr] = await eres(
|
||||
rpc.z_sendmany(from, [{ address: to, amount, memo }], 1, fee),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
if (sendErr) return dispatch(sendTransactionError({ error: sendErr.message }));
|
||||
|
||||
dispatch(sendTransactionSuccess());
|
||||
},
|
||||
});
|
||||
|
||||
export const SendContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(SendView);
|
|
@ -6,9 +6,11 @@ import type { RouterHistory } from 'react-router-dom';
|
|||
|
||||
import wallet from './wallet';
|
||||
import transactions from './transactions';
|
||||
import send from './send';
|
||||
|
||||
export const createRootReducer = (history: RouterHistory) => combineReducers({
|
||||
walletSummary: wallet,
|
||||
transactions,
|
||||
sendStatus: send,
|
||||
router: connectRouter(history),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
// @flow
|
||||
import type { Action } from '../../types/redux';
|
||||
|
||||
export const SEND_TRANSACTION = 'SEND_TRANSACTION';
|
||||
export const SEND_TRANSACTION_SUCCESS = 'SEND_TRANSACTION_SUCCESS';
|
||||
export const SEND_TRANSACTION_ERROR = 'SEND_TRANSACTION_ERROR';
|
||||
|
||||
export const sendTransaction = () => ({
|
||||
type: SEND_TRANSACTION,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
export const sendTransactionSuccess = () => ({
|
||||
type: SEND_TRANSACTION_SUCCESS,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
export const sendTransactionError = ({ error }: { error: string }) => ({
|
||||
type: SEND_TRANSACTION_ERROR,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
export type State = {
|
||||
isSending: boolean,
|
||||
error: string | null,
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
isSending: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export default (state: State = initialState, action: Action) => {
|
||||
switch (action.type) {
|
||||
case SEND_TRANSACTION:
|
||||
return { isSending: true, error: null };
|
||||
case SEND_TRANSACTION_SUCCESS:
|
||||
return { isSending: false, error: null };
|
||||
case SEND_TRANSACTION_ERROR:
|
||||
return { isSending: false, error: action.payload.error };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -8,7 +8,7 @@ import { ScrollTopComponent } from './scroll-top';
|
|||
import { SidebarContainer } from '../containers/sidebar';
|
||||
import { DashboardContainer } from '../containers/dashboard';
|
||||
import { TransactionsContainer } from '../containers/transactions';
|
||||
import { SendView } from '../views/send';
|
||||
import { SendContainer } from '../containers/send';
|
||||
import { ReceiveView } from '../views/receive';
|
||||
import { SettingsView } from '../views/settings';
|
||||
import { NotFoundView } from '../views/not-found';
|
||||
|
@ -48,16 +48,16 @@ export const RouterComponent = ({ location }: { location: Location }) => (
|
|||
<HeaderComponent title={getTitle(location.pathname)} />
|
||||
<ContentWrapper>
|
||||
<SidebarContainer location={location} />
|
||||
<ScrollTopComponent>
|
||||
{/* $FlowFixMe */}
|
||||
<LayoutComponent>
|
||||
<ScrollTopComponent>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={DASHBOARD_ROUTE}
|
||||
component={DashboardContainer}
|
||||
/>
|
||||
<Route path={SEND_ROUTE} component={SendView} />
|
||||
<Route path={SEND_ROUTE} component={SendContainer} />
|
||||
<Route path={RECEIVE_ROUTE} component={ReceiveView} />
|
||||
<Route path={SETTINGS_ROUTE} component={SettingsView} />
|
||||
<Route path={CONSOLE_ROUTE} component={ConsoleView} />
|
||||
|
@ -67,8 +67,8 @@ export const RouterComponent = ({ location }: { location: Location }) => (
|
|||
/>
|
||||
<Route component={NotFoundView} />
|
||||
</Switch>
|
||||
</LayoutComponent>
|
||||
</ScrollTopComponent>
|
||||
</LayoutComponent>
|
||||
</ContentWrapper>
|
||||
</FullWrapper>
|
||||
);
|
||||
|
|
|
@ -23,6 +23,7 @@ const transactionSent = '#FF6C6C';
|
|||
const transactionReceived = '#6AEAC0';
|
||||
const transactionsDate = '#777777';
|
||||
const transactionsItemHovered = '#222222';
|
||||
const selectButtonShadow = 'rgba(238,201,76,0.65)';
|
||||
const transactionsDetailsLabel = transactionsDate;
|
||||
|
||||
const appTheme = {
|
||||
|
@ -64,12 +65,16 @@ const appTheme = {
|
|||
transactionReceived,
|
||||
transactionsDate,
|
||||
transactionsItemHovered,
|
||||
inputBackground: brandOne,
|
||||
selectButtonShadow,
|
||||
transactionsDetailsLabel,
|
||||
},
|
||||
sidebarWidth: '200px',
|
||||
headerHeight: '60px',
|
||||
layoutPaddingLeft: '50px',
|
||||
layoutPaddingRight: '45px',
|
||||
layoutContentPaddingTop: '20px',
|
||||
boxBorderRadius: '6px',
|
||||
};
|
||||
|
||||
export const GlobalStyle = createGlobalStyle`
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
import type { State as WalletSummaryState } from '../redux/modules/wallet';
|
||||
import type { State as TransactionsState } from '../redux/modules/transactions';
|
||||
import type { State as SendState } from '../redux/modules/send';
|
||||
|
||||
export type AppState = {
|
||||
walletSummary: WalletSummaryState,
|
||||
transactions: TransactionsState,
|
||||
sendStatus: SendState,
|
||||
};
|
||||
|
|
|
@ -61,6 +61,7 @@ export class DashboardView extends PureComponent<Props> {
|
|||
transactionsDate={day}
|
||||
transactions={transactions[day]}
|
||||
zecPrice={zecPrice}
|
||||
key={day}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
|
|
|
@ -1,5 +1,263 @@
|
|||
// @flow
|
||||
import React, { PureComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import React from 'react';
|
||||
import FEES from '../constants/fees';
|
||||
|
||||
export const SendView = () => <div className='send'>send</div>;
|
||||
import { InputLabelComponent } from '../components/input-label';
|
||||
import { InputComponent } from '../components/input';
|
||||
import { TextComponent } from '../components/text';
|
||||
import { SelectComponent } from '../components/select';
|
||||
import { RowComponent } from '../components/row';
|
||||
import { ColumnComponent } from '../components/column';
|
||||
import { Divider } from '../components/divider';
|
||||
import { Button } from '../components/button';
|
||||
|
||||
import formatNumber from '../utils/formatNumber';
|
||||
|
||||
import type { SendTransactionInput } from '../containers/send';
|
||||
|
||||
const FormWrapper = styled.div`
|
||||
margin-top: ${props => props.theme.layoutContentPaddingTop};
|
||||
width: 80%;
|
||||
`;
|
||||
|
||||
const SendWrapper = styled(ColumnComponent)`
|
||||
margin-top: 60px;
|
||||
width: 15%;
|
||||
`;
|
||||
|
||||
const ShowFeeButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
color: ${props => props.theme.colors.text};
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const InfoCard = styled.div`
|
||||
width: 100%;
|
||||
background-color: ${props => props.theme.colors.cardBackgroundColor};
|
||||
border-radius: ${props => props.theme.boxBorderRadius};
|
||||
`;
|
||||
|
||||
const InfoContent = styled.div`
|
||||
padding: 15px;
|
||||
`;
|
||||
|
||||
const InfoCardLabel = styled(TextComponent)`
|
||||
opacity: 0.5;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
const InfoCardUSD = styled(TextComponent)`
|
||||
opacity: 0.5;
|
||||
margin-top: 2.5px;
|
||||
`;
|
||||
|
||||
const FormButton = styled(Button)`
|
||||
margin: 10px 0;
|
||||
border-color: ${props => (props.focused
|
||||
? props.theme.colors.activeItem
|
||||
: props.theme.colors.inactiveItem)};
|
||||
|
||||
&:hover {
|
||||
border-color: ${props => (props.focused
|
||||
? props.theme.colors.activeItem
|
||||
: props.theme.colors.inactiveItem)};
|
||||
background-color: ${props => (props.focused
|
||||
? props.theme.colors.activeItem
|
||||
: props.theme.colors.inactiveItem)};
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
zecPrice: number,
|
||||
addresses: string[],
|
||||
sendTransaction: SendTransactionInput => void,
|
||||
// error: string | null,
|
||||
isSending: boolean,
|
||||
};
|
||||
type State = {
|
||||
showFee: boolean,
|
||||
from: string,
|
||||
amount: number,
|
||||
to: string,
|
||||
feeType: string | number,
|
||||
fee: number | null,
|
||||
memo: string,
|
||||
};
|
||||
|
||||
export class SendView extends PureComponent<Props, State> {
|
||||
state = {
|
||||
showFee: false,
|
||||
from: '',
|
||||
amount: 0,
|
||||
to: '',
|
||||
feeType: FEES.LOW,
|
||||
fee: FEES.LOW,
|
||||
memo: '',
|
||||
};
|
||||
|
||||
handleChange = (field: string) => (value: string) => {
|
||||
this.setState(() => ({
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
handleChangeFeeType = (value: string) => {
|
||||
this.setState(
|
||||
{
|
||||
feeType: value,
|
||||
fee: null,
|
||||
},
|
||||
() => {
|
||||
if (
|
||||
value === String(FEES.LOW)
|
||||
|| value === String(FEES.MEDIUM)
|
||||
|| value === String(FEES.HIGH)
|
||||
) {
|
||||
this.setState(() => ({
|
||||
fee: Number(value),
|
||||
}));
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
const {
|
||||
from, amount, to, memo, fee,
|
||||
} = this.state;
|
||||
const { sendTransaction } = this.props;
|
||||
|
||||
if (!from || !amount || !to || !memo || !fee) return;
|
||||
|
||||
sendTransaction({
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
fee,
|
||||
memo,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
addresses, balance, zecPrice, isSending,
|
||||
} = this.props;
|
||||
const {
|
||||
showFee, from, amount, to, memo, fee, feeType,
|
||||
} = this.state;
|
||||
|
||||
const zecBalance = formatNumber({ value: balance, append: 'ZEC ' });
|
||||
const zecBalanceInUsd = formatNumber({
|
||||
value: balance * zecPrice,
|
||||
append: 'USD $',
|
||||
});
|
||||
const valueSent = formatNumber({ value: amount, append: 'ZEC ' });
|
||||
const valueSentInUsd = formatNumber({
|
||||
value: amount * zecPrice,
|
||||
append: 'USD $',
|
||||
});
|
||||
|
||||
return (
|
||||
<RowComponent justifyContent='space-between'>
|
||||
<FormWrapper>
|
||||
<InputLabelComponent value='From' />
|
||||
<SelectComponent
|
||||
onChange={this.handleChange('from')}
|
||||
value={from}
|
||||
placeholder='Select a address'
|
||||
options={addresses.map(addr => ({ value: addr, label: addr }))}
|
||||
/>
|
||||
<InputLabelComponent value='Amount' />
|
||||
<InputComponent
|
||||
type='number'
|
||||
onChange={this.handleChange('amount')}
|
||||
value={String(amount)}
|
||||
placeholder='kjnasG86431nvtsa…ks345jbhbdsDGvds'
|
||||
/>
|
||||
<InputLabelComponent value='To' />
|
||||
<InputComponent
|
||||
onChange={this.handleChange('to')}
|
||||
value={to}
|
||||
placeholder='kjnasG86431nvtsa…ks345jbhbdsDGvds'
|
||||
/>
|
||||
<InputLabelComponent value='Memo' />
|
||||
<InputComponent
|
||||
onChange={this.handleChange('memo')}
|
||||
value={memo}
|
||||
inputType='textarea'
|
||||
placeholder='Enter a text here'
|
||||
/>
|
||||
<ShowFeeButton
|
||||
onClick={() => this.setState(state => ({ showFee: !state.showFee }))
|
||||
}
|
||||
>
|
||||
<TextComponent
|
||||
value={`${showFee ? 'Hide' : 'Show'} Additional Options`}
|
||||
align='right'
|
||||
/>
|
||||
</ShowFeeButton>
|
||||
{showFee && (
|
||||
<RowComponent alignItems='flex-end' justifyContent='space-between'>
|
||||
<ColumnComponent width='74%'>
|
||||
<InputLabelComponent value='Fee' />
|
||||
<InputComponent
|
||||
type='number'
|
||||
onChange={this.handleChange('fee')}
|
||||
value={String(fee)}
|
||||
disabled={feeType !== FEES.CUSTOM}
|
||||
/>
|
||||
</ColumnComponent>
|
||||
<ColumnComponent width='25%'>
|
||||
<SelectComponent
|
||||
onChange={this.handleChangeFeeType}
|
||||
value={String(feeType)}
|
||||
options={Object.keys(FEES).map(cur => ({
|
||||
label: cur.toLowerCase(),
|
||||
value: String(FEES[cur]),
|
||||
}))}
|
||||
placement='top'
|
||||
/>
|
||||
</ColumnComponent>
|
||||
</RowComponent>
|
||||
)}
|
||||
{feeType === FEES.CUSTOM && (
|
||||
<TextComponent value='Custom fees may compromise your privacy since fees are transparent' />
|
||||
)}
|
||||
</FormWrapper>
|
||||
<SendWrapper>
|
||||
<InfoCard>
|
||||
<InfoContent>
|
||||
<InfoCardLabel value='Available Funds:' />
|
||||
<TextComponent value={zecBalance} size={1.125} isBold />
|
||||
<InfoCardUSD value={zecBalanceInUsd} />
|
||||
</InfoContent>
|
||||
<Divider opacity={0.5} />
|
||||
<InfoContent>
|
||||
<InfoCardLabel value='Sending' />
|
||||
<TextComponent value={valueSent} size={1.125} isBold />
|
||||
<InfoCardUSD value={valueSentInUsd} />
|
||||
</InfoContent>
|
||||
</InfoCard>
|
||||
<FormButton
|
||||
label='Send'
|
||||
variant='secondary'
|
||||
focused
|
||||
onClick={this.handleSubmit}
|
||||
isLoading={isSending}
|
||||
/>
|
||||
<FormButton label='Cancel' variant='secondary' />
|
||||
</SendWrapper>
|
||||
</RowComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"@babel/core": "^7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
|
||||
"@babel/plugin-transform-regenerator": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
|
|
|
@ -32,7 +32,10 @@ const api: APIMethods = METHODS.reduce(
|
|||
params: args,
|
||||
},
|
||||
})
|
||||
.then(data => Promise.resolve(data.body && data.body.result)),
|
||||
.then(data => Promise.resolve(data.body && data.body.result))
|
||||
.catch(payload => Promise.reject(
|
||||
new Error(payload.body?.error.message || 'Something went wrong'),
|
||||
)),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -406,6 +406,14 @@
|
|||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
|
||||
|
||||
"@babel/plugin-proposal-optional-chaining@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.2.0.tgz#ae454f4c21c6c2ce8cb2397dc332ae8b420c5441"
|
||||
integrity sha512-ea3Q6edZC/55wEBVZAEz42v528VulyO0eir+7uky/sT4XRcdkWJcFi1aPtitTlwUzGnECWJNExWww1SStt+yWw==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/plugin-syntax-optional-chaining" "^7.2.0"
|
||||
|
||||
"@babel/plugin-proposal-unicode-property-regex@^7.0.0", "@babel/plugin-proposal-unicode-property-regex@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520"
|
||||
|
@ -478,6 +486,13 @@
|
|||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-syntax-optional-chaining@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.2.0.tgz#a59d6ae8c167e7608eaa443fda9fa8fa6bf21dff"
|
||||
integrity sha512-HtGCtvp5Uq/jH/WNUPkK6b7rufnCPLLlDAFN7cmACoIjaOOiXxUt3SswU5loHqrhtqTsa/WoLQ1OQ1AGuZqaWA==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-syntax-typescript@^7.0.0", "@babel/plugin-syntax-typescript@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.2.0.tgz#55d240536bd314dcbbec70fd949c5cabaed1de29"
|
||||
|
|
Loading…
Reference in New Issue