diff --git a/app/assets/images/close_icon.svg b/app/assets/images/close_icon.svg new file mode 100644 index 0000000..7b218da --- /dev/null +++ b/app/assets/images/close_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/components/dropdown.js b/app/components/dropdown.js index 59c9101..a2f17d1 100644 --- a/app/components/dropdown.js +++ b/app/components/dropdown.js @@ -90,7 +90,7 @@ export class DropdownComponent extends Component { )} {options.map(({ label: optionLabel, onClick }) => ( - + ))} diff --git a/app/components/modal.js b/app/components/modal.js new file mode 100644 index 0000000..c110779 --- /dev/null +++ b/app/components/modal.js @@ -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.4); +`; + +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 { + 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 ( + + {renderTrigger(toggleVisibility)} + {isVisible + ? createPortal( + { + if ( + closeOnBackdropClick + && event.target.id === 'modal-portal-wrapper' + ) this.close(); + }} + > + {children(toggleVisibility)} + , + this.element, + ) + : null} + + ); + } +} diff --git a/app/components/modal.mdx b/app/components/modal.mdx new file mode 100644 index 0000000..83b9f74 --- /dev/null +++ b/app/components/modal.mdx @@ -0,0 +1,57 @@ +--- +name: Modal +--- + +import { Playground, PropsTable } from 'docz' + +import { ModalComponent } from './modal.js' + +# Modal + +## Properties + + + +## Basic usage + + + ( + + )} + > + {toggleVisibility => ( +
+ Modal Content + +
+ )} +
+
+ +## Don't close with ESC or backdrop click + + + ( + + )} + > + {toggleVisibility => ( +
+ Modal Content + +
+ )} +
+
diff --git a/app/components/transaction-daily.js b/app/components/transaction-daily.js index 68a3cd7..244c77f 100644 --- a/app/components/transaction-daily.js +++ b/app/components/transaction-daily.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { Fragment } from 'react'; import styled from 'styled-components'; import { TransactionItemComponent, type Transaction } from './transaction-item'; import { TextComponent } from './text'; @@ -46,20 +46,23 @@ export const TransactionDailyComponent = ({ - {transactions.map(({ - date, type, address, amount, - }, idx) => ( -
- - {idx < transactions.length - 1 && } -
- ))} + {transactions.map( + ({ + date, type, address, amount, transactionId, + }, idx) => ( + + + {idx < transactions.length - 1 && } + + ), + )}
); diff --git a/app/components/transaction-details.js b/app/components/transaction-details.js new file mode 100644 index 0000000..67615b2 --- /dev/null +++ b/app/components/transaction-details.js @@ -0,0 +1,176 @@ +// @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: 30vw; + 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; +`; + +const TitleWrapper = styled.div` + margin-top: 20px; +`; + +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; +`; + +const CloseIconImg = styled.img` + width: 12.5px; + height: 12.5px; + margin-top: 10px; + margin-right: 10px; + cursor: pointer; +`; + +const InfoRow = styled(RowComponent)` + justify-content: space-between; + align-items: center; + width: 100%; + height: 80px; + padding: 0 30px; +`; + +const Divider = styled.div` + width: 100%; + background-color: ${props => props.theme.colors.transactionsDetailsLabel}; + 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; +`; + +const Ellipsis = styled(TextComponent)` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: calc(30vw - 60px); +`; + +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 ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/app/components/transaction-details.mdx b/app/components/transaction-details.mdx new file mode 100644 index 0000000..75840b1 --- /dev/null +++ b/app/components/transaction-details.mdx @@ -0,0 +1,32 @@ +--- +name: Transaction Details +--- + +import { Playground, PropsTable } from 'docz' + +import { TransactionDetails } from './transaction-details.js' +import { DoczWrapper } from '../theme.js' + +# Transaction Details + +## Properties + + + +## Basic Usage + + + + {() => ( + + )} + + diff --git a/app/components/transaction-item.js b/app/components/transaction-item.js index 5f73a77..6c0a31e 100644 --- a/app/components/transaction-item.js +++ b/app/components/transaction-item.js @@ -9,6 +9,8 @@ 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'; @@ -53,6 +55,7 @@ export type Transaction = { address: string, amount: number, zecPrice: number, + transactionId: string, }; export const TransactionItemComponent = ({ @@ -61,6 +64,7 @@ export const TransactionItemComponent = ({ address, amount, zecPrice, + transactionId, }: Transaction) => { const isReceived = type === 'receive'; const transactionTime = dateFns.format(new Date(date), 'HH:mm A'); @@ -75,34 +79,55 @@ export const TransactionItemComponent = ({ const transactionAddress = truncateAddress(address); return ( - - - - - - - - - - - - - ( + + + + + + + + + + + + + + + + + )} + > + {toggleVisibility => ( + - - - + )} + ); }; diff --git a/app/containers/dashboard.js b/app/containers/dashboard.js index 6b94c6f..c55f943 100644 --- a/app/containers/dashboard.js +++ b/app/containers/dashboard.js @@ -41,9 +41,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ // eslint-disable-next-line if (addressesErr) return dispatch(loadWalletSummaryError({ error: addressesErr.message })); - const [transactionsErr, transactions = []] = await eres( - rpc.listtransactions(), - ); + const [transactionsErr, transactions] = await eres(rpc.listtransactions()); if (transactionsErr) { return dispatch( @@ -59,6 +57,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ addresses, transactions: flow([ arr => arr.map(transaction => ({ + transactionId: transaction.txid, type: transaction.category, date: new Date(transaction.time * 1000).toISOString(), address: transaction.address, diff --git a/app/containers/transactions.js b/app/containers/transactions.js index 6789a0f..2877779 100644 --- a/app/containers/transactions.js +++ b/app/containers/transactions.js @@ -42,6 +42,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ loadTransactionsSuccess({ list: flow([ arr => arr.map(transaction => ({ + transactionId: transaction.txid, type: transaction.category, date: new Date(transaction.time * 1000).toISOString(), address: transaction.address, diff --git a/app/theme.js b/app/theme.js index f6c816a..2a096ed 100644 --- a/app/theme.js +++ b/app/theme.js @@ -23,6 +23,7 @@ const transactionSent = '#FF6C6C'; const transactionReceived = '#6AEAC0'; const transactionsDate = '#777777'; const transactionsItemHovered = '#222222'; +const transactionsDetailsLabel = transactionsDate; const appTheme = { mode: DARK, @@ -61,6 +62,7 @@ const appTheme = { transactionReceived, transactionsDate, transactionsItemHovered, + transactionsDetailsLabel, }, sidebarWidth: '200px', headerHeight: '60px', diff --git a/app/utils/formatNumber.js b/app/utils/formatNumber.js index 5d96753..943e4a4 100644 --- a/app/utils/formatNumber.js +++ b/app/utils/formatNumber.js @@ -1,3 +1,3 @@ // @flow -export default ({ value, append = '' }: { value: number, append?: string }) => `${append}${(value || 0).toLocaleString('de-DE')}`; +export default ({ value, append = '' }: { value: number, append?: string }) => `${append}${(value || 0).toLocaleString()}`; diff --git a/doczrc.js b/doczrc.js index 6a61d60..fbc6e48 100644 --- a/doczrc.js +++ b/doczrc.js @@ -14,5 +14,8 @@ export default { }, ], }, + body: { + raw: '