diff --git a/.env b/.env new file mode 100644 index 0000000..1f5a59b --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +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 +CLOUDINARY_ACCOUNT_NAME=dsp diff --git a/.env.example b/.env.example index 4a42b68..1f5a59b 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ -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 +CLOUDINARY_ACCOUNT_NAME=dsp 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..0b7dfed 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` @@ -18,12 +19,31 @@ See Guild https://github.com/lorenwest/node-config/wiki/Configuration-Files |`PORT`| The port to listen| |`GOOGLE_API_KEY`| The google api key see (https://developers.google.com/maps/documentation/javascript/get-api-key#key)| |`API_BASE_URL`| The base URL for Drone API | -|`REACT_APP_API_BASE_PATH`| The React app api base path`| -|`REACT_APP_SOCKET_URL`| The React app app socket url`| -|`REACT_APP_AUTH0_CLIEND_ID`| The React app auth0 client id`| -|`REACT_APP_AUTH0_DOMAIN`| The React app auth0 domain`| +|`REACT_APP_API_BASE_PATH`| The React app api base path| +|`REACT_APP_SOCKET_URL`| The React app app socket url| +|`REACT_APP_AUTH0_CLIEND_ID`| The React app auth0 client id| +|`REACT_APP_AUTH0_DOMAIN`| The React app auth0 domain| +|`CLOUDINARY_ACCOUNT_NAME`| Your `Cloud name` from https://cloudinary.com/console| 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..9219d8a 100644 --- a/config/default.js +++ b/config/default.js @@ -1,11 +1,11 @@ /* 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 PORT: process.env.PORT || 3000, // below env variables are visible in frontend - GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI', + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyC9tPymo7xUlvPlK_yNulgXTZalxJM2Wv8', }; diff --git a/config/development.js b/config/development.js index f1bbbf3..165fda8 100644 --- a/config/development.js +++ b/config/development.js @@ -7,5 +7,5 @@ module.exports = { PORT: process.env.PORT || 3000, // below env variables are visible in frontend - GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI', + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyC9tPymo7xUlvPlK_yNulgXTZalxJM2Wv8', }; diff --git a/config/production.js b/config/production.js index 2bdcc33..799300c 100644 --- a/config/production.js +++ b/config/production.js @@ -7,5 +7,5 @@ module.exports = { PORT: process.env.PORT || 3000, // below env variables are visible in frontend - GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI', + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyC9tPymo7xUlvPlK_yNulgXTZalxJM2Wv8', }; diff --git a/config/staging.js b/config/staging.js index f1bbbf3..165fda8 100644 --- a/config/staging.js +++ b/config/staging.js @@ -7,5 +7,5 @@ module.exports = { PORT: process.env.PORT || 3000, // below env variables are visible in frontend - GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI', + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyC9tPymo7xUlvPlK_yNulgXTZalxJM2Wv8', }; diff --git a/config/test.js b/config/test.js index f1bbbf3..165fda8 100644 --- a/config/test.js +++ b/config/test.js @@ -7,5 +7,5 @@ module.exports = { PORT: process.env.PORT || 3000, // below env variables are visible in frontend - GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI', + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyC9tPymo7xUlvPlK_yNulgXTZalxJM2Wv8', }; diff --git a/package.json b/package.json index 7ae293e..a8a60ab 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,11 @@ "author": "", "license": "MIT", "dependencies": { + "attr-accept": "^1.1.0", "auth0-js": "^7.6.0", "autoprefixer": "^6.5.0", + "aws-sdk": "^2.7.21", + "aws-sdk-promise": "0.0.2", "axios": "^0.15.3", "babel-core": "^6.17.0", "babel-eslint": "^7.0.0", @@ -67,6 +70,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", @@ -78,8 +82,11 @@ "react-google-maps": "^6.0.1", "react-highcharts": "^11.0.0", "react-icheck": "^0.3.6", + "react-image-lightbox": "^3.4.1", "react-input-range": "^0.9.3", + "react-measure": "^1.4.5", "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,8 +96,10 @@ "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", + "react-toggle-button": "^2.1.0", "reactable": "^0.14.1", "redbox-react": "^1.2.10", "redux": "^3.0.0", @@ -107,6 +116,7 @@ "superagent-promise": "^1.1.0", "uncontrollable": "^4.0.3", "url-loader": "^0.5.6", + "uuid": "^3.0.1", "webpack": "^1.13.2", "yargs": "^4.0.0" }, 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/ClickWithoutDrag/ClickWithoutDrag.jsx b/src/components/ClickWithoutDrag/ClickWithoutDrag.jsx new file mode 100644 index 0000000..21b6a22 --- /dev/null +++ b/src/components/ClickWithoutDrag/ClickWithoutDrag.jsx @@ -0,0 +1,40 @@ +import React, {PropTypes} from 'react'; + +/** + * Fires onClick only when cursor doesn't move + * Used in react-slick because slick always fires onClick when dragging the slider + */ + +class ClickWithoutDrag extends React.Component { + constructor(props) { + super(props); + this.isClick = false; + + this.onMouseUp = (e) => { + if (this.isClick) { + this.props.onClick(e); + } + }; + this.onMouseMove = () => { + this.isClick = false; + }; + this.onMouseDown = () => { + this.isClick = true; + }; + } + + render() { + return ( + + {this.props.children} + + ); + } +} + +ClickWithoutDrag.propTypes = { + children: PropTypes.any.isRequired, + onClick: PropTypes.func.isRequired, +}; + +export default ClickWithoutDrag; diff --git a/src/components/ClickWithoutDrag/index.js b/src/components/ClickWithoutDrag/index.js new file mode 100644 index 0000000..abb94f3 --- /dev/null +++ b/src/components/ClickWithoutDrag/index.js @@ -0,0 +1,3 @@ +import ClickWithoutDrag from './ClickWithoutDrag'; + +export default ClickWithoutDrag; diff --git a/src/components/CloudinaryGallery/CloudinaryGallery.jsx b/src/components/CloudinaryGallery/CloudinaryGallery.jsx new file mode 100644 index 0000000..1d6b70d --- /dev/null +++ b/src/components/CloudinaryGallery/CloudinaryGallery.jsx @@ -0,0 +1,132 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import Slider from 'react-slick'; +import _ from 'lodash'; +import Measure from 'react-measure'; +import Lightbox from 'react-image-lightbox'; +import config from '../../config'; +import styles from './CloudinaryGallery.scss'; +import CloudinaryGalleryItem from './CloudinaryGalleryItem'; +import Button from 'components/Button'; + +const CLOUDINARY_PREFIX = `http://res.cloudinary.com/${config.CLOUDINARY_ACCOUNT_NAME}/image/fetch/`; + +const sliderProps = { + infinite: false, + dots: true, + speed: 500, + slidesToShow: 1, + slidesToScroll: 1, + vertical: false, + variableWidth: false, +}; + +// css margin +const MARGIN = 8; + +class CloudinaryGallery extends React.Component { + constructor(props) { + super(props); + this.state = { + photoIndex: 0, + isOpen: false, + }; + } + + render() { + const {items, width, count, height, noItemsText} = this.props; + const {isOpen, photoIndex} = this.state; + + if (!items || !items.length) { + return ( +

{noItemsText}

+ ); + } + const itemWidth = Math.floor(width / count) - 2 * MARGIN; + const resizedItems = items.map((item) => ({ + ...item, + // c_fill = crop with retaining original proportions + // g_auto = auto detect point of interests + // see http://cloudinary.com/blog/introducing_smart_cropping_intelligent_quality_selection_and_automated_responsive_images#automatic_content_aware_cropping_g_auto + src: `${CLOUDINARY_PREFIX}w_${itemWidth},h_${height},c_fill,g_auto/${item.src}`, + })); + return ( +
+ {isOpen && +
+ this.setState({isOpen: false})} + onMovePrevRequest={() => this.setState({ + photoIndex: (photoIndex + items.length - 1) % items.length, + })} + onMoveNextRequest={() => this.setState({ + photoIndex: (photoIndex + 1) % items.length, + })} + /> + { + items[photoIndex].type !== 'image' ? + (
+
+
{items[photoIndex].type}
+
+
) : null + } +
+ + + +
+
+ } + + {_.chunk(resizedItems, count).map((slideItems, slideIndex) => ( +
+
+ {slideItems.map((item, itemIndex) => ( +
+ { + this.setState({ + isOpen: true, + photoIndex: slideIndex * count + itemIndex, + }); + }} + /> +
+ ))} +
+
+ ))} +
+
+ ); + } +} + +CloudinaryGallery.propTypes = { + items: PropTypes.array.isRequired, + width: PropTypes.number, + count: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + noItemsText: PropTypes.string.isRequired, +}; + +// HOC wrapping + +const CloudinaryGalleryWithStyles = CSSModules(CloudinaryGallery, styles); + +const CloudinaryGalleryWithMeasure = (props) => ( + + { + ({width}) => + } + +); +export default CloudinaryGalleryWithMeasure; diff --git a/src/components/CloudinaryGallery/CloudinaryGallery.scss b/src/components/CloudinaryGallery/CloudinaryGallery.scss new file mode 100644 index 0000000..d5acb65 --- /dev/null +++ b/src/components/CloudinaryGallery/CloudinaryGallery.scss @@ -0,0 +1,236 @@ +.cloudinary-gallery { + background-color: transparent; + + :global { + /* slick css style https://github.com/kenwheeler/slick/blob/master/slick/slick.css */ + .slick-slider + { + position: relative; + + display: block; + box-sizing: border-box; + + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + -webkit-touch-callout: none; + -khtml-user-select: none; + -ms-touch-action: pan-y; + touch-action: pan-y; + -webkit-tap-highlight-color: transparent; + } + + .slick-list + { + position: relative; + + display: block; + overflow: hidden; + + margin: 0; + padding: 0; + } + .slick-list:focus + { + outline: none; + } + .slick-list.dragging + { + cursor: pointer; + cursor: hand; + } + + .slick-slider .slick-track, + .slick-slider .slick-list + { + -webkit-transform: translate3d(0, 0, 0); + -moz-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + .slick-track + { + position: relative; + top: 0; + left: 0; + + display: block; + } + .slick-track:before, + .slick-track:after + { + display: table; + + content: ''; + } + .slick-track:after + { + clear: both; + } + .slick-loading .slick-track + { + visibility: hidden; + } + + .slick-slide + { + display: none; + float: left; + + height: 100%; + min-height: 1px; + } + [dir='rtl'] .slick-slide + { + float: right; + } + .slick-slide img + { + display: block; + } + .slick-slide.slick-loading img + { + display: none; + } + .slick-slide.dragging img + { + pointer-events: none; + } + .slick-initialized .slick-slide + { + display: block; + } + .slick-loading .slick-slide + { + visibility: hidden; + } + .slick-vertical .slick-slide + { + display: block; + + height: auto; + + border: 1px solid transparent; + } + .slick-arrow.slick-hidden { + display: none; + } + + /* custom styles for slick */ + .slick-slider { + margin-bottom: 7px; + } + + .slick-dots { + margin: 28px 0 0 0; + padding: 0; + text-align: center; + + > li { + list-style: none; + display: inline-block; + margin-left: 8px; + + &:first-child { + margin-left: 0; + } + + > button { + border-radius: 6px; + height: 12px; + margin: 0; + outline: none; + padding: 0; + text-indent: -9999px; + width: 12px; + } + } + } + + .slick-arrow { + background: rgba(#36393e, .74) no-repeat center; + border: 0; + height: 60px; + outline: none; + position: absolute; + text-indent: -9999px; + top: 50%; + width: 35px; + z-index: 1; + + &.slick-prev { + background-image: url("https://github.com/styles/img/icon-gallery-arrow-left.png"); + left: 1px; + + &:before { + display: none; + } + } + + &.slick-next { + background-image: url("https://github.com/styles/img/icon-gallery-arrow-right.png"); + right: 1px; + + &:before { + display: none; + } + } + } + } +} + + +.no-items { + margin: 0; + padding: 0 0 115px 0; +} + +.slide { + display: block; +} + +.slide-inner { + margin: 0 -8px; + display: flex; +} + +.item { + padding: 0 8px; + width: calc(25%); +} +.other-type{ + .icon{ + position:fixed; + top:0; + bottom:0; + right:0; + left:0; + margin:auto; + display: flex; + align-items:center; + justify-content: center; + border: 1px solid #888; + width:240px; + height: 240px; + background-color: #FFF; + z-index: 9999; + text-transform: uppercase; + .type-name{ + font-size: 32px; + } + } +} +.bottom-group{ + position:fixed; + bottom:25px; + left:50%; + transform: translateX(-50%); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/components/CloudinaryGallery/CloudinaryGalleryItem.jsx b/src/components/CloudinaryGallery/CloudinaryGalleryItem.jsx new file mode 100644 index 0000000..d6acc41 --- /dev/null +++ b/src/components/CloudinaryGallery/CloudinaryGalleryItem.jsx @@ -0,0 +1,27 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import ClickWithoutDrag from '../ClickWithoutDrag'; +import styles from './CloudinaryGalleryItem.scss'; + +export const CloudinaryGalleryItem = ({type, src, onClick, height}) => ( + + {type === 'image' && + + } + { + type !== 'image' && +
+ {type} +
+ } +
+); + +CloudinaryGalleryItem.propTypes = { + type: PropTypes.string.isRequired, + src: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + height: PropTypes.number.isRequired, +}; + +export default CSSModules(CloudinaryGalleryItem, styles); diff --git a/src/components/CloudinaryGallery/CloudinaryGalleryItem.scss b/src/components/CloudinaryGallery/CloudinaryGalleryItem.scss new file mode 100644 index 0000000..e9820c2 --- /dev/null +++ b/src/components/CloudinaryGallery/CloudinaryGalleryItem.scss @@ -0,0 +1,21 @@ +.image { + display: block; + height: auto; + width: 100%; +} +.other-type { + border: 1px solid #888; + height: 100%; + width: 100%; + text-align: center; + margin-bottom: 5px; + cursor: pointer; + display: flex; + align-items:center; + justify-content: center; + span{ + display: inline-block; + font-size: 32px; + text-transform: uppercase; + } +} diff --git a/src/components/CloudinaryGallery/index.js b/src/components/CloudinaryGallery/index.js new file mode 100644 index 0000000..407fc6d --- /dev/null +++ b/src/components/CloudinaryGallery/index.js @@ -0,0 +1,3 @@ +import CloudinaryGallery from './CloudinaryGallery'; + +export default CloudinaryGallery; 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..3c2c1b1 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -8,64 +8,95 @@ 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/Header/Header.scss b/src/components/Header/Header.scss index af75609..dcd24be 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.scss @@ -3,7 +3,7 @@ width: 100%; color: white; position: relative; - z-index: 2; + z-index: 1001; > ul { padding: 0; diff --git a/src/components/InfoWindow/InfoWindow.jsx b/src/components/InfoWindow/InfoWindow.jsx index 12821fa..8ba6251 100644 --- a/src/components/InfoWindow/InfoWindow.jsx +++ b/src/components/InfoWindow/InfoWindow.jsx @@ -32,6 +32,8 @@ class InfoWindow extends Component { commandText = `Waypoint (${this.props.command} / ${this.getType()} ) `; } else if (this.props.command === 21) { commandText = `Land (${this.props.command} / ${this.getType()} ) `; + } else if (this.props.command === 203) { + commandText = `Take a Picture (${this.props.command} / ${this.getType()} ) `; } return commandText; diff --git a/src/components/InfoWindow/data/commands.js b/src/components/InfoWindow/data/commands.js index 22e785f..8f7a03b 100644 --- a/src/components/InfoWindow/data/commands.js +++ b/src/components/InfoWindow/data/commands.js @@ -38,6 +38,10 @@ const commands = [ { value: 112, label: 'MAV_CMD_CONDITION_DELAY' + }, + { + value: 203, + label: 'Take a Picture' } ] diff --git a/src/components/Loader/Loader.jsx b/src/components/Loader/Loader.jsx new file mode 100644 index 0000000..889d9c1 --- /dev/null +++ b/src/components/Loader/Loader.jsx @@ -0,0 +1,20 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import styles from './Loader.scss'; + +// Loader generated from http://loading.io/ + +export const Loader = ({scale}) => ( +
    +
    +
    +
    +
    +
    +); + +Loader.propTypes = { + scale: PropTypes.number.isRequired, +}; + +export default CSSModules(Loader, styles); diff --git a/src/components/Loader/Loader.scss b/src/components/Loader/Loader.scss new file mode 100644 index 0000000..0f22b91 --- /dev/null +++ b/src/components/Loader/Loader.scss @@ -0,0 +1,216 @@ + +@-webkit-keyframes uil-rolling-anim { + 0% { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -ms-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes uil-rolling-anim { + 0% { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -ms-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes uil-rolling-anim { + 0% { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -ms-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-ms-keyframes uil-rolling-anim { + 0% { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -ms-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes uil-rolling-anim { + 0% { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -ms-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes uil-rolling-anim { + 0% { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -ms-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-o-keyframes uil-rolling-anim { + 0% { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -ms-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes uil-rolling-anim { + 0% { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -ms-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +.uil-rolling-css { + width: 200px; + height: 200px; +} +.uil-rolling-css > div { + width: 200px; + height: 200px; + position: relative; + -ms-animation: uil-rolling-anim 1s linear infinite; + -moz-animation: uil-rolling-anim 1s linear infinite; + -webkit-animation: uil-rolling-anim 1s linear infinite; + -o-animation: uil-rolling-anim 1s linear infinite; + animation: uil-rolling-anim 1s linear infinite; +} +.uil-rolling-css > div div { + position: absolute; + width: 200px; + height: 100px; + border-radius: 1000px 1000px 0 0; + border-color: #d25353; + border-style: solid; + border-width: 40px; + border-bottom-width: 0; +} +.uil-rolling-css > div div:nth-of-type(2) { + -ms-transform: translate(0, 50px) rotate(54deg) translate(0, -50px); + -moz-transform: translate(0, 50px) rotate(54deg) translate(0, -50px); + -webkit-transform: translate(0, 50px) rotate(54deg) translate(0, -50px); + -o-transform: translate(0, 50px) rotate(54deg) translate(0, -50px); + transform: translate(0, 50px) rotate(54deg) translate(0, -50px); +} diff --git a/src/components/Loader/index.js b/src/components/Loader/index.js new file mode 100644 index 0000000..45ded85 --- /dev/null +++ b/src/components/Loader/index.js @@ -0,0 +1,3 @@ +import Loader from './Loader'; + +export default Loader; 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..8de0df9 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/Spinner/Spinner.jsx b/src/components/Spinner/Spinner.jsx new file mode 100644 index 0000000..50982a8 --- /dev/null +++ b/src/components/Spinner/Spinner.jsx @@ -0,0 +1,52 @@ +import React, {PropTypes} from 'react'; +import CSSModules from 'react-css-modules'; +import Modal from 'react-modal'; +import styles from './Spinner.scss'; + +const customStyles = { + overlay: { + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(9, 9, 9, 0.58)', + zIndex: '9999', + }, + content: { + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + marginRight: '-50%', + transform: 'translate(-50%, -50%)', + padding: '20px', + minWidth: '217px', + textAlign: 'center', + borderRadius: '5px', + fontWeight: 'bold', + fontSize: '20px', + zIndex: '99999', + }, +}; + +const Spinner = ({content, isOpen, error}) => ( + +
    + {content} +
    +
    + ); + +Spinner.propTypes = { + content: PropTypes.string, + isOpen: PropTypes.bool.isRequired, + error: PropTypes.bool, +}; + +export default CSSModules(Spinner, styles); diff --git a/src/components/Spinner/Spinner.scss b/src/components/Spinner/Spinner.scss new file mode 100644 index 0000000..4d69729 --- /dev/null +++ b/src/components/Spinner/Spinner.scss @@ -0,0 +1,3 @@ +.error{ + color:red; +} diff --git a/src/components/Spinner/index.js b/src/components/Spinner/index.js new file mode 100644 index 0000000..0484da0 --- /dev/null +++ b/src/components/Spinner/index.js @@ -0,0 +1,3 @@ +import Spinner from './Spinner'; + +export default Spinner; diff --git a/src/components/StatusLabel/StatusLabel.jsx b/src/components/StatusLabel/StatusLabel.jsx index 08a11cc..44f1a4f 100644 --- a/src/components/StatusLabel/StatusLabel.jsx +++ b/src/components/StatusLabel/StatusLabel.jsx @@ -1,21 +1,27 @@ 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', + pending: 'Pending', + scheduled: 'Scheduled', + rejected: 'Rejected', + waiting: 'Waiting', }; 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..f08cd2a 100644 --- a/src/components/StatusLabel/StatusLabel.scss +++ b/src/components/StatusLabel/StatusLabel.scss @@ -33,3 +33,38 @@ @extend .status-label; } + +.status-label_pending { + background-color: lightblue; + background-image: url('https://github.com/icon-status-inprogress.png'); + + @extend .status-label; +} + +.status-label_waiting { + background-color: lightblue; + background-image: url('https://github.com/icon-status-inprogress.png'); + + @extend .status-label; +} + +.status-label_rejected{ + background-color: red; + background-image: url('https://github.com/icon-status-cancelled.png'); + + @extend .status-label; +} + +.status-label_scheduled{ + background-color: pink; + background-image: url('https://github.com/icon-status-inprogress.png'); + + @extend .status-label; +} + +.status-label_waiting { + background-color: #e3e3e3; + 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/Tabs/Tabs.jsx b/src/components/Tabs/Tabs.jsx index 567da7c..52e2fef 100644 --- a/src/components/Tabs/Tabs.jsx +++ b/src/components/Tabs/Tabs.jsx @@ -2,10 +2,10 @@ import React, {PropTypes} from 'react'; import CSSModules from 'react-css-modules'; import styles from './Tabs.scss'; -export const Tabs = ({tabList, activeTab}) => ( +export const Tabs = ({tabList, onSelect, activeTab}) => (
      {(tabList || []).map((tab, i) => ( -
    • {tab.name}
    • +
    • onSelect(i)} styleName={activeTab === i ? 'active-tab' : null} key={i}>{tab.name}
    • ))}
    ); @@ -13,6 +13,7 @@ export const Tabs = ({tabList, activeTab}) => ( Tabs.propTypes = { tabList: PropTypes.array.isRequired, activeTab: PropTypes.number.isRequired, + onSelect: PropTypes.func, }; export default CSSModules(Tabs, styles); 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) => ( -
    -