Browse Source

Create the Modal component and associated reducer

and make use of it in the SideBar component, to open the send and
receive modals.
master
meriadec 7 years ago
parent
commit
2a473e3378
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 1
      package.json
  2. 13
      src/components/ReceiveModal.js
  3. 13
      src/components/SendModal.js
  4. 35
      src/components/SideBar/Item.js
  5. 4
      src/components/SideBar/index.js
  6. 4
      src/components/Wrapper.js
  7. 98
      src/components/base/Modal.js
  8. 2
      src/reducers/index.js
  9. 50
      src/reducers/modals.js
  10. 33
      yarn.lock

1
package.json

@ -39,6 +39,7 @@
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-i18next": "^7.3.1",
"react-mortal": "^3.0.1",
"react-redux": "^5.0.6",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",

13
src/components/ReceiveModal.js

@ -0,0 +1,13 @@
// @flow
import React, { PureComponent } from 'react'
import Modal from 'components/base/Modal'
class ReceiveModal extends PureComponent {
render() {
return <Modal name="receive">receive modal</Modal>
}
}
export default ReceiveModal

13
src/components/SendModal.js

@ -0,0 +1,13 @@
// @flow
import React, { PureComponent } from 'react'
import Modal from 'components/base/Modal'
class SendModal extends PureComponent {
render() {
return <Modal name="send">send modal</Modal>
}
}
export default SendModal

35
src/components/SideBar/Item.js

@ -7,6 +7,8 @@ import { withRouter } from 'react-router'
import { push } from 'react-router-redux'
import { connect } from 'react-redux'
import { openModal, isModalOpened } from 'reducers/modals'
import type { Element } from 'react'
import type { Location } from 'react-router'
@ -16,14 +18,25 @@ import Text from 'components/base/Text'
type Props = {
children: string,
linkTo?: string | null,
modal?: string | null,
desc?: string | null,
icon?: Element<*> | null,
location: Location,
isModalOpened: boolean,
push: Function,
openModal: Function,
}
const mapStateToProps = (state, { modal }) => ({
// connect router here only to make components re-render
// see https://github.com/ReactTraining/react-router/issues/4671
router: state.router,
isModalOpened: modal ? isModalOpened(state, modal) : false,
})
const mapDispatchToProps = {
push,
openModal,
}
const Container = styled(Box).attrs({
@ -46,10 +59,23 @@ const IconWrapper = styled(Box)`
border: 2px solid rgba(255, 255, 255, 0.1);
`
function Item({ children, desc, icon, linkTo, push, location }: Props) {
function Item({
children,
desc,
icon,
linkTo,
push,
location,
modal,
openModal,
isModalOpened,
}: Props) {
const { pathname } = location
return (
<Container onClick={linkTo ? () => push(linkTo) : void 0} active={pathname === linkTo}>
<Container
onClick={linkTo ? () => push(linkTo) : modal ? () => openModal(modal) : void 0}
active={pathname === linkTo || isModalOpened}
>
<IconWrapper mr={2}>{icon || null}</IconWrapper>
<div>
<Text fontWeight="bold" fontSize={1}>
@ -69,13 +95,12 @@ Item.defaultProps = {
icon: null,
desc: null,
linkTo: null,
modal: null,
}
export default compose(
withRouter,
// connect router here only to make components re-render
// see https://github.com/ReactTraining/react-router/issues/4671
connect(({ router }) => ({ router }), mapDispatchToProps, null, {
connect(mapStateToProps, mapDispatchToProps, null, {
pure: false,
}),
)(Item)

4
src/components/SideBar/index.js

@ -24,8 +24,8 @@ class SideBar extends PureComponent<{}> {
<CapsSubtitle>{'Menu'}</CapsSubtitle>
<div>
<Item linkTo="/">{'Dashboard'}</Item>
<Item>{'Send'}</Item>
<Item>{'Receive'}</Item>
<Item modal="send">{'Send'}</Item>
<Item modal="receive">{'Receive'}</Item>
<Item linkTo="/settings">{'Settings'}</Item>
</div>
</Box>

4
src/components/Wrapper.js

@ -9,6 +9,8 @@ import Box from 'components/base/Box'
import DashboardPage from 'components/DashboardPage'
import SettingsPage from 'components/SettingsPage'
import AccountPage from 'components/AccountPage'
import SendModal from 'components/SendModal'
import ReceiveModal from 'components/ReceiveModal'
import SideBar from 'components/SideBar'
import TopBar from 'components/TopBar'
@ -22,6 +24,8 @@ const Wrapper = () => (
<Route path="/settings" component={SettingsPage} />
<Route path="/account/:account" component={AccountPage} />
</Box>
<SendModal />
<ReceiveModal />
</Box>
)

98
src/components/base/Modal.js

@ -0,0 +1,98 @@
// @flow
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import Mortal from 'react-mortal'
import styled from 'styled-components'
import { closeModal, isModalOpened } from 'reducers/modals'
type Props = {
isOpened: boolean,
onClose: Function,
children: any,
}
const springConfig = {
stiffness: 350,
}
const mapStateToProps = (state, { name, isOpened }) => ({
isOpened: isOpened || (name && isModalOpened(state, name)),
})
const mapDispatchToProps = (dispatch, { name }) => ({
onClose: name ? () => dispatch(closeModal(name)) : undefined,
})
const Container = styled.div.attrs({
style: p => ({
pointerEvents: p.isVisible ? 'auto' : 'none',
}),
})`
position: fixed;
z-index: 1;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
display: flex;
align-items: flex-start;
justify-content: center;
`
const Backdrop = styled.div.attrs({
style: p => ({
opacity: p.op,
}),
})`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
`
const Body = styled.div.attrs({
style: p => ({
opacity: p.op,
transform: `translate3d(0, ${p.offset}px, 0)`,
}),
})`
padding: 20px;
margin-top: 100px;
background: white;
width: 400px;
z-index: 2;
`
class Modal extends PureComponent<Props> {
render() {
const { isOpened, onClose, children } = this.props
return (
<Mortal
isOpened={isOpened}
onClose={onClose}
motionStyle={(spring, isVisible) => ({
opacity: spring(isVisible ? 1 : 0, springConfig),
y: spring(isVisible ? 0 : 20, springConfig),
})}
>
{(m, isVisible) => (
<Container isVisible={isVisible}>
<Backdrop op={m.opacity} onClick={onClose} />
<Body op={m.opacity} offset={m.y}>
{children}
</Body>
</Container>
)}
</Mortal>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Modal)

2
src/reducers/index.js

@ -4,8 +4,10 @@ import { combineReducers } from 'redux'
import { routerReducer as router } from 'react-router-redux'
import devices from './devices'
import modals from './modals'
export default combineReducers({
router,
devices,
modals,
})

50
src/reducers/modals.js

@ -0,0 +1,50 @@
// @flow
import { handleActions, createAction } from 'redux-actions'
const state = {}
type OpenPayload = {
name: string,
data?: Object | null,
}
type ClosePayload = {
name: string,
}
const handlers = {
MODAL_OPEN: (state, { payload }: { payload: OpenPayload }) => {
const { name, data } = payload
return {
...state,
[name]: {
isOpened: true,
data,
},
}
},
MODAL_CLOSE: (state, { payload }: { payload: ClosePayload }) => {
const { name } = payload
return {
...state,
[name]: {
isOpened: false,
data: null,
},
}
},
}
// Actions
export const openModal = createAction('MODAL_OPEN', (name, data = {}) => ({ name, data }))
export const closeModal = createAction('MODAL_CLOSE', name => ({ name }))
// Selectors
export const isModalOpened = (state, name) => state.modals[name] && state.modals[name].isOpened
// Exporting reducer
export default handleActions(handlers, state)

33
yarn.lock

@ -5901,7 +5901,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0:
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0:
version "15.6.0"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
@ -5993,6 +5993,12 @@ querystringify@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb"
raf@^3.1.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
dependencies:
performance-now "^2.1.0"
randomatic@^1.1.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
@ -6063,6 +6069,29 @@ react-i18next@^7.3.1:
html-parse-stringify2 "2.0.1"
prop-types "^15.6.0"
react-mortal@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/react-mortal/-/react-mortal-3.0.1.tgz#be8477513deb08ceb22ba8ae4b52220dc5c741ff"
dependencies:
prop-types "^15.6.0"
react "^16.1.1"
react-motion "^0.5.0"
react-portal "^4.0.0"
react-motion@^0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
dependencies:
performance-now "^0.2.0"
prop-types "^15.5.8"
raf "^3.1.0"
react-portal@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.1.2.tgz#7f28f3c8c2ed5c541907c0ed0f24e8996acf627f"
dependencies:
prop-types "^15.5.8"
react-redux@^5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946"
@ -6111,7 +6140,7 @@ react-stand-in@^4.0.0-beta.14:
dependencies:
shallowequal "^1.0.2"
react@^16.2.0:
react@^16.1.1, react@^16.2.0:
version "16.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
dependencies:

Loading…
Cancel
Save