diff --git a/client/src/actions/modal-actions.js b/client/src/actions/modal-actions.js new file mode 100644 index 00000000..06f6c939 --- /dev/null +++ b/client/src/actions/modal-actions.js @@ -0,0 +1,16 @@ + +export default { + openModal(content) { + return { + type: 'OPEN_MODAL', + payload: content + } + }, + + closeModal() { + return { + type: 'CLOSE_MODAL', + payload: {} + }; + } +}; \ No newline at end of file diff --git a/client/src/app-components/are-you-sure.js b/client/src/app-components/are-you-sure.js new file mode 100644 index 00000000..a1502c09 --- /dev/null +++ b/client/src/app-components/are-you-sure.js @@ -0,0 +1,64 @@ +import React from 'react'; + +import i18n from 'lib-app/i18n'; +import Button from 'core-components/button'; + +class AreYouSure extends React.Component { + static propTypes = { + description: React.PropTypes.string, + onYes: React.PropTypes.func + }; + + static contextTypes = { + closeModal: React.PropTypes.func + }; + + componentDidMount() { + this.refs.yesButton && this.refs.yesButton.focus(); + } + + render() { + return ( +
+
+ {i18n('ARE_YOU_SURE')} +
+
+ {this.props.description} +
+
+
+ +
+
+ +
+
+
+ ); + } + + onYes() { + this.closeModal(); + + if (this.props.onYes) { + this.props.onYes(); + } + } + + onNo() { + this.closeModal(); + } + + closeModal() { + if (this.context.closeModal) { + this.context.closeModal(); + } + } +} + +export default AreYouSure; \ No newline at end of file diff --git a/client/src/app-components/are-you-sure.scss b/client/src/app-components/are-you-sure.scss new file mode 100644 index 00000000..921ed1c3 --- /dev/null +++ b/client/src/app-components/are-you-sure.scss @@ -0,0 +1,30 @@ +@import "../scss/vars"; + +.are-you-sure { + width: 400px; + text-align: center; + + &__header { + color: $primary-red; + font-size: $font-size--xl; + font-weight: bold; + margin-bottom: 20px; + + } + + &__description { + color: $dark-grey; + font-size: $font-size--md; + margin-bottom: 50px; + } + + &__butttons { + margin: 0 auto; + } + + &__yes-button, + &__no-button { + display: inline-block; + margin-right: 10px; + } +} \ No newline at end of file diff --git a/client/src/app/App.js b/client/src/app/App.js index d6639e06..00f2efdf 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -1,8 +1,11 @@ import React from 'react'; import _ from 'lodash'; +import classNames from 'classnames'; import { connect } from 'react-redux' import { browserHistory } from 'react-router'; +import ModalContainer from 'app/modal-container'; + class App extends React.Component { static contextTypes = { router: React.PropTypes.object, @@ -19,12 +22,24 @@ class App extends React.Component { render() { return ( -
- {React.cloneElement(this.props.children, {})} +
+
+ {React.cloneElement(this.props.children, {})} +
+
); } + getClass() { + let classes = { + 'application': true, + 'application_modal-opened': (this.props.modal.opened) + }; + + return classNames(classes); + } + redirectIfPathIsNotValid(props) { const validations = { languageChanged: props.config.language !== this.props.config.language, @@ -49,6 +64,7 @@ class App extends React.Component { export default connect((store) => { return { config: store.config, + modal: store.modal, session: store.session, routing: store.routing }; diff --git a/client/src/app/app.scss b/client/src/app/app.scss new file mode 100644 index 00000000..feccbaf6 --- /dev/null +++ b/client/src/app/app.scss @@ -0,0 +1,12 @@ +@import "../scss/vars"; + +.application { + padding: $half-space; + + &_modal-opened { + .application__content { + position: fixed; + overflow: hidden; + } + } +} \ No newline at end of file diff --git a/client/src/app/demo/components-demo-page.js b/client/src/app/demo/components-demo-page.js index 2d64b834..04473348 100644 --- a/client/src/app/demo/components-demo-page.js +++ b/client/src/app/demo/components-demo-page.js @@ -3,6 +3,9 @@ const React = require('react'); const DocumentTitle = require('react-document-title'); +const ModalContainer = require('app/modal-container'); +const AreYouSure = require('app-components/are-you-sure'); + const Button = require('core-components/button'); const Input = require('core-components/input'); const Checkbox = require('core-components/checkbox'); @@ -82,7 +85,21 @@ let DemoPage = React.createClass({ { title: 'Tooltip', render: ( - hola +
+ hola +
+ ) + }, + { + title: 'ModalTrigger', + render: ( + ) } ], diff --git a/client/src/app/modal-container.js b/client/src/app/modal-container.js new file mode 100644 index 00000000..c9591c31 --- /dev/null +++ b/client/src/app/modal-container.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import keyCode from 'keycode'; + +import store from 'app/store'; +import ModalActions from 'actions/modal-actions'; +import Modal from 'core-components/modal'; + +class ModalContainer extends React.Component { + + static openModal(content) { + store.dispatch( + ModalActions.openModal( + content + ) + ); + } + + static childContextTypes = { + closeModal: React.PropTypes.func + }; + + getChildContext() { + return { + closeModal: this.closeModal.bind(this) + }; + }; + + componentDidMount() { + window.addEventListener('keydown', this.onKeyDown.bind(this)); + } + + componentWillUnmount() { + window.removeEventListener('keydown', this.onKeyDown.bind(this)); + } + + render() { + return ( +
+ {(this.props.modal.opened) ? this.renderModal() : null} +
+ ); + } + + renderModal() { + return ( + + ); + } + + onKeyDown(event) { + if (event.keyCode === keyCode('ESCAPE')) { + this.closeModal(); + } + } + + closeModal() { + this.props.dispatch(ModalActions.closeModal()); + } +} + +export default connect((store) => { + return { + modal: store.modal + }; +})(ModalContainer); \ No newline at end of file diff --git a/client/src/core-components/__tests__/button-test.js b/client/src/core-components/__tests__/button-test.js index d60d41e6..a65ad853 100644 --- a/client/src/core-components/__tests__/button-test.js +++ b/client/src/core-components/__tests__/button-test.js @@ -25,7 +25,7 @@ describe('Button component', function () { ); - expect(ReactDOM.findDOMNode(button).getAttribute('class')).to.include('button-' + type); + expect(ReactDOM.findDOMNode(button).getAttribute('class')).to.include('button_' + type); }); }); }); diff --git a/client/src/core-components/button.js b/client/src/core-components/button.js index 8ad69522..222bedab 100644 --- a/client/src/core-components/button.js +++ b/client/src/core-components/button.js @@ -17,9 +17,16 @@ class Button extends React.Component { static propTypes = { children: React.PropTypes.node, + size: React.PropTypes.oneOf([ + 'small', + 'medium', + 'large', + 'auto' + ]), type: React.PropTypes.oneOf([ 'primary', - 'primary-icon', + 'secondary', + 'tertiary', 'clean', 'link' ]), @@ -32,7 +39,8 @@ class Button extends React.Component { }; static defaultProps = { - type: 'primary' + type: 'primary', + size: 'medium' }; render() { @@ -48,6 +56,7 @@ class Button extends React.Component { props.onClick = callback(this.handleClick.bind(this), this.props.onClick); props.className = this.getClass(); + props.ref = 'button'; delete props.route; delete props.iconName; @@ -59,10 +68,20 @@ class Button extends React.Component { getClass() { let classes = { 'button': true, - 'button_disabled': this.props.disabled + 'button_disabled': this.props.disabled, + + 'button_primary': (this.props.type === 'primary'), + 'button_secondary': (this.props.type === 'secondary'), + 'button_tertiary': (this.props.type === 'tertiary'), + 'button_clean': (this.props.type === 'clean'), + 'button_link': (this.props.type === 'link'), + + 'button_small': (this.props.size === 'small'), + 'button_medium': (this.props.size === 'medium'), + 'button_large': (this.props.size === 'large'), + 'button_auto': (this.props.size === 'auto') }; - classes['button-' + this.props.type] = (this.props.type); classes[this.props.className] = (this.props.className); return classNames(classes); @@ -73,6 +92,10 @@ class Button extends React.Component { this.context.router.push(this.props.route.to); } } + + focus() { + this.refs.button.focus(); + } } export default Button; diff --git a/client/src/core-components/button.scss b/client/src/core-components/button.scss index 34c58bea..89295775 100644 --- a/client/src/core-components/button.scss +++ b/client/src/core-components/button.scss @@ -2,39 +2,64 @@ .button { - &-primary, - &-primary-icon { + &_primary, + &_secondary, + &_tertiary { background-color: $primary-red; border: solid transparent; border-radius: 4px; color: white; height: 47px; text-transform: uppercase; - width: 239px; } - &-primary-icon { - width: initial; - height: initial; + &_secondary { + background-color: $primary-green; } - &-clean { - background: none; - border: none; - } - - &-link { - background: none; - border: none; - color: $dark-grey; - text-decoration: underline; - - &:focus { - outline: none; - } + &_tertiary { + background-color: $secondary-blue; } &_disabled { background-color: #ec9696; } + + &_small { + width: 100px; + height: 47px; + } + + &_medium { + width: 239px; + } + + &_large { + //width: 239px; + } + + &_auto { + width: initial; + height: initial; + } + + &_clean { + background: none; + border: none; + width: initial; + height: initial; + } + + &_link { + background: none; + border: none; + color: $dark-grey; + width: initial; + height: initial; + + &:focus, &:hover { + outline: none; + text-decoration: underline; + } + } } \ No newline at end of file diff --git a/client/src/core-components/checkbox.js b/client/src/core-components/checkbox.js index 28a48ffa..4c4bfe32 100644 --- a/client/src/core-components/checkbox.js +++ b/client/src/core-components/checkbox.js @@ -1,7 +1,7 @@ import React from 'react'; import classNames from 'classnames'; import _ from 'lodash'; -import keyCode from 'keycode'; +import keyCode from 'keycode'; import callback from 'lib-core/callback'; import getIcon from 'lib-core/get-icon'; diff --git a/client/src/core-components/input.scss b/client/src/core-components/input.scss index d3b88eee..2bcb1f54 100644 --- a/client/src/core-components/input.scss +++ b/client/src/core-components/input.scss @@ -44,7 +44,7 @@ } .input__text { - padding-left: 40px; + padding-left: 48px; } } diff --git a/client/src/core-components/modal.js b/client/src/core-components/modal.js new file mode 100644 index 00000000..64fb8b15 --- /dev/null +++ b/client/src/core-components/modal.js @@ -0,0 +1,42 @@ +import React from 'react'; + +import {Motion, spring} from 'react-motion'; + +class Modal extends React.Component { + static propTypes = { + content: React.PropTypes.node + }; + + render() { + return ( + + {this.renderModal.bind(this)} + + ); + } + + getAnimations() { + return { + defaultStyle: { + scale: spring(0.7), + fade: spring(0.5) + }, + style: { + scale: spring(1), + fade: spring(1) + } + } + } + + renderModal(animation) { + return ( +
+
+ {this.props.content} +
+
+ ) + } +} + +export default Modal; \ No newline at end of file diff --git a/client/src/core-components/modal.scss b/client/src/core-components/modal.scss new file mode 100644 index 00000000..061142c6 --- /dev/null +++ b/client/src/core-components/modal.scss @@ -0,0 +1,18 @@ +.modal { + position: absolute; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.8); + width: 100%; + height: 100%; + display: flex; + + &__content { + position: relative; + margin: auto; + background-color: white; + border-radius: 4px; + padding: 50px; + box-shadow: 0 0 10px white; + } +} \ No newline at end of file diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index 6efc2e4b..c4da923d 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -32,6 +32,8 @@ export default { 'NO_ATTACHMENT': 'No file attachment', 'STAFF': 'Staff', 'CUSTOMER': 'Customer', + 'YES': 'Yes', + 'CANCEL': 'Cancel', //ERRORS 'EMAIL_NOT_EXIST': 'Email does not exist', @@ -49,5 +51,6 @@ export default { 'SIGNUP_SUCCESS': 'You have registered successfully in our support system.', 'TICKET_SENT': 'Ticket has been created successfully.', 'VALID_RECOVER': 'Password recovered successfully', - 'EMAIL_EXISTS': 'Email already exists, please try to log in or recover password' + 'EMAIL_EXISTS': 'Email already exists, please try to log in or recover password', + 'ARE_YOU_SURE': 'Are you sure?' }; \ No newline at end of file diff --git a/client/src/main.scss b/client/src/main.scss index b7bd9324..9bc26c11 100644 --- a/client/src/main.scss +++ b/client/src/main.scss @@ -5,4 +5,5 @@ @import 'scss/font_awesome/font-awesome'; @import 'core-components/*'; +@import 'app-components/*'; @import 'app/*'; diff --git a/client/src/reducers/_reducers.js b/client/src/reducers/_reducers.js index b5745f14..b63ba602 100644 --- a/client/src/reducers/_reducers.js +++ b/client/src/reducers/_reducers.js @@ -3,9 +3,11 @@ import { routerReducer } from 'react-router-redux'; import sessionReducer from 'reducers/session-reducer'; import configReducer from 'reducers/config-reducer'; +import modalReducer from 'reducers/modal-reducer'; export default combineReducers({ session: sessionReducer, config: configReducer, + modal: modalReducer, routing: routerReducer }); \ No newline at end of file diff --git a/client/src/reducers/modal-reducer.js b/client/src/reducers/modal-reducer.js new file mode 100644 index 00000000..d49459e6 --- /dev/null +++ b/client/src/reducers/modal-reducer.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; + +import Reducer from 'reducers/reducer'; + +class ModalReducer extends Reducer { + + getInitialState() { + return { + opened: false, + content: null + }; + } + + getTypeHandlers() { + return { + 'OPEN_MODAL': this.onOpenModal, + 'CLOSE_MODAL': this.onCloseModal + }; + } + + onOpenModal(state, payload) { + return _.extend({}, state, { + opened: true, + content: payload + }); + } + + onCloseModal(state) { + return _.extend({}, state, { + opened: false, + content: null + }); + } +} + +export default ModalReducer.getInstance(); \ No newline at end of file diff --git a/client/src/scss/_base.scss b/client/src/scss/_base.scss index cba21b56..11a6fffb 100644 --- a/client/src/scss/_base.scss +++ b/client/src/scss/_base.scss @@ -5,5 +5,4 @@ body { font-family: Helvetica, sans-serif; background-color: $light-grey; - padding: $half-space; } \ No newline at end of file