diff --git a/src/components/MainSideBar.js b/src/components/MainSideBar.js new file mode 100644 index 00000000..61131246 --- /dev/null +++ b/src/components/MainSideBar.js @@ -0,0 +1,173 @@ +// @flow + +import React, { PureComponent } from 'react' +import styled from 'styled-components' +import { translate } from 'react-i18next' +import { connect } from 'react-redux' +import { compose } from 'redux' +import { withRouter } from 'react-router' +import { push } from 'react-router-redux' +import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' + +import type { Location } from 'react-router' +import type { Account } from '@ledgerhq/live-common/lib/types' + +import type { T } from 'types/common' +import type { UpdateStatus } from 'reducers/update' + +import { MODAL_RECEIVE, MODAL_SEND } from 'config/constants' + +import { accountsSelector } from 'reducers/accounts' +import { openModal } from 'reducers/modals' +import { getUpdateStatus } from 'reducers/update' + +import { SideBarList } from 'components/base/SideBar' +import Box, { Tabbable } from 'components/base/Box' +import Space from 'components/base/Space' +import FormattedVal from 'components/base/FormattedVal' + +import IconManager from 'icons/Manager' +import IconPieChart from 'icons/PieChart' +import IconPlus from 'icons/Plus' +import IconReceive from 'icons/Receive' +import IconSend from 'icons/Send' +import IconExchange from 'icons/Exchange' + +const mapStateToProps = state => ({ + accounts: accountsSelector(state), + updateStatus: getUpdateStatus(state), +}) + +const mapDispatchToProps = { + push, + openModal, +} + +type Props = { + t: T, + accounts: Account[], + location: Location, + push: string => void, + openModal: string => void, + updateStatus: UpdateStatus, +} + +class MainSideBar extends PureComponent { + push(to: string) { + const { push } = this.props + const { + location: { pathname }, + } = this.props + if (pathname === to) { + return + } + push(to) + } + + render() { + const { t, accounts, openModal, location, updateStatus } = this.props + const { pathname } = location + + const navigationItems = [ + { + value: 'dashboard', + label: t('dashboard:title'), + icon: IconPieChart, + iconActiveColor: 'wallet', + onClick: () => this.push('/'), + isActive: pathname === '/', + hasNotif: updateStatus === 'downloaded', + }, + { + value: 'send', + label: t('send:title'), + icon: IconSend, + iconActiveColor: 'wallet', + onClick: () => openModal(MODAL_SEND), + }, + { + value: 'receive', + label: t('receive:title'), + icon: IconReceive, + iconActiveColor: 'wallet', + onClick: () => openModal(MODAL_RECEIVE), + }, + { + value: 'manager', + label: t('sidebar:manager'), + icon: IconManager, + iconActiveColor: 'wallet', + onClick: () => this.push('/manager'), + isActive: pathname === '/manager', + }, + { + value: 'exchange', + label: t('sidebar:exchange'), + icon: IconExchange, + iconActiveColor: 'wallet', + onClick: () => this.push('/exchange'), + isActive: pathname === '/exchange', + }, + ] + + const accountsItems = accounts.map(account => { + const accountURL = `/account/${account.id}` + return { + value: account.id, + label: account.name, + desc: () => ( + + ), + iconActiveColor: account.currency.color, + icon: getCryptoCurrencyIcon(account.currency), + onClick: () => this.push(accountURL), + isActive: pathname === accountURL, + } + }) + + return ( + + + + + openModal('importAccounts')}> + + + } + items={accountsItems} + emptyText={t('emptyState:sidebar.text')} + /> + + ) + } +} + +const PlusWrapper = styled(Tabbable).attrs({ + p: 1, + cursor: 'pointer', + borderRadius: 1, +})` + opacity: 0.4; + &:hover { + opacity: 1; + } + + border: 1px dashed rgba(0, 0, 0, 0); + &:focus { + border: 1px dashed rgba(0, 0, 0, 0.2); + outline: none; + } +` + +const decorate = compose(withRouter, translate(), connect(mapStateToProps, mapDispatchToProps)) +export default decorate(MainSideBar) diff --git a/src/components/SideBar/Item.js b/src/components/SideBar/Item.js deleted file mode 100644 index 932de6da..00000000 --- a/src/components/SideBar/Item.js +++ /dev/null @@ -1,146 +0,0 @@ -// @flow - -import React from 'react' -import styled from 'styled-components' -import { compose } from 'redux' -import { withRouter } from 'react-router' -import { push } from 'react-router-redux' -import { connect } from 'react-redux' - -import { openModal } from 'reducers/modals' - -import type { Node } from 'react' -import type { Location } from 'react-router' - -import Box, { Tabbable } from 'components/base/Box' -import Text from 'components/base/Text' - -const mapStateToProps = (state: any) => ({ - // connect router here only to make components re-render - // see https://github.com/ReactTraining/react-router/issues/4671 - router: state.router, -}) - -const mapDispatchToProps: Object = { - push, - openModal, -} - -const Container = styled(Tabbable).attrs({ - alignItems: 'center', - borderRadius: 1, - ff: 'Open Sans|SemiBold', - flow: 3, - horizontal: true, - pl: 3, -})` - cursor: ${p => (p.isActive ? 'default' : 'pointer')}; - color: ${p => p.theme.colors.dark}; - background: ${p => (p.isActive ? p.theme.colors.lightGrey : '')}; - height: ${p => (p.big ? 50 : 36)}px; - outline: none; - - .desaturate { - opacity: ${p => (p.isActive ? 1 : 0.4)}; - } - - &:hover, - &:focus { - .desaturate { - opacity: 1; - } - - svg { - color: ${p => p.theme.colors[p.iconActiveColor] || p.iconActiveColor}; - } - } -` - -const Bullet = styled.div` - background: ${p => p.theme.colors.wallet}; - width: 8px; - height: 8px; - border-radius: 100%; -` - -type Props = { - iconActiveColor?: string, - children: string, - linkTo?: string | null, - modal?: string | null, - desc?: string | null, - icon?: Node | null, - big?: boolean, - highlight?: boolean, - location: Location, - push: Function, - openModal: Function, -} - -function Item({ - big, - iconActiveColor, - children, - desc, - icon, - linkTo, - push, - location, - modal, - openModal, - highlight, -}: Props) { - const { pathname } = location - const isActive = linkTo === pathname - return ( - push(linkTo) - : modal - ? () => openModal(modal) - : void 0 - } - > - {icon && ( - - {icon} - - )} - - {children} - {desc && ( - - {desc} - - )} - - {highlight && ( - - - - )} - - ) -} - -Item.defaultProps = { - iconActiveColor: 'wallet', - big: false, - desc: null, - icon: null, - linkTo: null, - modal: null, -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps, null, { - pure: false, - }), -)(Item) diff --git a/src/components/SideBar/index.js b/src/components/SideBar/index.js deleted file mode 100644 index 227df258..00000000 --- a/src/components/SideBar/index.js +++ /dev/null @@ -1,178 +0,0 @@ -// @flow - -import React, { PureComponent, Fragment } from 'react' -import { compose } from 'redux' -import { translate } from 'react-i18next' -import styled from 'styled-components' -import { withRouter } from 'react-router' -import { connect } from 'react-redux' -import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' -import type { Account } from '@ledgerhq/live-common/lib/types' - -import { MODAL_SEND, MODAL_RECEIVE } from 'config/constants' - -import type { T } from 'types/common' - -import { openModal } from 'reducers/modals' -import { accountsSelector } from 'reducers/accounts' -import { getUpdateStatus } from 'reducers/update' -import type { UpdateStatus } from 'reducers/update' - -import IconManager from 'icons/Manager' -import IconPieChart from 'icons/PieChart' -import IconPlus from 'icons/Plus' -import IconReceive from 'icons/Receive' -import IconSend from 'icons/Send' -import IconExchange from 'icons/Exchange' - -import Box, { Tabbable } from 'components/base/Box' -import FormattedVal from 'components/base/FormattedVal' -import GrowScroll from 'components/base/GrowScroll' -import Tooltip from 'components/base/Tooltip' - -import Item from './Item' - -const CapsSubtitle = styled(Box).attrs({ - color: 'dark', - ff: 'Museo Sans|ExtraBold', - fontSize: 1, - px: 4, -})` - cursor: default; - letter-spacing: 2px; - text-transform: uppercase; -` - -const Container = styled(Box)` - width: ${p => p.theme.sizes.sideBarWidth}px; -` - -const PlusBtn = styled(Tabbable).attrs({ - p: 1, - m: -1, -})` - cursor: pointer; - outline: none; -` - -type Props = { - t: T, - openModal: Function, - updateStatus: UpdateStatus, - accounts: Account[], -} - -const mapStateToProps = state => ({ - accounts: accountsSelector(state), - updateStatus: getUpdateStatus(state), -}) - -const mapDispatchToProps: Object = { - openModal, -} - -class SideBar extends PureComponent { - render() { - const { t, openModal, updateStatus, accounts } = this.props - - return ( - - - - {t('sidebar:menu')} - - } - linkTo="/" - highlight={updateStatus === 'downloaded'} - > - {t('dashboard:title')} - - } modal={MODAL_SEND}> - {t('send:title')} - - } modal={MODAL_RECEIVE}> - {t('receive:title')} - - } linkTo="/manager"> - {t('sidebar:manager')} - - } linkTo="/exchange"> - {t('sidebar:exchange')} - - - - - - {t('sidebar:accounts')} - t('addAccount:title')}> - openModal('importAccounts')}> - - - - - - {accounts.length > 0 ? ( - - ) : ( - {t('emptyState:sidebar.text')} - )} - - - - - ) - } -} - -const AccountsList = compose( - withRouter, - connect( - state => ({ - accounts: accountsSelector(state), - }), - null, - null, - { pure: false }, - ), -)(({ accounts }: { accounts: Account[] }) => ( - - {accounts.map(account => { - const Icon = getCryptoCurrencyIcon(account.currency) - return ( - - } - iconActiveColor={account.currency.color} - icon={Icon ? : null} - key={account.id} - linkTo={`/account/${account.id}`} - > - {account.name} - - ) - })} - -)) - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps, null, { pure: false }), - translate(), -)(SideBar) - -export const NoAccountsText = styled(Box).attrs({ - ff: 'Open Sans|Regular', - fontSize: 3, - color: p => p.theme.colors.grey, - shrink: true, - mt: 3, -})`` diff --git a/src/components/base/SideBar/SideBarList.js b/src/components/base/SideBar/SideBarList.js new file mode 100644 index 00000000..8dd1f00e --- /dev/null +++ b/src/components/base/SideBar/SideBarList.js @@ -0,0 +1,71 @@ +// @flow + +import React, { PureComponent, Fragment } from 'react' +import styled from 'styled-components' + +import GrowScroll from 'components/base/GrowScroll' +import Box from 'components/base/Box' +import Space from 'components/base/Space' + +import SideBarListItem from './SideBarListItem' + +import type { Item } from './SideBarListItem' + +type Props = { + items: Item[], + title?: Node | string, + activeValue?: string, + scroll?: boolean, + titleRight?: any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯ + emptyText?: string, +} + +class SideBarList extends PureComponent { + render() { + const { items, title, activeValue, scroll, titleRight, emptyText, ...props } = this.props + const ListWrapper = scroll ? GrowScroll : Box + return ( + + {!!title && ( + + + {title} + {!!titleRight && {titleRight}} + + + + )} + {items.length > 0 ? ( + + {items.map(item => { + const itemProps = { + item, + isActive: item.isActive || (!!activeValue && activeValue === item.value), + } + return + })} + + ) : emptyText ? ( + + {emptyText} + + ) : null} + + ) + } +} + +const SideBarListTitle = styled(Box).attrs({ + horizontal: true, + align: 'center', + color: 'dark', + ff: 'Museo Sans|ExtraBold', + fontSize: 1, + px: 4, +})` + cursor: default; + letter-spacing: 2px; + text-transform: uppercase; +` + +export default SideBarList diff --git a/src/components/base/SideBar/SideBarListItem.js b/src/components/base/SideBar/SideBarListItem.js new file mode 100644 index 00000000..56d09ddf --- /dev/null +++ b/src/components/base/SideBar/SideBarListItem.js @@ -0,0 +1,94 @@ +// @flow + +import React, { PureComponent } from 'react' +import styled from 'styled-components' + +import Box, { Tabbable } from 'components/base/Box' + +export type Item = { + value: string, + label: string | (Props => React$Element), + desc?: Props => any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯ + icon?: any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯ + iconActiveColor: ?string, + hasNotif?: boolean, + isActive?: boolean, + onClick?: void => void, +} + +export type Props = { + item: Item, + isActive: boolean, +} + +class SideBarListItem extends PureComponent { + render() { + const { + item: { icon: Icon, label, desc, iconActiveColor, hasNotif, onClick }, + isActive, + } = this.props + return ( + + {!!Icon && } + + {typeof label === 'function' ? ( + label(this.props) + ) : ( + + {label} + + )} + {!!desc && desc(this.props)} + + {!!hasNotif && } + + ) + } +} + +const Container = styled(Tabbable).attrs({ + align: 'center', + borderRadius: 1, + ff: 'Open Sans|SemiBold', + flow: 3, + horizontal: true, + px: 3, + py: 2, +})` + cursor: ${p => (p.isActive ? 'default' : 'pointer')}; + color: ${p => p.theme.colors.dark}; + background: ${p => (p.isActive ? p.theme.colors.lightGrey : '')}; + + opacity: ${p => (p.isActive ? 1 : 0.4)}; + + &:active { + background: ${p => p.theme.colors.lightGrey}; + } + + &:hover { + opacity: ${p => (p.isActive ? 1 : 0.7)}; + } + + border: 1px dashed rgba(0, 0, 0, 0); + &:focus { + border: 1px dashed rgba(0, 0, 0, 0.2); + outline: none; + } + + ${p => { + const iconActiveColor = p.theme.colors[p.iconActiveColor] || p.iconActiveColor + return ` + svg { color: ${p.isActive ? iconActiveColor : ''}; } + &:hover svg { color: ${iconActiveColor}; } + ` + }}; +` + +const Bullet = styled.div` + background: ${p => p.theme.colors.wallet}; + width: 8px; + height: 8px; + border-radius: 100%; +` + +export default SideBarListItem diff --git a/src/components/base/SideBar/index.js b/src/components/base/SideBar/index.js new file mode 100644 index 00000000..0461a8d0 --- /dev/null +++ b/src/components/base/SideBar/index.js @@ -0,0 +1,3 @@ +// @flow + +export { default as SideBarList } from './SideBarList' diff --git a/src/components/base/SideBar/stories.js b/src/components/base/SideBar/stories.js new file mode 100644 index 00000000..ce7e0505 --- /dev/null +++ b/src/components/base/SideBar/stories.js @@ -0,0 +1,62 @@ +// @flow + +import React from 'react' +import { storiesOf } from '@storybook/react' +import { select } from '@storybook/addon-knobs' + +import { SideBarList } from 'components/base/SideBar' +import Box from 'components/base/Box' + +import IconAccountSettings from 'icons/AccountSettings' +import IconPrint from 'icons/Print' +import IconControls from 'icons/Controls' +import IconCurrencies from 'icons/Currencies' +import IconExclamationCircle from 'icons/ExclamationCircle' + +const stories = storiesOf('Components/base/SideBar', module) + +const SIDEBAR_ITEMS = [ + { + value: 'first', + label: 'First', + icon: IconAccountSettings, + iconActiveColor: '#ffae35', + }, + { + value: 'second', + label: 'Second', + icon: IconPrint, + iconActiveColor: '#0ebdcd', + }, + { + value: 'third', + label: 'Third very very very very long text very long text very long', + icon: IconControls, + iconActiveColor: '#27a2db', + hasNotif: true, + }, + { + value: 'fourth', + label: () => ( + + {'custom'} + {'render'} + + ), + icon: IconCurrencies, + iconActiveColor: '#3ca569', + }, + { + value: 'fifth', + label: 'Fifth', + icon: IconExclamationCircle, + iconActiveColor: '#0e76aa', + }, +] + +stories.add('SideBarList', () => ( + i.value), null], 'third')} + /> +)) diff --git a/src/components/base/Space.js b/src/components/base/Space.js new file mode 100644 index 00000000..a78a0af4 --- /dev/null +++ b/src/components/base/Space.js @@ -0,0 +1,5 @@ +import styled from 'styled-components' + +export default styled.div` + height: ${p => p.of}px; +` diff --git a/src/components/layout/Default.js b/src/components/layout/Default.js index 7e109f7b..cece3bb3 100644 --- a/src/components/layout/Default.js +++ b/src/components/layout/Default.js @@ -21,7 +21,7 @@ import SettingsPage from 'components/SettingsPage' import AppRegionDrag from 'components/AppRegionDrag' import IsUnlocked from 'components/IsUnlocked' -import SideBar from 'components/SideBar' +import SideBar from 'components/MainSideBar' import TopBar from 'components/TopBar' const Container = styled(GrowScroll).attrs({