Browse Source

Merge pull request #438 from meriadec/design/refacto-sidebar

Rewrite SideBar component
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
4b9f25064a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 173
      src/components/MainSideBar.js
  2. 146
      src/components/SideBar/Item.js
  3. 178
      src/components/SideBar/index.js
  4. 71
      src/components/base/SideBar/SideBarList.js
  5. 94
      src/components/base/SideBar/SideBarListItem.js
  6. 3
      src/components/base/SideBar/index.js
  7. 62
      src/components/base/SideBar/stories.js
  8. 5
      src/components/base/Space.js
  9. 2
      src/components/layout/Default.js

173
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<Props> {
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: () => (
<FormattedVal
alwaysShowSign={false}
color="graphite"
unit={account.unit}
showCode
val={account.balance || 0}
/>
),
iconActiveColor: account.currency.color,
icon: getCryptoCurrencyIcon(account.currency),
onClick: () => this.push(accountURL),
isActive: pathname === accountURL,
}
})
return (
<Box bg="white" style={{ width: 230 }}>
<Space of={70} />
<SideBarList title={t('sidebar:menu')} items={navigationItems} />
<Space of={40} />
<SideBarList
scroll
title={t('sidebar:accounts')}
titleRight={
<PlusWrapper onClick={() => openModal('importAccounts')}>
<IconPlus size={16} />
</PlusWrapper>
}
items={accountsItems}
emptyText={t('emptyState:sidebar.text')}
/>
</Box>
)
}
}
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)

146
src/components/SideBar/Item.js

@ -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 (
<Container
big={big}
iconActiveColor={iconActiveColor}
isActive={isActive}
onClick={
linkTo
? isActive
? undefined
: () => push(linkTo)
: modal
? () => openModal(modal)
: void 0
}
>
{icon && (
<Box color={isActive ? iconActiveColor : void 0} className="desaturate">
{icon}
</Box>
)}
<Box justifyContent="center" className="desaturate">
<Text fontSize={4}>{children}</Text>
{desc && (
<Box color="graphite" fontSize={3}>
{desc}
</Box>
)}
</Box>
{highlight && (
<Box flex="1" align="flex-end" pr={2}>
<Bullet />
</Box>
)}
</Container>
)
}
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)

178
src/components/SideBar/index.js

@ -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<Props> {
render() {
const { t, openModal, updateStatus, accounts } = this.props
return (
<Container bg="white">
<Box flow={7} pt={8} grow>
<Box flow={4}>
<CapsSubtitle>{t('sidebar:menu')}</CapsSubtitle>
<Box px={4} flow={2}>
<Item
icon={<IconPieChart size={16} />}
linkTo="/"
highlight={updateStatus === 'downloaded'}
>
{t('dashboard:title')}
</Item>
<Item icon={<IconSend size={16} />} modal={MODAL_SEND}>
{t('send:title')}
</Item>
<Item icon={<IconReceive size={16} />} modal={MODAL_RECEIVE}>
{t('receive:title')}
</Item>
<Item icon={<IconManager size={16} />} linkTo="/manager">
{t('sidebar:manager')}
</Item>
<Item icon={<IconExchange size={16} />} linkTo="/exchange">
{t('sidebar:exchange')}
</Item>
</Box>
</Box>
<Box flow={4} grow pt={1}>
<CapsSubtitle horizontal alignItems="center">
<Box grow>{t('sidebar:accounts')}</Box>
<Tooltip render={() => t('addAccount:title')}>
<PlusBtn onClick={() => openModal('importAccounts')}>
<IconPlus size={16} />
</PlusBtn>
</Tooltip>
</CapsSubtitle>
<GrowScroll pb={4} px={4} flow={2}>
{accounts.length > 0 ? (
<AccountsList />
) : (
<NoAccountsText>{t('emptyState:sidebar.text')}</NoAccountsText>
)}
</GrowScroll>
</Box>
</Box>
</Container>
)
}
}
const AccountsList = compose(
withRouter,
connect(
state => ({
accounts: accountsSelector(state),
}),
null,
null,
{ pure: false },
),
)(({ accounts }: { accounts: Account[] }) => (
<Fragment>
{accounts.map(account => {
const Icon = getCryptoCurrencyIcon(account.currency)
return (
<Item
big
desc={
<FormattedVal
alwaysShowSign={false}
color="graphite"
unit={account.unit}
showCode
val={account.balance || 0}
/>
}
iconActiveColor={account.currency.color}
icon={Icon ? <Icon size={16} /> : null}
key={account.id}
linkTo={`/account/${account.id}`}
>
{account.name}
</Item>
)
})}
</Fragment>
))
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,
})``

71
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<Props> {
render() {
const { items, title, activeValue, scroll, titleRight, emptyText, ...props } = this.props
const ListWrapper = scroll ? GrowScroll : Box
return (
<Fragment>
{!!title && (
<Fragment>
<SideBarListTitle>
{title}
{!!titleRight && <Box ml="auto">{titleRight}</Box>}
</SideBarListTitle>
<Space of={20} />
</Fragment>
)}
{items.length > 0 ? (
<ListWrapper flow={2} px={3} fontSize={3} {...props}>
{items.map(item => {
const itemProps = {
item,
isActive: item.isActive || (!!activeValue && activeValue === item.value),
}
return <SideBarListItem key={item.value} {...itemProps} />
})}
</ListWrapper>
) : emptyText ? (
<Box px={4} ff="Open Sans|Regular" fontSize={3} color="grey">
{emptyText}
</Box>
) : null}
</Fragment>
)
}
}
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

94
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<any>),
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<Props> {
render() {
const {
item: { icon: Icon, label, desc, iconActiveColor, hasNotif, onClick },
isActive,
} = this.props
return (
<Container isActive={isActive} iconActiveColor={iconActiveColor} onClick={onClick}>
{!!Icon && <Icon size={16} />}
<Box grow shrink>
{typeof label === 'function' ? (
label(this.props)
) : (
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{label}
</span>
)}
{!!desc && desc(this.props)}
</Box>
{!!hasNotif && <Bullet />}
</Container>
)
}
}
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

3
src/components/base/SideBar/index.js

@ -0,0 +1,3 @@
// @flow
export { default as SideBarList } from './SideBarList'

62
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: () => (
<Box>
{'custom'}
<Box>{'render'}</Box>
</Box>
),
icon: IconCurrencies,
iconActiveColor: '#3ca569',
},
{
value: 'fifth',
label: 'Fifth',
icon: IconExclamationCircle,
iconActiveColor: '#0e76aa',
},
]
stories.add('SideBarList', () => (
<SideBarList
items={SIDEBAR_ITEMS}
activeValue={select('activeValue', [...SIDEBAR_ITEMS.map(i => i.value), null], 'third')}
/>
))

5
src/components/base/Space.js

@ -0,0 +1,5 @@
import styled from 'styled-components'
export default styled.div`
height: ${p => p.of}px;
`

2
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({

Loading…
Cancel
Save