diff --git a/client/gulp/tasks/browserSync.js b/client/gulp/tasks/browserSync.js index 5a2ed387..20417584 100644 --- a/client/gulp/tasks/browserSync.js +++ b/client/gulp/tasks/browserSync.js @@ -8,7 +8,7 @@ gulp.task('browserSync', function() { browserSync({ proxy: 'localhost:' + config.serverport, - startPath: 'app' + startPath: '/' }); }); \ No newline at end of file diff --git a/client/gulp/tasks/browserify.js b/client/gulp/tasks/browserify.js index 87726e2a..352a2ebe 100644 --- a/client/gulp/tasks/browserify.js +++ b/client/gulp/tasks/browserify.js @@ -21,7 +21,7 @@ var util = require('gulp-util'); function buildScript(file, watch) { var bundler = browserify({ - entries: [config.sourceDir + 'app/' + file], + entries: [config.sourceDir + file], debug: !global.isProd, insertGlobalVars: { noFixtures: function() { diff --git a/client/package.json b/client/package.json index f25dea66..09757c3a 100644 --- a/client/package.json +++ b/client/package.json @@ -56,6 +56,7 @@ "app-module-path": "^1.0.3", "classnames": "^2.1.3", "jquery": "^2.1.4", + "keycode": "^2.1.4", "localStorage": "^1.0.3", "lodash": "^3.10.0", "messageformat": "^0.2.2", diff --git a/client/src/actions/__tests__/session-actions-test.js b/client/src/actions/__tests__/session-actions-test.js index 642f1fa4..432970a9 100644 --- a/client/src/actions/__tests__/session-actions-test.js +++ b/client/src/actions/__tests__/session-actions-test.js @@ -11,42 +11,55 @@ const SessionActions = requireUnit('actions/session-actions', { }); describe('Session Actions,', function () { - APICallMock.call.returns('API_RESULT'); describe('login action', function () { - it('should return LOGIN with with API_RESULT promise', function () { - APICallMock.call.reset(); + it('should return LOGIN with with a result promise', function () { + APICallMock.call.returns({ + then: function (resolve) { + resolve({ + data: { + userId: 14 + } + }); + } + }); + let loginData = { email: 'SOME_EMAIL', password: 'SOME_PASSWORD', remember: false }; - expect(SessionActions.login(loginData)).to.deep.equal({ - type: 'LOGIN', - payload: 'API_RESULT' - }); - + expect(SessionActions.login(loginData).type).to.equal('LOGIN'); + expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'}); expect(APICallMock.call).to.have.been.calledWith({ - path: '/user/login', - data: loginData + path: '/user/get', + data: { + userId: 14 + } }); }); }); describe('autoLogin action', function () { it('should return LOGIN_AUTO with remember data from sessionStore', function () { - APICallMock.call.reset(); + APICallMock.call.returns({ + then: function (resolve) { + resolve({ + data: { + userId: 14 + } + }); + } + }); sessionStoreMock.getRememberData.returns({ token: 'SOME_TOKEN', userId: 'SOME_ID', expiration: 'SOME_EXPIRATION' }); - expect(SessionActions.autoLogin()).to.deep.equal({ - type: 'LOGIN_AUTO', - payload: 'API_RESULT' - }); + expect(SessionActions.autoLogin().type).to.equal('LOGIN_AUTO'); + expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'}); expect(APICallMock.call).to.have.been.calledWith({ path: '/user/login', data: { @@ -60,6 +73,7 @@ describe('Session Actions,', function () { describe('logout action', function () { it('should return LOGOUT and call /user/logout', function () { + APICallMock.call.returns('API_RESULT'); APICallMock.call.reset(); expect(SessionActions.logout()).to.deep.equal({ diff --git a/client/src/actions/session-actions.js b/client/src/actions/session-actions.js index f4ca14fc..ed9490b4 100644 --- a/client/src/actions/session-actions.js +++ b/client/src/actions/session-actions.js @@ -9,6 +9,10 @@ export default { payload: API.call({ path: '/user/login', data: loginData + }).then((result) => { + store.dispatch(this.getUserData(result.data.userId)); + + return result; }) }; }, @@ -25,6 +29,10 @@ export default { rememberToken: rememberData.token, isAutomatic: true } + }).then((result) => { + store.dispatch(this.getUserData(result.data.userId)); + + return result; }) }; }, @@ -39,6 +47,18 @@ export default { }; }, + getUserData(userId) { + return { + type: 'USER_DATA', + payload: API.call({ + path: '/user/get', + data: { + userId: userId + } + }) + } + }, + initSession() { return { type: 'CHECK_SESSION', diff --git a/client/src/app/App.js b/client/src/app/App.js index 4aca0e3c..d6639e06 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -28,8 +28,8 @@ class App extends React.Component { redirectIfPathIsNotValid(props) { const validations = { languageChanged: props.config.language !== this.props.config.language, - loggedIn: !_.includes(props.location.pathname, '/app/dashboard') && props.session.logged, - loggedOut: _.includes(props.location.pathname, '/app/dashboard') && !props.session.logged + loggedIn: !_.includes(props.location.pathname, '/dashboard') && props.session.logged, + loggedOut: _.includes(props.location.pathname, '/dashboard') && !props.session.logged }; if (validations.languageChanged) { @@ -37,11 +37,11 @@ class App extends React.Component { } if (validations.loggedOut) { - browserHistory.push('/app'); + browserHistory.push('/'); } if (validations.loggedIn) { - browserHistory.push('/app/dashboard'); + browserHistory.push('/dashboard'); } } } diff --git a/client/src/app/Routes.js b/client/src/app/Routes.js index 1d71b369..317b4081 100644 --- a/client/src/app/Routes.js +++ b/client/src/app/Routes.js @@ -27,8 +27,8 @@ const history = syncHistoryWithStore(browserHistory, store); export default ( - - + + @@ -40,7 +40,7 @@ export default ( - + diff --git a/client/src/app/main/dashboard/dashboard-layout.js b/client/src/app/main/dashboard/dashboard-layout.js index 89f9c0a7..93e66076 100644 --- a/client/src/app/main/dashboard/dashboard-layout.js +++ b/client/src/app/main/dashboard/dashboard-layout.js @@ -2,14 +2,17 @@ import React from 'react'; import {connect} from 'react-redux'; import DashboardMenu from 'app/main/dashboard/dashboard-menu'; +import Widget from 'core-components/widget'; class DashboardLayout extends React.Component { render() { return (this.props.session.logged) ? ( -
-
-
{this.props.children}
+
+
+ + {this.props.children} +
) : null; } diff --git a/client/src/app/main/dashboard/dashboard-layout.scss b/client/src/app/main/dashboard/dashboard-layout.scss new file mode 100644 index 00000000..a61782a9 --- /dev/null +++ b/client/src/app/main/dashboard/dashboard-layout.scss @@ -0,0 +1,9 @@ +.dashboard { + &__menu { + + } + + &__content { + + } +} \ No newline at end of file diff --git a/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js index 8374828e..184bf375 100644 --- a/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js +++ b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js @@ -1,14 +1,76 @@ import React from 'react'; +import {connect} from 'react-redux'; + +import Table from 'core-components/table'; +import Button from 'core-components/button'; class DashboardListTicketsPage extends React.Component { + static propTypes = { + tickets: React.PropTypes.arrayOf(React.PropTypes.object) + }; + + static defaultProps = { + tickets: [] + }; render() { return ( -
- DASHBOARD TICKET LIST +
+
Tickets
+ ); } + + getTableHeaders() { + return [ + { + key: 'number', + value: 'Number', + className: 'dashboard-ticket-list__number col-md-1' + }, + { + key: 'title', + value: 'Title', + className: 'dashboard-ticket-list__title col-md-6' + }, + { + key: 'department', + value: 'Department', + className: 'dashboard-ticket-list__department col-md-3' + }, + { + key: 'date', + value: 'Date', + className: 'dashboard-ticket-list__date col-md-2' + } + ]; + } + + getTableRows() { + return this.props.tickets.map(this.gerTicketTableObject.bind(this)); + } + + gerTicketTableObject(ticket) { + let titleText = (ticket.unread) ? ticket.title + ' (1)' : ticket.title; + + return { + number: '#' + ticket.ticketNumber, + title: ( + + ), + department: ticket.department.name, + date: ticket.date, + highlighted: ticket.unread + }; + } } -export default DashboardListTicketsPage; + +export default connect((store) => { + return { + tickets: store.session.userTickets + }; +})(DashboardListTicketsPage); diff --git a/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.scss b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.scss new file mode 100644 index 00000000..19223237 --- /dev/null +++ b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.scss @@ -0,0 +1,32 @@ +@import "../../../../scss/vars"; + +.dashboard-ticket-list { + + &__header { + text-align: left; + font-variant: small-caps; + font-size: 16px; + } + + &__number { + text-align: left; + } + + &__title { + text-align: left; + } + + &__department { + text-align: right; + } + + &__date { + text-align: right; + } + + &__title-link:hover, + &__title-link:focus { + outline: none; + text-decoration: underline; + } +} \ No newline at end of file diff --git a/client/src/app/main/dashboard/dashboard-menu.js b/client/src/app/main/dashboard/dashboard-menu.js index 5066647f..a44c470f 100644 --- a/client/src/app/main/dashboard/dashboard-menu.js +++ b/client/src/app/main/dashboard/dashboard-menu.js @@ -1,14 +1,11 @@ import React from 'react'; import _ from 'lodash'; -import Menu from 'core-components/menu'; +import {dispatch} from 'app/store'; +import SessionActions from 'actions/session-actions'; +import i18n from 'lib-app/i18n'; -let dashboardRoutes = [ - { path: '/app/dashboard', text: 'Ticket List' }, - { path: '/app/dashboard/create-ticket', text: 'Create Ticket' }, - { path: '/app/dashboard/articles', text: 'View Articles' }, - { path: '/app/dashboard/edit-profile', text: 'Edit Profile' } -]; +import Menu from 'core-components/menu'; class DashboardMenu extends React.Component { static contextTypes = { @@ -27,30 +24,62 @@ class DashboardMenu extends React.Component { getProps() { return { + header: 'Dashboard', items: this.getMenuItems(), selectedIndex: this.getSelectedIndex(), - onItemClick: this.goToPathByIndex.bind(this) + onItemClick: this.onItemClick.bind(this), + tabbable: true, + type: 'secondary' }; } getMenuItems() { - return dashboardRoutes.map(this.getMenuItem.bind(this)); + let items = this.getDashboardRoutes().map(this.getMenuItem.bind(this)); + + items.push(this.getCloseSessionItem()); + + return items; } getMenuItem(item) { return { - content: item.text + content: item.text, + icon: item.icon }; } + getCloseSessionItem() { + return { + content: i18n('CLOSE_SESSION'), + icon: 'lock' + } + } + getSelectedIndex() { let pathname = this.props.location.pathname; - return _.findIndex(dashboardRoutes, {path: pathname}); + return _.findIndex(this.getDashboardRoutes(), {path: pathname}); + } + + onItemClick(itemIndex) { + if (itemIndex < this.getDashboardRoutes().length) { + this.goToPathByIndex(itemIndex) + } else { + dispatch(SessionActions.logout()); + } } goToPathByIndex(itemIndex) { - this.context.router.push(dashboardRoutes[itemIndex].path); + this.context.router.push(this.getDashboardRoutes()[itemIndex].path); + } + + getDashboardRoutes() { + return [ + { path: '/dashboard', text: i18n('TICKET_LIST'), icon: 'file-text-o' }, + { path: '/dashboard/create-ticket', text: i18n('CREATE_TICKET'), icon: 'plus' }, + { path: '/dashboard/articles', text: i18n('VIEW_ARTICLES'), icon: 'book' }, + { path: '/dashboard/edit-profile', text: i18n('EDIT_PROFILE'), icon: 'pencil' } + ]; } } diff --git a/client/src/app/main/main-layout-header.js b/client/src/app/main/main-layout-header.js index b6897723..43bc7e45 100644 --- a/client/src/app/main/main-layout-header.js +++ b/client/src/app/main/main-layout-header.js @@ -34,16 +34,16 @@ class MainLayoutHeader extends React.Component { if (this.props.session.logged) { result = ( -
- Welcome, John - +
+ {i18n('WELCOME')}, + {this.props.session.userName}
); } else { result = ( -
- - +
+ +
); } @@ -53,7 +53,7 @@ class MainLayoutHeader extends React.Component { getLanguageSelectorProps() { return { - className: 'main-layout-header--languages', + className: 'main-layout-header__languages', items: this.getLanguageList(), selectedIndex: Object.values(codeLanguages).indexOf(this.props.config.language), onChange: this.changeLanguage.bind(this) @@ -74,10 +74,6 @@ class MainLayoutHeader extends React.Component { this.props.dispatch(ConfigActions.changeLanguage(codeLanguages[language])); } - - logout() { - this.props.dispatch(SessionActions.logout()); - } } export default connect((store) => { diff --git a/client/src/app/main/main-layout-header.scss b/client/src/app/main/main-layout-header.scss index fbb2feda..b4491020 100644 --- a/client/src/app/main/main-layout-header.scss +++ b/client/src/app/main/main-layout-header.scss @@ -6,7 +6,11 @@ height: 32px; width: 100%; - &--login-links { + &__user-name { + color: $primary-red; + } + + &__login-links { border-top-left-radius: 4px; color: white; display: inline-block; @@ -14,7 +18,7 @@ padding: 5px 20px 0 10px; } - &--languages { + &__languages { float: right; position: relative; top: 5px; diff --git a/client/src/app/main/main-recover-password/main-recover-password-page.js b/client/src/app/main/main-recover-password/main-recover-password-page.js index 4c6580ae..451bcc05 100644 --- a/client/src/app/main/main-recover-password/main-recover-password-page.js +++ b/client/src/app/main/main-recover-password/main-recover-password-page.js @@ -74,7 +74,7 @@ class MainRecoverPasswordPage extends React.Component { } onPasswordRecovered() { - setTimeout(() => {this.props.history.push('/app')}, 2000); + setTimeout(() => {this.props.history.push('/')}, 2000); this.setState({ recoverStatus: 'valid', loading: false diff --git a/client/src/core-components/checkbox.js b/client/src/core-components/checkbox.js index f5261aa7..8394772d 100644 --- a/client/src/core-components/checkbox.js +++ b/client/src/core-components/checkbox.js @@ -1,6 +1,7 @@ import React from 'react'; import classNames from 'classnames'; import _ from 'lodash'; +import keyCode from 'keycode'; import callback from 'lib-core/callback'; import getIcon from 'lib-core/get-icon'; @@ -87,7 +88,7 @@ class CheckBox extends React.Component { } handleIconKeyDown(event) { - if (event.keyCode == 32) { + if (event.keyCode === keyCode('SPACE')) { event.preventDefault(); callback(this.handleChange.bind(this), this.props.onChange)({ diff --git a/client/src/core-components/menu.js b/client/src/core-components/menu.js index 79740fb7..1a102038 100644 --- a/client/src/core-components/menu.js +++ b/client/src/core-components/menu.js @@ -1,33 +1,50 @@ import React from 'react'; import _ from 'lodash'; import classNames from 'classnames'; +import keyCode from 'keycode'; import Icon from 'core-components/icon'; class Menu extends React.Component { static propTypes = { + header: React.PropTypes.string, type: React.PropTypes.oneOf(['primary', 'secondary']), items: React.PropTypes.arrayOf(React.PropTypes.shape({ content: React.PropTypes.string.isRequired, icon: React.PropTypes.string })).isRequired, - selectedIndex: React.PropTypes.number + selectedIndex: React.PropTypes.number, + tabbable: React.PropTypes.bool }; static defaultProps = { type: 'primary', - selectedIndex: 0 + selectedIndex: 0, + tabbable: false }; render() { return ( -
    - {this.props.items.map(this.renderListItem.bind(this))} -
+
+ {this.renderHeader()} +
    + {this.props.items.map(this.renderListItem.bind(this))} +
+
) } + renderHeader() { + let header = null; + + if (this.props.header) { + header =
{this.props.header}
; + } + + return header; + } + renderListItem(item, index) { let iconNode = null; @@ -45,11 +62,13 @@ class Menu extends React.Component { getProps() { var props = _.clone(this.props); - props.className = this.getClass(); + props.className = 'menu__list'; + delete props.header; delete props.items; delete props.onItemClick; delete props.selectedIndex; + delete props.tabbable; delete props.type; return props; @@ -69,7 +88,9 @@ class Menu extends React.Component { getItemProps(index) { return { className: this.getItemClass(index), - onClick: this.handleItemClick.bind(this, index), + onClick: this.onItemClick.bind(this, index), + tabIndex: (this.props.tabbable) ? '0' : null, + onKeyDown: this.onKeyDown.bind(this, index), key: index }; } @@ -83,7 +104,16 @@ class Menu extends React.Component { return classNames(classes); } - handleItemClick(index) { + onKeyDown(index, event) { + let enterKey = keyCode('ENTER'); + let spaceKey = keyCode('SPACE'); + + if(event.keyCode === enterKey || event.keyCode === spaceKey) { + this.onItemClick(index); + } + } + + onItemClick(index) { if (this.props.onItemClick) { this.props.onItemClick(index); } diff --git a/client/src/core-components/menu.scss b/client/src/core-components/menu.scss index a0beb93f..380a571f 100644 --- a/client/src/core-components/menu.scss +++ b/client/src/core-components/menu.scss @@ -1,12 +1,15 @@ @import "../scss/vars"; .menu { - background-color: white; - color: $dark-grey; - margin: 0; - padding: 0; - list-style-type: none; - cursor: pointer; + + &__list { + background-color: white; + color: $dark-grey; + margin: 0; + padding: 0; + list-style-type: none; + cursor: pointer; + } &__list-item { padding: 8px; @@ -28,5 +31,14 @@ .menu__list-item:hover { background-color: $secondary-blue; } + + .menu__header { + padding: 8px; + background-color: $primary-blue; + color: white; + font-size: 16px; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + } } } \ No newline at end of file diff --git a/client/src/core-components/table.js b/client/src/core-components/table.js new file mode 100644 index 00000000..806d1830 --- /dev/null +++ b/client/src/core-components/table.js @@ -0,0 +1,76 @@ +import React from 'react'; +import classNames from 'classnames'; + +class Table extends React.Component { + static propTypes = { + headers: React.PropTypes.arrayOf(React.PropTypes.shape({ + key: React.PropTypes.string, + value: React.PropTypes.string, + className: React.PropTypes.string + })), + rows: React.PropTypes.arrayOf(React.PropTypes.object), + type: React.PropTypes.oneOf(['default']) + }; + + static defaultProps = { + type: 'default' + }; + + render() { + return ( +
+ + + {this.props.headers.map(this.renderHeaderColumn.bind(this))} + + + + {this.props.rows.map(this.renderRow.bind(this))} + +
+ ); + } + + renderHeaderColumn(header) { + let classes = { + 'table__header-column': true, + [header.className]: (header.className) + }; + + return ( + {header.value} + ); + } + + renderRow(row, index) { + let headersKeys = this.props.headers.map(header => header.key); + + return ( + + {headersKeys.map(this.renderCell.bind(this, row))} + + ); + } + + renderCell(row, key, index) { + let classes = { + 'table__cell': true, + [this.props.headers[index].className]: (this.props.headers[index].className) + }; + + return ( + {row[key]} + ); + } + + getRowClass(row) { + let classes = { + 'table__row': true, + 'table__row-highlighted': row.highlighted + }; + + return classNames(classes); + } +} + +export default Table; \ No newline at end of file diff --git a/client/src/core-components/table.scss b/client/src/core-components/table.scss new file mode 100644 index 00000000..2055d5dd --- /dev/null +++ b/client/src/core-components/table.scss @@ -0,0 +1,46 @@ +@import "../scss/vars"; + +.table { + + &__header { + background-color: $primary-blue; + color: white; + font-weight: normal; + } + + &__header-column { + font-weight: normal; + + &:first-child { + border-top-left-radius: 4px; + } + + &:last-child { + border-top-right-radius: 4px + } + } + + &__row { + border: 0; + color: #B8B8B8; + + &:nth-child(even) { + background-color: #F9F9F9; + } + + &:nth-child(odd) { + background-color: #F1F1F1; + } + + &-highlighted { + color: $secondary-blue; + font-weight: bold; + background-color: white !important; + } + } + + &__cell00 { + border: 0; + padding: 10px; + } +} \ No newline at end of file diff --git a/client/src/data/fixtures/user-fixtures.js b/client/src/data/fixtures/user-fixtures.js index d84f061a..e661d46c 100644 --- a/client/src/data/fixtures/user-fixtures.js +++ b/client/src/data/fixtures/user-fixtures.js @@ -103,5 +103,107 @@ module.exports = [ }; } } + }, + { + path: '/user/get', + time: 100, + response: function () { + return { + status: 'success', + data: { + name: 'Haskell Curry', + email: 'haskell@lambda.com', + tickets: [ + { + ticketNumber: '445441', + title: 'Problem with installation', + content: 'I had a problem with the installation of the php server', + department: { + id: 2, + name: 'Environment Setup' + }, + date: '15 Apr 2016', + file: 'http://www.opensupports.com/some_file.zip', + language: 'en', + unread: true, + closed: false, + author: { + id: 12, + name: 'Haskell Curry', + email: 'haskell@lambda.com' + }, + owner: { + id: 15, + name: 'Steve Jobs', + email: 'steve@jobs.com' + }, + comments: [ + { + content: 'Do you have apache installed? It generally happens if you dont have apache.', + author: { + id: 15, + name: 'Steve Jobs', + email: 'jobs@steve.com', + staff: true + }, + date: '12 Dec 2016', + file: '' + }, + { + content: 'I have already installed apache, but the problem persists', + author: { + id: 12, + name: 'Haskell Curry', + steve: 'haskell@lambda.com', + staff: false + }, + date: '12 Dec 2016', + file: '' + } + ] + }, + { + ticketNumber: '878552', + title: 'Lorem ipsum door', + content: 'I had a problem with the installation of the php server', + department: { + id: 2, + name: 'Environment Setup' + }, + date: '15 Apr 2016', + file: 'http://www.opensupports.com/some_file.zip', + language: 'en', + unread: false, + closed: false, + author: { + name: 'Haskell Curry', + email: 'haskell@lambda.com' + }, + owner: { + name: 'Steve Jobs' + }, + comments: [ + { + content: 'Do you have apache installed? It generally happens if you dont have apache.', + author: { + name: 'Steve Jobs', + email: 'jobs@steve.com', + staff: true + } + }, + { + content: 'I have already installed apache, but the problem persists', + author: { + name: 'Haskell Curry', + steve: 'haskell@lambda.com', + staff: false + } + } + ] + } + ] + } + }; + } } -]; +]; \ No newline at end of file diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index 6a56b4ac..acd007f8 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -1,4 +1,5 @@ export default { + 'WELCOME': 'Welcome', 'SUBMIT': 'Submit', 'LOG_IN': 'Log in', 'SIGN_UP': 'Sign up', @@ -8,6 +9,11 @@ export default { 'NEW_PASSWORD': 'New password', 'REPEAT_NEW_PASSWORD': 'Repeat new password', 'BACK_LOGIN_FORM': 'Back to login form', + 'TICKET_LIST': 'Ticket List', + 'CREATE_TICKET': 'Create Ticket', + 'VIEW_ARTICLES': 'View Articles', + 'EDIT_PROFILE': 'Edit Profile', + 'CLOSE_SESSION': 'Close session', //ERRORS 'EMAIL_NOT_EXIST': 'Email does not exist', diff --git a/client/src/app/index.js b/client/src/index.js similarity index 92% rename from client/src/app/index.js rename to client/src/index.js index 5d686499..7d53e718 100644 --- a/client/src/app/index.js +++ b/client/src/index.js @@ -4,8 +4,8 @@ import { Provider } from 'react-redux'; import SessionActions from 'actions/session-actions'; import ConfigActions from 'actions/config-actions'; -import routes from './Routes'; -import store from './store'; +import routes from 'app/Routes'; +import store from 'app/store'; if ( process.env.NODE_ENV !== 'production' ) { // Enable React devtools diff --git a/client/src/lib-app/__mocks__/api-call-mock.js b/client/src/lib-app/__mocks__/api-call-mock.js index f7de46f3..3500f061 100644 --- a/client/src/lib-app/__mocks__/api-call-mock.js +++ b/client/src/lib-app/__mocks__/api-call-mock.js @@ -1,5 +1,8 @@ export default { call: stub().returns(new Promise(function (resolve) { - resolve(); + resolve({ + status: 'success', + data: {} + }); })) }; \ No newline at end of file diff --git a/client/src/lib-app/session-store.js b/client/src/lib-app/session-store.js index a243c9fd..fa635f4e 100644 --- a/client/src/lib-app/session-store.js +++ b/client/src/lib-app/session-store.js @@ -29,6 +29,17 @@ class SessionStore { closeSession() { this.removeItem('userId'); this.removeItem('token'); + + this.clearRememberData(); + this.clearUserData(); + } + + storeUserData(data) { + this.setItem('userData', JSON.stringify(data)); + } + + getUserData() { + return JSON.parse(this.getItem('userData')); } storeRememberData({token, userId, expiration}) { @@ -73,6 +84,10 @@ class SessionStore { this.removeItem('rememberData-expiration'); } + clearUserData() { + this.removeItem('userData'); + } + getItem(key) { return this.storage.getItem(key); } diff --git a/client/src/reducers/session-reducer.js b/client/src/reducers/session-reducer.js index 42dbe9e2..877bb590 100644 --- a/client/src/reducers/session-reducer.js +++ b/client/src/reducers/session-reducer.js @@ -19,8 +19,9 @@ class SessionReducer extends Reducer { 'LOGIN_FULFILLED': this.onLoginCompleted.bind(this), 'LOGIN_REJECTED': this.onLoginFailed, 'LOGOUT_FULFILLED': this.onLogout, + 'USER_DATA_FULFILLED': this.onUserDataRetrieved, 'CHECK_SESSION_REJECTED': (state) => { return _.extend({}, state, {initDone: true})}, - 'SESSION_CHECKED': (state) => { return _.extend({}, state, {initDone: true, logged: true})}, + 'SESSION_CHECKED': this.onSessionChecked, 'LOGIN_AUTO_FULFILLED': this.onAutoLogin.bind(this), 'LOGIN_AUTO_REJECTED': this.onAutoLoginFail }; @@ -54,7 +55,6 @@ class SessionReducer extends Reducer { onLogout(state) { sessionStore.closeSession(); - sessionStore.clearRememberData(); return _.extend({}, state, { initDone: true, @@ -77,7 +77,6 @@ class SessionReducer extends Reducer { onAutoLoginFail(state) { sessionStore.closeSession(); - sessionStore.clearRememberData(); return _.extend({}, state, { initDone: true @@ -95,6 +94,30 @@ class SessionReducer extends Reducer { sessionStore.createSession(resultData.userId, resultData.token); } + + onUserDataRetrieved(state, payload) { + let userData = payload.data; + + sessionStore.storeUserData(payload.data); + + return _.extend({}, state, { + userName: userData.name, + userEmail: userData.email, + userTickets: userData.tickets + }); + } + + onSessionChecked(state) { + let userData = sessionStore.getUserData(); + + return _.extend({}, state, { + initDone: true, + logged: true, + userName: userData.name, + userEmail: userData.email, + userTickets: userData.tickets + }); + } } export default SessionReducer.getInstance(); \ No newline at end of file diff --git a/client/src/scss/_reset.scss b/client/src/scss/_reset.scss index 5abecd75..95e2b640 100644 --- a/client/src/scss/_reset.scss +++ b/client/src/scss/_reset.scss @@ -1,4 +1,53 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +/*! + * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=95955b93b458c15deb17e935e7374a2f) + * Config saved to config.json and https://gist.github.com/95955b93b458c15deb17e935e7374a2f + */ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} [hidden], template { display: none; @@ -148,6 +197,78 @@ td, th { padding: 0; } +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + background: transparent !important; + color: #000 !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + text-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -188,7 +309,6 @@ a:focus { text-decoration: underline; } a:focus { - outline: thin dotted; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } @@ -250,6 +370,416 @@ hr { [role="button"] { cursor: pointer; } +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777777; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 18px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +mark, +.mark { + background-color: #fcf8e3; + padding: .2em; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #777777; +} +.text-primary { + color: #337ab7; +} +a.text-primary:hover, +a.text-primary:focus { + color: #286090; +} +.text-success { + color: #3c763d; +} +a.text-success:hover, +a.text-success:focus { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:hover, +a.text-info:focus { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:hover, +a.text-warning:focus { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:hover, +a.text-danger:focus { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #337ab7; +} +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #286090; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:hover, +a.bg-success:focus { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:hover, +a.bg-info:focus { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eeeeee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + list-style: none; + margin-left: -5px; +} +.list-inline > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} .container { margin-right: auto; margin-left: auto; @@ -917,8 +1447,250 @@ hr { margin-left: 0%; } } +table { + background-color: transparent; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777777; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + //border-top: 1px solid #dddddd; +} +.table > thead > tr > th { + vertical-align: bottom; + //border-bottom: 2px solid #dddddd; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + //border-top: 2px solid #dddddd; +} +.table .table { + background-color: #ffffff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + //border: 1px solid #dddddd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + //border: 1px solid #dddddd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + //border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; +} +.table-responsive { + overflow-x: auto; + min-height: 0.01%; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + //border: 1px solid #dddddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} .clearfix:before, .clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, .container:before, .container:after, .container-fluid:before, @@ -929,6 +1701,7 @@ hr { display: table; } .clearfix:after, +.dl-horizontal dd:after, .container:after, .container-fluid:after, .row:after {