diff --git a/README.md b/README.md index 6389d32..3c961f9 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ * node v6 (https://nodejs.org) ## Quick Start +* copy `.env.example` to `.env` * `npm install` * `npm run dev` * Navigate browser to `http://localhost:3000` diff --git a/package.json b/package.json index fff3a9a..cbdb376 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "react-simple-dropdown": "^1.1.5", "react-slick": "^0.14.5", "react-star-rating-component": "^1.2.2", + "react-table": "^3.1.4", "react-tabs": "^0.8.2", "react-timeago": "^3.1.3", "reactable": "^0.14.1", diff --git a/src/components/BreadcrumbItem/BreadcrumbItem.jsx b/src/components/BreadcrumbItem/BreadcrumbItem.jsx new file mode 100644 index 0000000..b036fc6 --- /dev/null +++ b/src/components/BreadcrumbItem/BreadcrumbItem.jsx @@ -0,0 +1,15 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import styles from './BreadcrumbItem.scss'; + +export const BreadcrumbItem = ({title}) => ( + + {title} + +); + +BreadcrumbItem.propTypes = { + title: PropTypes.string.isRequired, +}; + +export default CSSModules(BreadcrumbItem, styles); diff --git a/src/components/BreadcrumbItem/BreadcrumbItem.scss b/src/components/BreadcrumbItem/BreadcrumbItem.scss new file mode 100644 index 0000000..a5e8856 --- /dev/null +++ b/src/components/BreadcrumbItem/BreadcrumbItem.scss @@ -0,0 +1,7 @@ +.breadcrumb-item { + background-color: transparent; + + :global { + + } +} diff --git a/src/components/BreadcrumbItem/index.js b/src/components/BreadcrumbItem/index.js new file mode 100644 index 0000000..28647ff --- /dev/null +++ b/src/components/BreadcrumbItem/index.js @@ -0,0 +1,3 @@ +import BreadcrumbItem from './BreadcrumbItem'; + +export default BreadcrumbItem; diff --git a/src/components/Button/Button.jsx b/src/components/Button/Button.jsx index ddba5e8..761b19d 100644 --- a/src/components/Button/Button.jsx +++ b/src/components/Button/Button.jsx @@ -19,6 +19,7 @@ Button.propTypes = { Button.defaultProps = { type: 'button', size: 'normal', + color: 'blue', }; export default CSSModules(Button, styles, {allowMultiple: true}); diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 55e9742..5867f37 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -71,6 +71,14 @@ export function Header({ ); + } else if (user.role === 'pilot') { + res = ( +
  • + +
  • + ); } return res; })() diff --git a/src/components/Radiobox/Radiobox.jsx b/src/components/Radiobox/Radiobox.jsx new file mode 100644 index 0000000..85b1066 --- /dev/null +++ b/src/components/Radiobox/Radiobox.jsx @@ -0,0 +1,36 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import styles from './Radiobox.scss'; + +const Radiobox = ({children, className, radioValue, name, value, onChange, disabled}) => ( +
    + + +
    +); + +Radiobox.propTypes = { + children: PropTypes.string, + className: PropTypes.string, + radioValue: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.string, + onChange: PropTypes.func, + disabled: PropTypes.bool, +}; + +Radiobox.defaultProps = { + disabled: false, +}; + +export default CSSModules(Radiobox, styles); diff --git a/src/components/Radiobox/Radiobox.scss b/src/components/Radiobox/Radiobox.scss new file mode 100644 index 0000000..d849723 --- /dev/null +++ b/src/components/Radiobox/Radiobox.scss @@ -0,0 +1,46 @@ +.radiobox { + height: 40px; + display: flex; + align-items: center; + + input[type="radio"] { + display: none; + } + + input[type="radio"] + label span { + flex-shrink: 0; + display: inline-block; + width: 23px; + height: 23px; + border: 1px solid #a1a1a1; + box-shadow: none; + appearance: none; + margin: 0 9px 0 0; + background-color: transparent; + vertical-align: middle; + cursor: pointer; + } + + input[type="radio"]:checked + label span { + background: url('icon-checkbox.png') no-repeat 50% 50%; + } + + label { + font-weight: normal; + cursor: pointer; + line-height: 1; + display: flex; + align-items: center; + margin-bottom: 0; + } + + input[type="radio"][disabled] + label { + cursor: default; + } + + input[type="radio"][disabled] + label span { + cursor: default; + background-color: #efefef; + border-color: #ebebeb; + } +} diff --git a/src/components/Radiobox/index.js b/src/components/Radiobox/index.js new file mode 100644 index 0000000..223571c --- /dev/null +++ b/src/components/Radiobox/index.js @@ -0,0 +1,3 @@ +import Radiobox from './Radiobox'; + +export default Radiobox; diff --git a/src/components/StatusLabel/StatusLabel.jsx b/src/components/StatusLabel/StatusLabel.jsx index 08a11cc..6e13840 100644 --- a/src/components/StatusLabel/StatusLabel.jsx +++ b/src/components/StatusLabel/StatusLabel.jsx @@ -1,21 +1,25 @@ import React, {PropTypes} from 'react'; import CSSModules from 'react-css-modules'; import styles from './StatusLabel.scss'; +import _ from 'lodash'; const statusLabels = { - inProgress: 'In Progress', + 'in-progress': 'In Progress', // new style + inProgress: 'In Progress', // old style should be removed when all code is binded to backend cancelled: 'Cancelled', completed: 'Completed', + waiting: 'Waiting', + scheduled: 'Scheduled', }; export const StatusLabel = ({value}) => ( - + {statusLabels[value]} ); StatusLabel.propTypes = { - value: PropTypes.oneOf(['inProgress', 'cancelled', 'completed']).isRequired, + value: PropTypes.oneOf(_.keys(statusLabels)).isRequired, }; export default CSSModules(StatusLabel, styles); diff --git a/src/components/StatusLabel/StatusLabel.scss b/src/components/StatusLabel/StatusLabel.scss index 11325b2..01b2730 100644 --- a/src/components/StatusLabel/StatusLabel.scss +++ b/src/components/StatusLabel/StatusLabel.scss @@ -33,3 +33,17 @@ @extend .status-label; } + +.status-label_waiting { + background-color: #e3e3e3; + background-image: url('icon-status-inprogress.png'); + + @extend .status-label; +} + +.status-label_scheduled { + background-color: #4c4c4c; + background-image: url('icon-status-inprogress.png'); + + @extend .status-label; +} diff --git a/src/components/Table/Table.jsx b/src/components/Table/Table.jsx new file mode 100644 index 0000000..87bc89d --- /dev/null +++ b/src/components/Table/Table.jsx @@ -0,0 +1,140 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import ReactTable from 'react-table'; +import styles from './Table.scss'; +import SelectPerPage from 'components/SelectPerPage'; +import Pagination from 'components/Pagination'; +import _ from 'lodash'; + +/** + * Populate column objects with id in class name + * this way we can pass id to the on click handler inside ThComponent + * @param {Array} columns original columns + * @return {Array} columns with id + */ +const prepareColumns = (columns) => ( + _.map(columns, (column) => ( + {...column, headerClassName: `-column-id-${column.accessor}`} + )) +); + +/** + * Convert sorting parameter from backend format to ReactTable format + * @param {String} sortBy in backend format + * @return {String} in ReactTable format + */ +const prepareSorting = (sortBy) => { + const sorting = []; + + sortBy && sorting.push({ + id: sortBy.replace(/^-/, ''), + asc: sortBy[0] !== '-', + }); + + return sorting; +}; + +/* + Table header cell component + use custom component to implement server-side sorting + */ +const ThComponent = (props) => { + const {className, onChange} = props; + + return ( + { + const matchSortable = className.match(/(?:^| )-cursor-pointer(?: |$)/); + if (matchSortable) { + const matchColumnId = className.match(/(?:^| )-column-id-([^\s]+)(?: |$)/); + const matchSortingDir = className.match(/(?:^| )-sort-([^\s]+)(?: |$)/); + if (matchColumnId) { + let sortDir; + // if sorting direction is set and it's 'desc' we change it to 'asc' + if (matchSortingDir && matchSortingDir[1] === 'desc') { + sortDir = ''; + // if sorting direction is not set, then we set to 'asc' by default + } else if (!matchSortingDir) { + sortDir = ''; + // in this case sort direction was set to 'asc', so we change it to 'desc' + } else { + sortDir = '-'; + } + onChange({sortBy: sortDir + matchColumnId[1]}); + } + } + }} + > + {props.children} + + ); +}; + +ThComponent.propTypes = { + className: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + children: PropTypes.any, +}; + +export const Table = ({columns, offset, limit, total, sortBy, onChange, ...props}) => ( +
    +
    + } + columns={prepareColumns(columns)} + {...props} + /> +
    + +
    +
    + { + // adjust page number (offset) when change per page quantity (limit) + const newOffset = Math.floor(offset / value); + onChange({limit: value, offset: newOffset}); + }} + /> +
    +
    + { + onChange({offset: Math.ceil(selected * limit)}); + }} + /> +
    +
    + +
    +); + +Table.propTypes = { + columns: PropTypes.array.isRequired, + offset: PropTypes.number.isRequired, + limit: PropTypes.number.isRequired, + total: PropTypes.number.isRequired, + sortBy: PropTypes.string, + onChange: PropTypes.func.isRequired, +}; + +export default CSSModules(Table, styles); diff --git a/src/components/Table/Table.scss b/src/components/Table/Table.scss new file mode 100644 index 0000000..32fe60a --- /dev/null +++ b/src/components/Table/Table.scss @@ -0,0 +1,101 @@ +.smart-table { + background-color: transparent; +} + +.table-wrap { + :global { + .-loading { + display: none; + } + + .-padRow { + display: none; + } + } +} + +.table { + width: 100%; +} + +.thead { + background-color: #1e526c; + + th { + color: #fff; + font-size: 14px; + font-weight: 400; + padding: 14px 27px 16px; + text-align: left; + } + + :global { + th.-cursor-pointer { + > div { + cursor: pointer; + + &:after { + background: url('../../styles/img/icon-sort-desc.png') no-repeat; + content: ''; + display: inline-block; + opacity: 0.5; + height: 7px; + margin-left: 8px; + transform: rotate(180deg); + width: 11px; + } + } + } + + th.-sort-asc, + th.-sort-desc { + > div { + &:after { + opacity: 1; + } + } + } + + th.-sort-desc { + > div { + &:after { + transform: none; + } + } + } + } +} + +.tbody { + td { + font-size: 14px; + padding: 12px 23px; + white-space: nowrap; + + > a { + color: #3b73b9; + } + } +} + +.tr { + border-bottom: 1px solid #e7e8ea; +} + +.navigation { + margin: 25px 20px; +} + +.navigation:after { + clear: both; + content: ''; + display: table; +} + +.pagination { + float: right; +} + +.perpage { + float: left; +} diff --git a/src/components/Table/index.js b/src/components/Table/index.js new file mode 100644 index 0000000..de4c7d5 --- /dev/null +++ b/src/components/Table/index.js @@ -0,0 +1,3 @@ +import Table from './Table'; + +export default Table; diff --git a/src/components/TextareaField/TextareaField.jsx b/src/components/TextareaField/TextareaField.jsx index 1d42974..9367b63 100644 --- a/src/components/TextareaField/TextareaField.jsx +++ b/src/components/TextareaField/TextareaField.jsx @@ -1,16 +1,24 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import CSSModules from 'react-css-modules'; import _ from 'lodash'; +import cn from 'classnames'; import styles from './TextareaField.scss'; -export const TextareaField = (props) => ( -
    -