diff --git a/.env b/.env new file mode 100644 index 0000000..21685c1 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +GOOGLE_API_KEY=AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI +REACT_APP_API_BASE_PATH=http://localhost:3500 +REACT_APP_AUTH0_CLIENT_ID=3CGKzjS2nVSqHxHHE64RhvvKY6e0TYpK +REACT_APP_AUTH0_CLIENT_DOMAIN=dronetest.auth0.com +REACT_APP_SOCKET_URL=http://localhost:3500 diff --git a/.env.example b/.env.example index 4a42b68..3830cb2 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -REACT_APP_API_BASE_PATH=https://kb-dsp-server.herokuapp.com -REACT_APP_SOCKET_URL=https://kb-dsp-server.herokuapp.com -REACT_APP_AUTH0_CLIEND_ID=3CGKzjS2nVSqHxHHE64RhvvKY6e0TYpK -REACT_APP_AUTH0_DOMAIN=dronetest.auth0.com -REACT_APP_GOOGLE_API_KEY=AIzaSyCR3jfBdv9prCBYBOf-fPUDhjPP4K05YjE +GOOGLE_API_KEY=AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI +REACT_APP_API_BASE_PATH=http://localhost:3500 +REACT_APP_AUTH0_CLIENT_ID=3CGKzjS2nVSqHxHHE64RhvvKY6e0TYpK +REACT_APP_AUTH0_CLIENT_DOMAIN=dronetest.auth0.com +REACT_APP_SOCKET_URL=http://localhost:3500 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 20c36db..e043f42 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ node_modules dist coverage .tmp -/.env diff --git a/README.md b/README.md index f8cfdec..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` @@ -24,6 +25,24 @@ See Guild https://github.com/lorenwest/node-config/wiki/Configuration-Files |`REACT_APP_AUTH0_DOMAIN`| The React app auth0 domain`| Environment variables will be loaded from the .env file during build. Create the .env file based on the provided env.example +### Auth0 setup +- Create an account on auth0. +- Click on clients in left side menu, it will redirect you to client page. Click on CREATE CLIENT button + to create a new client. +- Copy the client id and client domain and export them as environment variables. +- Add `http://localhost:3000` as Allowed callback url's in client settings. + +### Add social connections + +### Facebook social connection +- To add facebook social connection to auth0, you have to create a facebook app. + Go to facebook [developers](https://developers.facebook.com/apps) and create a new app. +- Copy the app secret and app id to auth0 social connections facebook tab. +- You have to setup the oauth2 callback in app oauth settings. +- For more information visit auth0 [docs](https://auth0.com/docs/connections/social/facebook) + +### Google social connection +- For more information on how to connect google oauth2 client, visit official [docs](https://auth0.com/docs/connections/social/google) ## Install dependencies `npm i` diff --git a/config/default.js b/config/default.js index f1bbbf3..d35b608 100644 --- a/config/default.js +++ b/config/default.js @@ -1,6 +1,6 @@ /* eslint-disable import/no-commonjs */ /** - * Main config file + * Main config file for the server which is hosting the reat app */ module.exports = { // below env variables are NOT visible in frontend diff --git a/package.json b/package.json index 7ae293e..cbdb376 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "rc-slider": "^5.4.0", "rc-tooltip": "^3.4.2", "react": "^15.3.2", + "react-addons-create-fragment": "^15.3.2", "react-breadcrumbs": "^1.5.1", "react-click-outside": "^2.2.0", "react-count-down": "^1.0.3", @@ -80,6 +81,7 @@ "react-icheck": "^0.3.6", "react-input-range": "^0.9.3", "react-modal": "^1.5.2", + "react-paginate": "^4.1.0", "react-portal": "^3.0.0", "react-redux": "^4.0.0", "react-redux-toastr": "^4.2.2", @@ -89,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/api/User.js b/src/api/User.js index 70c1728..53466dc 100644 --- a/src/api/User.js +++ b/src/api/User.js @@ -24,7 +24,6 @@ class UserApi { login(email, password) { const url = `${this.basePath}/api/v1/login`; - return reqwest({ url, method: 'post', @@ -40,7 +39,7 @@ class UserApi { }); } - register(name, email, password) { + register(firstName, lastName, email, password) { const url = `${this.basePath}/api/v1/register`; return reqwest({ url, @@ -48,15 +47,15 @@ class UserApi { type: 'json', contentType: 'application/json', data: JSON.stringify({ - firstName: name, - lastName: name, + firstName, + lastName, email, phone: '1', password, })}); } - registerSocialUser(name, email) { + registerSocialUser(name, email, token) { const url = `${this.basePath}/api/v1/login/social`; return reqwest({ @@ -64,6 +63,9 @@ class UserApi { method: 'post', type: 'json', contentType: 'application/json', + headers: { + Authorization: `Bearer ${token}`, + }, data: JSON.stringify({ name, email, diff --git a/src/components/AdminHeader/AdminHeader.jsx b/src/components/AdminHeader/AdminHeader.jsx index a822dd2..1c6bc82 100644 --- a/src/components/AdminHeader/AdminHeader.jsx +++ b/src/components/AdminHeader/AdminHeader.jsx @@ -2,6 +2,7 @@ import React from 'react'; import CSSModules from 'react-css-modules'; import {Link} from 'react-router'; import styles from './AdminHeader.scss'; +import Dropdown from '../Dropdown'; export const AdminHeader = () => ( ); diff --git a/src/components/AdminHeader/AdminHeader.scss b/src/components/AdminHeader/AdminHeader.scss index dcabbf5..ca74fa9 100644 --- a/src/components/AdminHeader/AdminHeader.scss +++ b/src/components/AdminHeader/AdminHeader.scss @@ -10,4 +10,8 @@ composes: pages from '../Header/Header.scss' } +.notifications { + composes: notifications from '../Header/Header.scss' +} + 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/Button/Button.scss b/src/components/Button/Button.scss index 48494a9..912970f 100644 --- a/src/components/Button/Button.scss +++ b/src/components/Button/Button.scss @@ -41,3 +41,7 @@ .color-silver { background: #67879a; } + +.color-red { + background: #f00; +} diff --git a/src/components/Dropdown/Dropdown.jsx b/src/components/Dropdown/Dropdown.jsx index d0a020b..5412451 100644 --- a/src/components/Dropdown/Dropdown.jsx +++ b/src/components/Dropdown/Dropdown.jsx @@ -3,18 +3,17 @@ import CSSModules from 'react-css-modules'; import ReactDropdown, {DropdownTrigger, DropdownContent} from 'react-simple-dropdown'; import styles from './Dropdown.scss'; -export const Dropdown = ({title, children}) => ( -
- - {title} - - {children} - - -
+export const Dropdown = ({onRef, title, children}) => ( + + {title} + + {children} + + ); Dropdown.propTypes = { + onRef: PropTypes.func, title: PropTypes.any.isRequired, children: PropTypes.any.isRequired, }; diff --git a/src/components/Dropdown/Dropdown.scss b/src/components/Dropdown/Dropdown.scss index 9acd9d2..61de447 100644 --- a/src/components/Dropdown/Dropdown.scss +++ b/src/components/Dropdown/Dropdown.scss @@ -1,18 +1,12 @@ -.dropdown { - :global { - .dropdown { - display: inline-block; - } - - - .dropdown--active .dropdown__content { - display: block; - } +:global { + .dropdown { + display: inline-block; + } + .dropdown--active .dropdown__content { + display: block; } } - - .content { display: none; position: absolute; diff --git a/src/components/FileField/FileField.jsx b/src/components/FileField/FileField.jsx new file mode 100644 index 0000000..50f442c --- /dev/null +++ b/src/components/FileField/FileField.jsx @@ -0,0 +1,47 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import _ from 'lodash'; +import styles from './FileField.scss'; + +/** + * Gets filename to display, no metter what was supplied: string, FileList object or an Object with numeral keys + * @param {Mixed} value source to get filename + * @return {String} filename to display + */ +const getFileName = (value) => { + let newValue = value; + + if (_.isUndefined(newValue)) { + newValue = ''; + } else if (value[0] && _.isString(value[0].name)) { + newValue = value[0].name; + } + + return newValue; +}; + +export const FileField = (props) => ( +
+
+ +
+); + +FileField.propTypes = { + size: PropTypes.oneOf(['normal', 'narrow']), + label: PropTypes.string, + accept: PropTypes.string, + value: PropTypes.any, + initialValue: PropTypes.any, + onChange: PropTypes.func, +}; + +FileField.defaultProps = { + size: 'normal', +}; + +export default CSSModules(FileField, styles); diff --git a/src/components/FileField/FileField.scss b/src/components/FileField/FileField.scss new file mode 100644 index 0000000..be83b31 --- /dev/null +++ b/src/components/FileField/FileField.scss @@ -0,0 +1,48 @@ +.file-field { + display: flex; + width: 100%; + + input[type="text"] { + width: 100%; + padding: 0 10px; + background: white; + color: black; + border: none; + height: 36px; + line-height: 36px; + } + + .text { + border: 1px solid #ebebeb; + flex: 1; + } + + label.button { + background: #315b95; + color: #fff; + display: block; + border: none; + height: 36px; + flex: 0 0 115px; + font-weight: bold; + line-height: 36px; + margin-left: 12px; + overflow: hidden; + position: relative; + text-align: center; + + input[type="file"] { + opacity: 0; + position: absolute; + } + } +} + +.file-field_narrow { + @extend .file-field; + + input[type="text"] { + height: 34px; + line-height: 32px; + } +} diff --git a/src/components/FileField/index.js b/src/components/FileField/index.js new file mode 100644 index 0000000..d88c0a3 --- /dev/null +++ b/src/components/FileField/index.js @@ -0,0 +1,3 @@ +import FileField from './FileField'; + +export default FileField; diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 5467cd4..5867f37 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -8,64 +8,94 @@ import Dropdown from '../Dropdown'; import Notification from '../Notification'; import styles from './Header.scss'; -export const Header = ({ +/** + * TODO: This component cries: 'REFACTOR ME!' + * Seriously, it is such a mess now, should be split into separate sub-components! + */ + +export function Header({ location, selectedCategory, categories, user, notifications, - routes, handleNotification, toggleNotif, loggedUser, -}) => ( + handleNotification, logoutAction, toggleNotif, loggedUser, +}) { + // Holds a reference to the function which hides the user dropdown (Profile, + // Logout, etc.). + let hideUserDropdown; - -); + ] + ); + } else { + res = ( + [ + (
  • handleNotification(!toggleNotif)}> + {notifications.length > 0 && {notifications.length}} + {toggleNotif && } +
  • ), + (
  • + { + if (dropdown) { + hideUserDropdown = dropdown.hide; + } + }} + title={Welcome,
    {user.name}
    } + > + +
    +
  • ), + ] + ); + } + return res; + })() + } + + + ); +} Header.propTypes = { - routes: PropTypes.any.isRequired, + // routes: PropTypes.any.isRequired, location: PropTypes.string.isRequired, selectedCategory: PropTypes.string.isRequired, categories: PropTypes.array.isRequired, notifications: PropTypes.array.isRequired, user: PropTypes.object.isRequired, handleNotification: PropTypes.func, + logoutAction: PropTypes.func.isRequired, toggleNotif: PropTypes.bool, loggedUser: PropTypes.bool, }; diff --git a/src/components/MapHistory/MapHistory.scss b/src/components/MapHistory/MapHistory.scss index 317e61e..dfffff0 100644 --- a/src/components/MapHistory/MapHistory.scss +++ b/src/components/MapHistory/MapHistory.scss @@ -24,7 +24,7 @@ left:0; right:0; margin:0 auto; - width:520px; + width:85%; height: 80px; background-color: #FFF; .slider{ diff --git a/src/components/ModalConfirm/ModalConfirm.jsx b/src/components/ModalConfirm/ModalConfirm.jsx new file mode 100644 index 0000000..e00dc06 --- /dev/null +++ b/src/components/ModalConfirm/ModalConfirm.jsx @@ -0,0 +1,72 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import Button from 'components/Button'; +import styles from './ModalConfirm.scss'; +import Modal from 'react-modal'; + + +/* +* customStyles +*/ + +const customStyles = { + overlay: { + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(9, 9, 9, 0.58)', + }, + content: { + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + marginRight: '-50%', + transform: 'translate(-50%, -50%)', + padding: '0px', + width: '633px', + }, +}; + + +/* +* ModalConfirm +*/ + + +const ModalConfirm = ({isOpen, onClose, onConfirm, title, message}) => ( + +
    +
    {title}
    +
    +
    +

    {message}

    +
    + + +
    + +); + +ModalConfirm.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, +}; + +export default CSSModules(ModalConfirm, styles); diff --git a/src/components/ModalConfirm/ModalConfirm.scss b/src/components/ModalConfirm/ModalConfirm.scss new file mode 100644 index 0000000..adcee07 --- /dev/null +++ b/src/components/ModalConfirm/ModalConfirm.scss @@ -0,0 +1,47 @@ +.modal-header { + display: flex; + height: 23px; + background: #f0f0f1; + height: 63px; + align-items: center; + padding: 5px 20px; +} + +.title { + font-size: 24px; + color: #0d0d0d; + align-self: center; + font-weight: bold; +} + +.icon-close-modal { + display: block; + width: 24px; + height: 24px; + background: url('https://github.com/icon-close-modal.png') no-repeat; + margin-left: auto; + cursor: pointer; +} + +.modal-msg { + font-size: 14px; + color: #131313; + text-align: center; + padding: 28px; +} + +.actions { + display: flex; + justify-content: center; + margin-bottom: 30px; + + .btnCancel { + padding: 14px 8px; + margin-right: 6px; + } + + .btnConfirm { + padding: 5px 8px; + margin-left: 6px; + } +} diff --git a/src/components/ModalConfirm/index.js b/src/components/ModalConfirm/index.js new file mode 100644 index 0000000..79c8d6d --- /dev/null +++ b/src/components/ModalConfirm/index.js @@ -0,0 +1,3 @@ +import ModalConfirm from './ModalConfirm'; + +export default ModalConfirm; diff --git a/src/components/Pagination/Pagination.jsx b/src/components/Pagination/Pagination.jsx index 52b333c..182d659 100644 --- a/src/components/Pagination/Pagination.jsx +++ b/src/components/Pagination/Pagination.jsx @@ -1,43 +1,34 @@ import React, {PropTypes} from 'react'; import CSSModules from 'react-css-modules'; -import _ from 'lodash'; import styles from './Pagination.scss'; -import Select from '../Select'; +import ReactPaginate from 'react-paginate'; -const pageOptions = [ - {value: 10, label: '10'}, - {value: 30, label: '30'}, - {value: 50, label: '50'}, -]; +export const Pagination = ({forcePage, pageCount, onPageChange}) => { + const props = {...{ + previousLabel: '', + nextLabel: '', + marginPagesDisplayed: 1, + pageRangeDisplayed: 3, + containerClassName: styles.pagination, + pageClassName: styles.page, + activeClassName: styles.page_active, + breakClassName: styles.break, + nextClassName: styles.next, + previousClassName: styles.prev, + disabledClassName: styles.disabled, + }, + forcePage, + pageCount, + onPageChange, + }; - -export const Pagination = ({pages, activePageIndex}) => ( -
    -
    - Show - + +
    +); + +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('https://github.com/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/SelectPerPage/SelectPerPage.jsx b/src/components/SelectPerPage/SelectPerPage.jsx new file mode 100644 index 0000000..bfac446 --- /dev/null +++ b/src/components/SelectPerPage/SelectPerPage.jsx @@ -0,0 +1,32 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import ReactSelect from 'react-select'; +import styles from './SelectPerPage.scss'; + +const options = [ + {value: 10, label: '10'}, + {value: 25, label: '25'}, + {value: 50, label: '50'}, + {value: 100, label: '100'}, +]; + +export const SelectPerPage = ({value, onChange}) => ( +
    + Show + + per page +
    +); + +SelectPerPage.propTypes = { + value: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default CSSModules(SelectPerPage, styles); diff --git a/src/components/SelectPerPage/SelectPerPage.scss b/src/components/SelectPerPage/SelectPerPage.scss new file mode 100644 index 0000000..a0bfa9c --- /dev/null +++ b/src/components/SelectPerPage/SelectPerPage.scss @@ -0,0 +1,86 @@ +.select-per-page { + align-items: center; + color: #282828; + display: flex; + font-size: 12px;; + + :global { + .Select-control { + background-color: #fbfbfb; + border: 2px solid #e3e3e3 !important; + box-shadow: none !important; + border-radius: 5px; + color: #131313; + height: 23px; + width: 51px; + } + + .Select-placeholder, + .Select--single > .Select-control .Select-value { + color: #282828; + font-size: 12px; + line-height: 23px; + padding-left: 0px; + padding-right: 17px; + text-align: center; + + &:after { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + } + } + + .Select-value-label { + display: inline-block; + vertical-align: middle; + } + + .Select-input { + height: 23px; + } + + .Select-arrow-zone { + padding-right: 0; + width: 23px; + } + + .Select-arrow { + background: url("https://github.com/styles/img/icon-select-arrow-small.png") no-repeat; + border: none; + display: block; + height: 5px; + margin-left: 6px; + position: relative; + top: 1px; + width: 8px; + } + + .Select-option { + color: #282828; + font-size: 12px; + line-height: 23px; + padding: 0 10px; + + &.is-selected { + background-color: #f3f5f9; + } + + &.is-focused { + background-color: #315b95; + color: #fff; + } + } + } +} + +.text-before { + display: block; + margin-right: 9px; +} + +.text-after { + display: block; + margin-left: 9px; +} diff --git a/src/components/SelectPerPage/index.js b/src/components/SelectPerPage/index.js new file mode 100644 index 0000000..080e41d --- /dev/null +++ b/src/components/SelectPerPage/index.js @@ -0,0 +1,3 @@ +import SelectPerPage from './SelectPerPage'; + +export default SelectPerPage; 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('https://github.com/icon-status-inprogress.png'); + + @extend .status-label; +} + +.status-label_scheduled { + background-color: #4c4c4c; + background-image: url('https://github.com/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('https://github.com/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/TextField/TextField.scss b/src/components/TextField/TextField.scss index 46a04ab..56f5f52 100644 --- a/src/components/TextField/TextField.scss +++ b/src/components/TextField/TextField.scss @@ -2,6 +2,7 @@ width: 100%; border: 1px solid #ebebeb; + input[type="password"], input[type="text"] { width: 100%; padding: 0 10px; 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) => ( -
    -