Merged in frontend-table-component (pull request #37)

Frontend table component + Implementation
This commit is contained in:
Ivan Diaz 2016-08-18 22:55:13 -03:00
commit cd49405f61
27 changed files with 1336 additions and 79 deletions

View File

@ -8,7 +8,7 @@ gulp.task('browserSync', function() {
browserSync({
proxy: 'localhost:' + config.serverport,
startPath: 'app'
startPath: '/'
});
});

View File

@ -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() {

View File

@ -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",

View File

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

View File

@ -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',

View File

@ -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');
}
}
}

View File

@ -27,8 +27,8 @@ const history = syncHistoryWithStore(browserHistory, store);
export default (
<Router history={history}>
<Route component={App} path='/'>
<Route path='/app' component={MainLayout}>
<Route component={App}>
<Route path='/' component={MainLayout}>
<IndexRoute component={MainHomePage} />
<Route path='signup' component={MainSignUpPage}/>
<Route path='recover-password' component={MainRecoverPasswordPage}/>
@ -40,7 +40,7 @@ export default (
<Route path='edit-profile' component={DashboardEditProfilePage}/>
<Route path='article' component={DashboardArticlePage}/>
<Route path='ticket' component={DashboardTicketPage}/>
<Route path='ticket/:ticketNumber' component={DashboardTicketPage}/>
</Route>
</Route>

View File

@ -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) ? (
<div>
<div><DashboardMenu location={this.props.location} /></div>
<div>{this.props.children}</div>
<div className="dashboard">
<div className="dashboard__menu col-md-3"><DashboardMenu location={this.props.location} /></div>
<Widget className="dashboard__content col-md-9">
{this.props.children}
</Widget>
</div>
) : null;
}

View File

@ -0,0 +1,9 @@
.dashboard {
&__menu {
}
&__content {
}
}

View File

@ -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 (
<div>
DASHBOARD TICKET LIST
<div className="dashboard-ticket-list">
<div className="dashboard-ticket-list__header">Tickets</div>
<Table headers={this.getTableHeaders()} rows={this.getTableRows()} />
</div>
);
}
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: (
<Button className="dashboard-ticket-list__title-link" type="clean" route={{to: '/dashboard/ticket/' + ticket.ticketNumber}}>
{titleText}
</Button>
),
department: ticket.department.name,
date: ticket.date,
highlighted: ticket.unread
};
}
}
export default DashboardListTicketsPage;
export default connect((store) => {
return {
tickets: store.session.userTickets
};
})(DashboardListTicketsPage);

View File

@ -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;
}
}

View File

@ -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' }
];
}
}

View File

@ -34,16 +34,16 @@ class MainLayoutHeader extends React.Component {
if (this.props.session.logged) {
result = (
<div className="main-layout-header--login-links">
Welcome, John
<Button type="clean" onClick={this.logout.bind(this)}>(Close Session)</Button>
<div className="main-layout-header__login-links">
{i18n('WELCOME')},
<span className="main-layout-header__user-name"> {this.props.session.userName}</span>
</div>
);
} else {
result = (
<div className="main-layout-header--login-links">
<Button type="clean" route={{to:'/app'}}>{i18n('LOG_IN')}</Button>
<Button type="clean" route={{to:'/app/signup'}}>Sign up</Button>
<div className="main-layout-header__login-links">
<Button type="clean" route={{to:'/'}}>{i18n('LOG_IN')}</Button>
<Button type="clean" route={{to:'/signup'}}>Sign up</Button>
</div>
);
}
@ -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) => {

View File

@ -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;

View File

@ -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

View File

@ -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)({

View File

@ -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 (
<ul {...this.getProps()}>
{this.props.items.map(this.renderListItem.bind(this))}
</ul>
<div className={this.getClass()}>
{this.renderHeader()}
<ul {...this.getProps()}>
{this.props.items.map(this.renderListItem.bind(this))}
</ul>
</div>
)
}
renderHeader() {
let header = null;
if (this.props.header) {
header = <div className="menu__header">{this.props.header}</div>;
}
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);
}

View File

@ -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;
}
}
}

View File

@ -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 (
<table className="table table-responsive">
<thead>
<tr className="table__header">
{this.props.headers.map(this.renderHeaderColumn.bind(this))}
</tr>
</thead>
<tbody>
{this.props.rows.map(this.renderRow.bind(this))}
</tbody>
</table>
);
}
renderHeaderColumn(header) {
let classes = {
'table__header-column': true,
[header.className]: (header.className)
};
return (
<th className={classNames(classes)} key={header.key}>{header.value}</th>
);
}
renderRow(row, index) {
let headersKeys = this.props.headers.map(header => header.key);
return (
<tr className={this.getRowClass(row)} key={index}>
{headersKeys.map(this.renderCell.bind(this, row))}
</tr>
);
}
renderCell(row, key, index) {
let classes = {
'table__cell': true,
[this.props.headers[index].className]: (this.props.headers[index].className)
};
return (
<td className={classNames(classes)} key={key}>{row[key]}</td>
);
}
getRowClass(row) {
let classes = {
'table__row': true,
'table__row-highlighted': row.highlighted
};
return classNames(classes);
}
}
export default Table;

View File

@ -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;
}
}

View File

@ -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
}
}
]
}
]
}
};
}
}
];
];

View File

@ -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',

View File

@ -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

View File

@ -1,5 +1,8 @@
export default {
call: stub().returns(new Promise(function (resolve) {
resolve();
resolve({
status: 'success',
data: {}
});
}))
};

View File

@ -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);
}

View File

@ -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();

View File

@ -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 {