diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..767883c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +server/* +config/webpack* diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..2938725 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,27 @@ +{ + "parser": "babel-eslint", + "extends": "airbnb", + "plugins": [ + "react", + "jsx-a11y", + "import" + ], + "env": { + "browser": true, + "node": true + }, + "rules": { + "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], + "react/jsx-one-expression-per-line": "off", + "react/no-array-index-key": "off", + "jsx-a11y/href-no-hash": "off", + "jsx-a11y/label-has-for": "off", + "react/prefer-stateless-function": "off", + "comma-dangle": ["error", {"functions": "never"}], + "indent": ["error", 2], + "linebreak-style": "off", + "import/no-unresolved": "off", + "no-console": "off", + "object-curly-newline": "off" + } +} diff --git a/.gitignore b/.gitignore index 5fee6a2..b80b89c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ Desktop.ini .directory *~ +# JetBrains +.idea # npm node_modules @@ -25,3 +27,8 @@ coverage # Benchmarking benchmarks/graphs + +# Other +.vscode +build +server/.env \ No newline at end of file diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 0000000..62c21d0 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,3 @@ +{ + "extends": "stylelint-config-standard", +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f7a491 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# TripAssistantCore + +Start Express server: npm run express;

+NOTE: Edit database config before starting API server. Path: server/.env.sample (delete .sample and add your credential)

+Database setup:
+1: npm run createTables;
+2: npm run dbseed.
+For recreate all project tables with empty data use:
+npm run reCreateTables
+For clearing all tables use:
+npm run clearAllTabless
+
+
+
+For dev Server: npm run dev;
+For build production: npm run build;
+
+NOTE: "Dev Server" starting with Eslint;
+ diff --git a/client/App.jsx b/client/App.jsx new file mode 100644 index 0000000..218d624 --- /dev/null +++ b/client/App.jsx @@ -0,0 +1,92 @@ +import React, { Fragment } from 'react'; +import { Route, Switch, Redirect } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { GoogleApiWrapper } from 'google-maps-react'; +import axios from 'axios'; +import './App.scss'; +import Routes from './routes'; +import Login from './pages/Login'; +import Header from './components/Header'; +import Menu from './components/Menu'; + +const KEY = 'AIzaSyDWfF4B8J4mmrLGltJfU9XqEauLS8PCarg'; + +const PrivateRoute = ({ component: Component, isAuth, ...rest }) => { + const id = window.location.search.substr(1); + if (id) { + axios.get(`/api/register/${id}`) + .then(({ data: { iduser, role } }) => { + if (iduser) { + sessionStorage.setItem('iduser', iduser); + sessionStorage.setItem('role', role); + } + }) + .catch(e => console.log(e)); + } + return ( + ( + isAuth === true + ? + : + )} + /> + ); +}; + +class App extends React.Component { + constructor() { + super(); + this.state = { + isAuth: false + }; + } + + componentDidMount() { + this.updateIsAuth(); + } + + updateIsAuth = () => { + if (sessionStorage.getItem('iduser')) { + this.setState({ isAuth: true }); + } else { + this.setState({ isAuth: false }); + } + } + + runLogout = () => { + sessionStorage.removeItem('iduser'); + sessionStorage.removeItem('role'); + this.setState({ isAuth: false }); + } + + render() { + const { isAuth } = this.state; + + return ( + +
+
+ +
+ + {Routes.map((route, i) => )} + } /> + +
+
+ + ); + } +} + +PrivateRoute.propTypes = { + component: PropTypes.func.isRequired, + isAuth: PropTypes.bool.isRequired +}; + +export default GoogleApiWrapper({ + apiKey: (KEY), + language: 'en' +})(App); diff --git a/client/App.scss b/client/App.scss new file mode 100644 index 0000000..8778862 --- /dev/null +++ b/client/App.scss @@ -0,0 +1,144 @@ +@import 'materialize-css/sass/materialize.scss'; + +#app { + padding-top: 50px; + height: 100%; +} + +#map { + margin: -10px -15px 0 0; + flex-grow: 1; + height: calc(100vh - 50px); + position: relative; +} + +.main { + display: flex; + height: auto; + min-height: calc(100vh - 50px); +} + +.content { + width: calc(100% - 110px); + padding: 10px 15px 0; + background: url('~@images/profile-page-bg.jpg') top; + background-size: cover; + + &__wrapper { + margin-right: auto; + margin-left: auto; + padding-right: 15px; + padding-left: 15px; + width: 100%; + max-width: 1240px; + } +} + +.online::before { + content: ""; + display: inline-block; + width: 10px; + height: 10px; + margin-right: 5px; + border-radius: 50%; + background-color: #4caf50; +} + +.offline::before { + content: ""; + display: inline-block; + width: 10px; + height: 10px; + margin-right: 5px; + border-radius: 50%; + background-color: #e53935; +} + +button { + border-width: 0; + border-style: none; + border-color: transparent; + border-image: none; +} + +// COLORS +$blue: #33afe0; +$darkblue: #252d3a; +$green: #2ab7a9; + +p { + color: rgba($darkblue, 0.8); + font-size: 14px; + font-weight: 400; + line-height: 1.5em; +} + +h1 { + color: #252d3a; + font-size: 30px; + font-weight: 600; + line-height: 1.2em; +} + +.main-header { + color: #252d3a; + font-size: 30px; + font-weight: 600; + line-height: 1em; + padding: 40px 15px 30px; + background-color: rgba(#fff, 0.5); + text-align: center; + margin: -15px -10px 20px; +} + +.main-card { + background-color: #f7f8fa; + text-align: left; + width: 100%; + border-radius: 3px; + box-shadow: 20px 0 40px rgba(74, 77, 101, 0.2); + + &__heading { + color: #252d3a; + font-size: 14px; + line-height: 1em; + font-weight: 600; + margin-bottom: 0; + padding: 32px 15px 31px; + box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1); + border-radius: 3px 3px 0 0; + text-align: center; + } + + &__wrap { + background-color: #f7f8fa; + margin-bottom: 35px; + width: 100%; + border-radius: 3px; + box-shadow: 20px 0 40px rgba(74, 77, 101, 0.2); + } + + &__body { + padding: 15px; + text-align: center; + } +} + +.main-btn { + display: inline-block; + padding: 15px 23px 15px; + margin: 0 10px; + border-radius: 3px; + background-color: $blue; + color: #fff; + font-size: 12px; + font-weight: 600; + line-height: 1em; + letter-spacing: 0.6px; + cursor: pointer; + transition: background-color 0.15s linear; + + &:hover { + background-color: $darkblue; + } +} diff --git a/client/components/CarsCard/CarCard/CarCard.jsx b/client/components/CarsCard/CarCard/CarCard.jsx new file mode 100644 index 0000000..ec975a9 --- /dev/null +++ b/client/components/CarsCard/CarCard/CarCard.jsx @@ -0,0 +1,164 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import axios from 'axios'; +import { toast } from 'materialize-css'; + +import arrowDown from 'images/arrow-down.svg'; +import ProfileModal from '../../ProfileModal'; +import './CarCard.scss'; + +const CarInput = ({ + inputDefaultValue, + inputAdditionalInfo: { + inputLabel, inputName, inputPattern, inputTitle + } +}) => ( +
+ + +
+); + +class CarCard extends Component { + constructor() { + super(); + this.state = { + isActive: false, + isModalOpen: false + }; + } + + componentDidMount() { + + } + + setFirstCarAsActive = () => { + const { id } = this.props; + return id === 0 && this.setState({ isActive: true }); + } + + toggleModal = () => { + const { isModalOpen } = this.state; + this.setState({ isModalOpen: !isModalOpen }); + } + + handleDeleteCar = (e) => { + e.preventDefault(); + const { carInfo: { idcar: idCar }, updateCarData } = this.props; + axios.post('api/user/deleteCar/', { data: idCar }) + .then(() => { + updateCarData(); + toast({ html: 'Vehicle has been deleted!' }); + }) + .catch(() => toast({ html: 'Vehicle has NOT been deleted!' })) + .then(() => this.toggleModal()); + } + + handleUpdateCar = (e) => { + e.preventDefault(); + const { inputInfo, updateCarData } = this.props; + const updateCarinfo = { idCar: e.target.elements.idCars.value }; + inputInfo.forEach((el) => { + updateCarinfo[el.inputName] = e.target.elements[el.inputName].value; + }); + axios.post('/api/user/updateCar/', { newCarData: updateCarinfo }) + .then(() => { + updateCarData(); + toast({ html: 'Vehicle has been updated!' }); + }) + .catch(() => toast({ html: 'Vehicle has NOT been updated!' })); + } + + toggleCarCardBody = () => { + const { isActive } = this.state; + this.setState({ isActive: !isActive }); + } + + render() { + const { + carInfo: { + idcar: idCars, + namecar: nameCar, + tankvolume: tankVolume, + maxpassengerscount: maxPassengersCount, + avggascost: avgGasCost, + baggagevolume: baggageVolume, + avgspeed: avgSpeed + }, + id, + inputInfo + } = this.props; + const { isActive, isModalOpen } = this.state; + const carInfoArr = [ + nameCar.trim(), tankVolume, maxPassengersCount, avgGasCost, baggageVolume, avgSpeed + ]; + + return ( +
+ + + {isActive && ( +
+
+ {carInfoArr.map((inputDefaultValue, i) => ( + + ))} + +

Delete this vehicle

+ + +
+ )} +
+ +
e.stopPropagation()}> +

Are you sure you want to delete this vehicle?

+
+ + +
+
+
+
+ ); + } +} + +CarInput.propTypes = { + inputDefaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + inputAdditionalInfo: PropTypes.objectOf(PropTypes.string).isRequired +}; + +CarCard.propTypes = { + carInfo: PropTypes.shape({ + idCars: PropTypes.number, + nameCar: PropTypes.string, + tankVolume: PropTypes.number, + maxPassengersCount: PropTypes.number, + avgGasCost: PropTypes.number, + baggageVolume: PropTypes.number, + avgSpeed: PropTypes.number + }).isRequired, + id: PropTypes.number.isRequired, + inputInfo: PropTypes.arrayOf(PropTypes.object).isRequired, + updateCarData: PropTypes.func.isRequired +}; + +export default CarCard; diff --git a/client/components/CarsCard/CarCard/CarCard.scss b/client/components/CarsCard/CarCard/CarCard.scss new file mode 100644 index 0000000..a4b9c0c --- /dev/null +++ b/client/components/CarsCard/CarCard/CarCard.scss @@ -0,0 +1,167 @@ +.car-card__wrap { + box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1); + border-radius: 2px; + background-color: #fff; + margin-bottom: 5px; + position: relative; +} + +.profile button, +.search-route button { + border-width: 0; + border-style: none; + border-color: transparent; + border-image: none; +} + +.car-card__heading-block { + display: flex; + align-items: center; + padding: 27px 18px 24px 22px; + cursor: pointer; + -webkit-appearance: none; + background-color: #fff; + width: 100%; + border-width: 0; + border-style: none; + border-color: transparent; + border-image: none; + + &:active { + background-color: #fff; + border-style: none; + } + + &:focus { + background-color: #fff; + outline: none; + } +} + +.car-card__num { + text-align: left; + display: inline-block; + width: 35px; + color: #33afe0; + font-size: 30px; + font-weight: 600; + line-height: 1.2em; + letter-spacing: 1.5px; +} + +.car-card__arrow-down { + flex-shrink: 0; + display: block; + margin-left: auto; + margin-right: 0; + transform-origin: 50% 50%; + transition: transform 0.15s linear; +} + +.car-card__arrow-up { + flex-shrink: 0; + display: block; + margin-left: auto; + margin-right: 0; + transform-origin: 50% 50%; + transition: transform 0.15s linear; + transform: rotate(180deg); +} + +.car-card__name-p { + color: #333; + font-size: 15px; + font-weight: 600; + line-height: 1.5em; +} + +.car-card__body { + padding: 20px 20px 25px; +} + +.car-card__label { + display: block; + color: rgba(#252d3a, 0.8); + font-size: 14px; + font-weight: 400; + line-height: 1em; + margin-bottom: 5px; + text-align: left; +} + +input[type=text]:not(.browser-default).car-card__input:focus:not([readonly]) + label { + color: #33afe0; +} + +input[type=text]:not(.browser-default).car-card__input { + background-color: #fff; + height: 42px; + width: 100%; + display: block; + color: #252d3a; + font-size: 14px; + font-weight: 400; + line-height: 1em; + margin-bottom: 5px; + transition: + border 0.15s linear, + box-shadow 0.15s linear; + + &:focus { + outline: 0; + outline-offset: 0; + border-bottom: 1px solid #33afe0; + box-shadow: 0 1px 0 0 #33afe0; + } +} + +.car-card__btn-submit { + display: inline-block; + padding: 17px 35px 16px; + margin: 0 10px; + border-radius: 3px; + background-color: #33afe0; + color: #fff; + font-size: 12px; + font-weight: 600; + line-height: 1em; + letter-spacing: 0.6px; + cursor: pointer; + transition: background-color 0.15s linear; + + &:hover { + background-color: #252d3a; + } +} + +.car-card__form-wrap { + display: flex; + flex-wrap: wrap; + margin-left: -15px; + margin-right: -15px; + justify-content: center; +} + +.car-card__col { + width: calc(50% - 30px); + margin-right: 15px; + margin-left: 15px; +} + +.car-card__delete-link { + width: 100%; + text-align: right; + margin-bottom: 0; + padding: 0 15px 15px; + + a { + color: #33afe0; + text-decoration: underline; + + &:hover, + &:focus { + color: #33afe0; + text-decoration: underline; + } + } +} diff --git a/client/components/CarsCard/CarCard/package.json b/client/components/CarsCard/CarCard/package.json new file mode 100644 index 0000000..1b2e9be --- /dev/null +++ b/client/components/CarsCard/CarCard/package.json @@ -0,0 +1,3 @@ +{ + "main": "CarCard.jsx" +} \ No newline at end of file diff --git a/client/components/CarsCard/CarsCard.jsx b/client/components/CarsCard/CarsCard.jsx new file mode 100644 index 0000000..e2ad866 --- /dev/null +++ b/client/components/CarsCard/CarsCard.jsx @@ -0,0 +1,96 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import axios from 'axios'; +import { toast } from 'materialize-css'; + +import CarCard from './CarCard'; +import NewCarCard from './NewCarCard'; +import './CarsCard.scss'; + +const inputInfo = [ + { + inputLabel: 'Car name', inputName: 'nameCar', inputPattern: '^[a-zA-Z0-9_.-]*$', inputTitle: 'Please input a string without spaces (no special characters allowed)' + }, + { + inputLabel: 'Tank Volume', inputName: 'tankVolume', inputPattern: '^\\d+$', inputTitle: 'Please input positive integer' + }, + { + inputLabel: 'Number of passengers', inputName: 'maxPassengersCount', inputPattern: '^\\d+$', inputTitle: 'Please input positive integer' + }, + { + inputLabel: 'Average gas cost', inputName: 'avgGasCost', inputPattern: '^\\d+$', inputTitle: 'Please input positive integer' + }, + { + inputLabel: 'Baggage size, m3', inputName: 'baggageVolume', inputPattern: '^\\d+$', inputTitle: 'Please input positive integer' + }, + { + inputLabel: 'Average speed, km/h', inputName: 'avgSpeed', inputPattern: '^\\d+$', inputTitle: 'Please input positive integer' + } +]; + +class CarsCard extends Component { + constructor() { + super(); + this.state = { + addNew: false + }; + } + + handleAddNewCar = (e) => { + e.preventDefault(); + const iduser = sessionStorage.getItem('iduser'); + const { updateCarData } = this.props; + const newCarinfo = {}; + newCarinfo.iduser = iduser; + inputInfo.forEach((el) => { + newCarinfo[el.inputName] = e.target.elements[el.inputName].value; + }); + axios.post('api/user/addCar', { formData: newCarinfo }) + .then(() => { + updateCarData(); + toast({ html: 'New vehicle has been added!' }); + this.setState({ addNew: false }); + }) + .catch(() => toast({ html: 'New vehicle has NOT been added!' })); + } + + toggleAddNewBtn = () => { + const { addNew } = this.state; + this.setState({ addNew: !addNew }); + } + + render() { + const { carsInfo, updateCarData } = this.props; + const { addNew } = this.state; + + return ( +
+

+ Vehicles + {addNew ? 'CANCEL' : 'ADD NEW'} +

+
+ + {addNew && } + + {carsInfo.map((carInfo, i) => ( + ))} +
+
+ ); + } +} + +CarsCard.propTypes = { + carsInfo: PropTypes.arrayOf(PropTypes.object).isRequired, + updateCarData: PropTypes.func.isRequired +}; + +export default CarsCard; diff --git a/client/components/CarsCard/CarsCard.scss b/client/components/CarsCard/CarsCard.scss new file mode 100644 index 0000000..ba7b223 --- /dev/null +++ b/client/components/CarsCard/CarsCard.scss @@ -0,0 +1,94 @@ +.cars-card__wrap { + background-color: #f7f8fa; + text-align: left; + margin-bottom: 35px; + width: 100%; + border-radius: 3px; + box-shadow: 20px 0 40px rgba(74, 77, 101, 0.2); +} + +.cars-card__heading { + position: relative; + color: #252d3a; + font-size: 14px; + line-height: 1em; + font-weight: 600; + margin-bottom: 0; + padding: 32px 125px 31px; + box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1); + border-radius: 3px 3px 0 0; + text-align: center; +} + +.cars-card__body { + padding: 8px 10px 10px; + text-align: center; + overflow: hidden; +} + +.cars-card__add-car { + position: absolute; + top: 50%; + right: 15px; + transform: translateY(-50%); + display: inline-block; + padding: 17px 15px 16px; + border-radius: 3px; + background-color: #252d3a; + color: #fff; + font-size: 12px; + font-weight: 600; + line-height: 1em; + letter-spacing: 0.6px; + transition: background-color 0.15s linear; + + &:hover, + &:focus { + background-color: #33afe0; + } +} + +.slideInOut-enter { + transform: translateY(-20px); + opacity: 0; +} + +.slideInOut-leave { + transform: translateY(0); + opacity: 1; +} + +.slideInOut-enter.slideInOut-enter-active { + transform: translateY(0); + opacity: 1; + transition: + opacity 0.3s ease-in-out, + transform 0.3s ease-in-out; +} + +.slideInOut-leave.slideInOut-leave-active { + transform: translateY(-20px); + opacity: 0; + transition: + opacity 0.3s ease-in-out, + transform 0.3s ease-in-out; +} + +.fadeInOut-enter { + opacity: 0; +} + +.fadeInOut-leave { + transform: translateY(0); + opacity: 1; +} + +.fadeInOut-enter.fadeInOut-enter-active { + opacity: 1; + transition: opacity 0.3s ease-in-out; +} + +.fadeInOut-leave.fadeInOut-leave-active { + opacity: 0; + transition: opacity 0.3s ease-in-out; +} diff --git a/client/components/CarsCard/NewCarCard/NewCarCard.jsx b/client/components/CarsCard/NewCarCard/NewCarCard.jsx new file mode 100644 index 0000000..55da7a8 --- /dev/null +++ b/client/components/CarsCard/NewCarCard/NewCarCard.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const CarInput = ({ + inputAdditionalInfo: { + inputLabel, inputName, inputPattern, inputTitle + } +}) => ( +
+ + +
+); + +const NewCarCard = ({ submitHandler, inputInfo }) => ( +
+
+
+ {inputInfo.map((inputDefaultValue, i) => ( + ))} + + +
+
+); + +CarInput.propTypes = { + inputAdditionalInfo: PropTypes.objectOf(PropTypes.string).isRequired +}; + +NewCarCard.propTypes = { + submitHandler: PropTypes.func.isRequired, + inputInfo: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default NewCarCard; diff --git a/client/components/CarsCard/NewCarCard/package.json b/client/components/CarsCard/NewCarCard/package.json new file mode 100644 index 0000000..5468c73 --- /dev/null +++ b/client/components/CarsCard/NewCarCard/package.json @@ -0,0 +1,3 @@ +{ + "main": "NewCarCard.jsx" +} \ No newline at end of file diff --git a/client/components/CarsCard/package.json b/client/components/CarsCard/package.json new file mode 100644 index 0000000..f0158ef --- /dev/null +++ b/client/components/CarsCard/package.json @@ -0,0 +1,3 @@ +{ + "main": "CarsCard.jsx" +} \ No newline at end of file diff --git a/client/components/FeedbacksCard/FeedbackCard/FeedbackCard.jsx b/client/components/FeedbacksCard/FeedbackCard/FeedbackCard.jsx new file mode 100644 index 0000000..2dde5c7 --- /dev/null +++ b/client/components/FeedbacksCard/FeedbackCard/FeedbackCard.jsx @@ -0,0 +1,73 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; + +import './FeedbackCard.scss'; +import arrowDown from 'images/arrow-down.svg'; +import { getDateFromTimestamp } from '../../../helpers'; + +class FeedbackCard extends Component { + constructor() { + super(); + this.state = { + isActive: false + }; + this.toggleFeedbackBody = this.toggleFeedbackBody.bind(this); + } + + toggleFeedbackBody() { + const { isActive } = this.state; + this.setState({ + isActive: !isActive + }); + } + + render() { + const { + feedbackInfo: + { + trip_name: name, + rating, + text, + user_name: { first: userName }, + date + } + } = this.props; + const { isActive } = this.state; + return ( +
+ + + {isActive && ( +
+

{text}

+

{userName}

+

{getDateFromTimestamp(date)}

+
)} +
+
+ ); + } +} + +FeedbackCard.propTypes = { + feedbackInfo: PropTypes.shape({ + name: PropTypes.string, + rating: PropTypes.string, + feedback: PropTypes.string, + userName: PropTypes.string, + date: PropTypes.string + }).isRequired +}; + +export default FeedbackCard; diff --git a/client/components/FeedbacksCard/FeedbackCard/FeedbackCard.scss b/client/components/FeedbacksCard/FeedbackCard/FeedbackCard.scss new file mode 100644 index 0000000..1bb9a9d --- /dev/null +++ b/client/components/FeedbacksCard/FeedbackCard/FeedbackCard.scss @@ -0,0 +1,107 @@ +.feedback__wrap { + box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1); + border-radius: 2px; + background-color: #fff; + margin-bottom: 5px; +} + +.feedback__heading-block { + display: flex; + align-items: center; + justify-content: space-between; + padding: 15px 18px 15px 22px; + cursor: pointer; + -webkit-appearance: none; + background-color: #fff; + width: 100%; + border-width: 0; + border-style: none; + border-color: transparent; + border-image: none; + + &:active { + background-color: #fff; + border-style: none; + } + + &:focus { + background-color: #fff; + outline: none; + } +} + +.feedback__num { + text-align: left; + display: inline-block; + color: #33afe0; + font-size: 30px; + font-weight: 600; + line-height: 1.2em; + letter-spacing: 1.5px; + padding-right: 5px; +} + +.feedback__arrow-down { + flex-shrink: 0; + display: block; + margin-left: auto; + margin-right: 0; + transform-origin: 50% 50%; + transition: transform 0.15s linear; +} + +.feedback__arrow-up { + flex-shrink: 0; + display: block; + margin-left: auto; + margin-right: 0; + transform-origin: 50% 50%; + transition: transform 0.15s linear; + transform: rotate(180deg); +} + +.feedback__name-p { + color: #333; + font-size: 15px; + font-weight: 600; + line-height: 1.5em; +} + +.feedback__body { + padding: 10px 20px 25px; + text-align: left; +} + +.feedback__rating-wrap { + color: #252d3a; + font-size: 14px; + font-weight: 400; + line-height: 1.5em; + padding-right: 20px; + text-align: left; +} + +.feedback__sec { + display: block; + font-size: 13px; +} + +.feedback__right-side { + display: flex; + align-items: center; +} + +.feedback__body-p { + color: rgba(#252d3a, 0.8); + font-size: 14px; + font-weight: 400; + line-height: 1.5em; + margin-bottom: 30px; +} + +.feedback__body-p2 { + color: #252d3a; + font-size: 14px; + font-weight: 600; + line-height: 1.8em; +} diff --git a/client/components/FeedbacksCard/FeedbackCard/package.json b/client/components/FeedbacksCard/FeedbackCard/package.json new file mode 100644 index 0000000..5066509 --- /dev/null +++ b/client/components/FeedbacksCard/FeedbackCard/package.json @@ -0,0 +1,3 @@ +{ + "main": "FeedbackCard.jsx" +} \ No newline at end of file diff --git a/client/components/FeedbacksCard/FeedbacksCard.jsx b/client/components/FeedbacksCard/FeedbacksCard.jsx new file mode 100644 index 0000000..2fee28e --- /dev/null +++ b/client/components/FeedbacksCard/FeedbacksCard.jsx @@ -0,0 +1,29 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import FeedbackCard from './FeedbackCard'; +import './FeedbacksCard.scss'; + +class FeedbacksCard extends Component { + render() { + const { feedbacksInfo } = this.props; + + return ( +
+

+ Feedbacks +

+
+ {feedbacksInfo.map((feedbackInfo, i) => ( + ))} +
+
+ ); + } +} + +FeedbacksCard.propTypes = { + feedbacksInfo: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default FeedbacksCard; diff --git a/client/components/FeedbacksCard/FeedbacksCard.scss b/client/components/FeedbacksCard/FeedbacksCard.scss new file mode 100644 index 0000000..2963ac2 --- /dev/null +++ b/client/components/FeedbacksCard/FeedbacksCard.scss @@ -0,0 +1,26 @@ +.feedbacks__wrap { + background-color: #f7f8fa; + text-align: left; + margin-bottom: 35px; + width: 100%; + border-radius: 3px; + box-shadow: 20px 0 40px rgba(74, 77, 101, 0.2); +} + +.feedbacks__heading { + position: relative; + color: #252d3a; + font-size: 14px; + line-height: 1em; + font-weight: 600; + margin-bottom: 0; + padding: 32px 125px 31px; + box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1); + border-radius: 3px 3px 0 0; + text-align: center; +} + +.feedbacks__body { + padding: 8px 10px 10px; + text-align: center; +} diff --git a/client/components/FeedbacksCard/package.json b/client/components/FeedbacksCard/package.json new file mode 100644 index 0000000..ac21fff --- /dev/null +++ b/client/components/FeedbacksCard/package.json @@ -0,0 +1,3 @@ +{ + "main": "FeedbacksCard.jsx" +} \ No newline at end of file diff --git a/client/components/Header/Header.jsx b/client/components/Header/Header.jsx new file mode 100644 index 0000000..d8df545 --- /dev/null +++ b/client/components/Header/Header.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Authorization from './components/Authorization'; +import Logo from './components/Logo'; +import './Header.scss'; + +const Header = ({ isAuth, runLogout }) => ( +
+ + {isAuth ? : } +
+); + +Header.propTypes = { + isAuth: PropTypes.bool.isRequired, + runLogout: PropTypes.func.isRequired +}; + +export default Header; diff --git a/client/components/Header/Header.scss b/client/components/Header/Header.scss new file mode 100644 index 0000000..906868f --- /dev/null +++ b/client/components/Header/Header.scss @@ -0,0 +1,27 @@ +.header { + position: fixed; + z-index: 9999; + top: 0; + left: 0; + right: 0; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + height: 50px; + padding: 5px 15px; + border-bottom: 1px solid #364154; + background-color: #252d3a; +} + +.logoutBtn { + background: transparent; + color: #fff; + cursor: pointer; + + &:hover, + &:focus { + color: #fff; + background: transparent; + } +} diff --git a/client/components/Header/components/Authorization/Authorization.jsx b/client/components/Header/components/Authorization/Authorization.jsx new file mode 100644 index 0000000..cdcd524 --- /dev/null +++ b/client/components/Header/components/Authorization/Authorization.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import LoginIcon from 'images/user.svg'; +import './Authorization.scss'; + +const Authorization = () => ( + + Login + +); + +export default Authorization; diff --git a/client/components/Header/components/Authorization/Authorization.scss b/client/components/Header/components/Authorization/Authorization.scss new file mode 100644 index 0000000..1f99f07 --- /dev/null +++ b/client/components/Header/components/Authorization/Authorization.scss @@ -0,0 +1,10 @@ +.authorization { + display: inline-block; + width: 28px; + height: 28px; + + img { + width: 28px; + height: 28px; + } +} diff --git a/client/components/Header/components/Authorization/package.json b/client/components/Header/components/Authorization/package.json new file mode 100644 index 0000000..7ada78d --- /dev/null +++ b/client/components/Header/components/Authorization/package.json @@ -0,0 +1,3 @@ +{ + "main": "Authorization.jsx" +} \ No newline at end of file diff --git a/client/components/Header/components/Logo/Logo.jsx b/client/components/Header/components/Logo/Logo.jsx new file mode 100644 index 0000000..c9ba581 --- /dev/null +++ b/client/components/Header/components/Logo/Logo.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import './Logo.scss'; + +const Logo = () => ( + + TRIP ASSISTANT + +); + + +export default Logo; diff --git a/client/components/Header/components/Logo/Logo.scss b/client/components/Header/components/Logo/Logo.scss new file mode 100644 index 0000000..6ba3341 --- /dev/null +++ b/client/components/Header/components/Logo/Logo.scss @@ -0,0 +1,10 @@ +// ######################################################## +// This style is exemple. Please delete it after logo change. +// ######################################################## +.logo { + font-size: 14px; + font-weight: 700; + line-height: 1em; + letter-spacing: 2.8px; + color: #fff; +} diff --git a/client/components/Header/components/Logo/package.json b/client/components/Header/components/Logo/package.json new file mode 100644 index 0000000..4c083c1 --- /dev/null +++ b/client/components/Header/components/Logo/package.json @@ -0,0 +1,3 @@ +{ + "main": "Logo.jsx" +} \ No newline at end of file diff --git a/client/components/Header/package.json b/client/components/Header/package.json new file mode 100644 index 0000000..fca7a46 --- /dev/null +++ b/client/components/Header/package.json @@ -0,0 +1,3 @@ +{ + "main": "Header.jsx" +} \ No newline at end of file diff --git a/client/components/HistoryWrap/HistoryCard/HistoryCard.jsx b/client/components/HistoryWrap/HistoryCard/HistoryCard.jsx new file mode 100644 index 0000000..9dc5109 --- /dev/null +++ b/client/components/HistoryWrap/HistoryCard/HistoryCard.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { getDateFromTimestamp } from '../../../helpers'; + +import './HistoryCard.scss'; + +const HistoryCard = ({ routeName, routeDate, isActive, routeColor }) => ( +
+

+ {routeName}   + {getDateFromTimestamp(routeDate)} + + {isActive && Active} +

+ +
+); + +HistoryCard.defaultProps = { + isActive: false +}; + +HistoryCard.propTypes = { + routeName: PropTypes.string.isRequired, + routeDate: PropTypes.string.isRequired, + isActive: PropTypes.bool, + routeColor: PropTypes.string.isRequired +}; + +export default HistoryCard; diff --git a/client/components/HistoryWrap/HistoryCard/HistoryCard.scss b/client/components/HistoryWrap/HistoryCard/HistoryCard.scss new file mode 100644 index 0000000..2e16bf9 --- /dev/null +++ b/client/components/HistoryWrap/HistoryCard/HistoryCard.scss @@ -0,0 +1,50 @@ +.historyCard__p { + color: #333; + font-size: 15px; + font-weight: 600; + line-height: 1.5em; + + .historyCard__p--date { + font-weight: 400; + opacity: 0.8; + } + + .historyCard__p--is-active { + font-weight: 700; + opacity: 1; + display: inline-block; + margin-left: 15px; + color: #33afe0; + } +} + +.historyCard { + box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1); + border-radius: 2px; + background-color: #fff; + margin-bottom: 5px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 15px 18px 15px 22px; + width: 100%; +} + +.historyCard__btn { + display: inline-block; + padding: 15px 23px 15px; + margin: 0 10px; + border-radius: 3px; + background-color: #33afe0; + color: #fff; + font-size: 12px; + font-weight: 600; + line-height: 1em; + letter-spacing: 0.6px; + cursor: pointer; + transition: background-color 0.15s linear; + + &:hover { + background-color: #252d3a; + } +} diff --git a/client/components/HistoryWrap/HistoryCard/package.json b/client/components/HistoryWrap/HistoryCard/package.json new file mode 100644 index 0000000..9bbdf90 --- /dev/null +++ b/client/components/HistoryWrap/HistoryCard/package.json @@ -0,0 +1,3 @@ +{ + "main": "HistoryCard.jsx" +} \ No newline at end of file diff --git a/client/components/HistoryWrap/HistoryWrap.jsx b/client/components/HistoryWrap/HistoryWrap.jsx new file mode 100644 index 0000000..165135c --- /dev/null +++ b/client/components/HistoryWrap/HistoryWrap.jsx @@ -0,0 +1,35 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './HistoryWrap.scss'; +import HistoryCard from './HistoryCard'; + +class HistoryWrap extends Component { + render() { + const { allHistory } = this.props; + + return ( +
+

+ History +

+
+ {allHistory.map(({ name, date, active, color }, i) => ( + ))} +
+
+ ); + } +} + +HistoryWrap.propTypes = { + allHistory: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default HistoryWrap; diff --git a/client/components/HistoryWrap/HistoryWrap.scss b/client/components/HistoryWrap/HistoryWrap.scss new file mode 100644 index 0000000..8b13041 --- /dev/null +++ b/client/components/HistoryWrap/HistoryWrap.scss @@ -0,0 +1,26 @@ +.all-history__wrap { + background-color: #f7f8fa; + text-align: left; + margin-bottom: 35px; + width: 100%; + border-radius: 3px; + box-shadow: 20px 0 40px rgba(74, 77, 101, 0.2); +} + +.all-history__heading { + position: relative; + color: #252d3a; + font-size: 14px; + line-height: 1em; + font-weight: 600; + margin-bottom: 0; + padding: 32px 125px 31px; + box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1); + border-radius: 3px 3px 0 0; + text-align: center; +} + +.all-history__body { + padding: 8px 10px 10px; + text-align: center; +} diff --git a/client/components/HistoryWrap/package.json b/client/components/HistoryWrap/package.json new file mode 100644 index 0000000..1fa2488 --- /dev/null +++ b/client/components/HistoryWrap/package.json @@ -0,0 +1,3 @@ +{ + "main": "HistoryWrap.jsx" +} \ No newline at end of file diff --git a/client/components/LoginCard/Form/Form.scss b/client/components/LoginCard/Form/Form.scss new file mode 100644 index 0000000..c49e0f6 --- /dev/null +++ b/client/components/LoginCard/Form/Form.scss @@ -0,0 +1,80 @@ +.FormCenter { + margin-bottom: 50px; +} + +.FormField { + margin-bottom: 20px; + + &__Label { + display: block; + text-transform: uppercase; + font-weight: bold; + font-size: 0.9em; + color: #7e7b7b; + } + + &__Input { + width: 85%; + background-color: transparent; + border: none; + color: #252525; + outline: none; + border-bottom: 1px solid #50ff25; + font-size: 1em; + font-weight: 300; + padding-bottom: 10px; + margin-top: 10px; + + &::placeholder { + color: #616e7f; + } + } + + &__Button { + background-color: #4c5768; + color: white; + border: none; + outline: none; + cursor: pointer; + border-radius: 25px; + padding: 15px 70px; + font-size: 0.8em; + font-weight: 500; + } + + &__Link { + color: #0579d8f1; + text-decoration: none; + display: inline-block; + border-bottom: 1.5px solid #225e62; + padding-bottom: 5px; + } + + &__CheckboxLabel { + color: #e3eefd; + font-size: 0.9em; + } + + &__Checkbox { + position: relative; + top: 1.5px; + } + + &__TermsLink { + color: white; + border-bottom: 1px solid #901986; + text-decoration: none; + display: inline-block; + margin-left: 5px; + } + + .helper-text { + color: #ef5350; + font-size: 12px; + } +} + +.helper-text.red-helper-text { + color: #ef5350; + font-size: 12px; +} diff --git a/client/components/LoginCard/Form/SignInForm.jsx b/client/components/LoginCard/Form/SignInForm.jsx new file mode 100644 index 0000000..747a02b --- /dev/null +++ b/client/components/LoginCard/Form/SignInForm.jsx @@ -0,0 +1,104 @@ +import React, { Component } from 'react'; +import { Link, Redirect } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import axios from 'axios'; +import { sha256 } from 'hash.js'; +import './Form.scss'; + +class SignInForm extends Component { + constructor() { + super(); + + this.state = { + email: '', + password: '', + loginSuccess: false, + errorMsg: '' + }; + } + + componentDidMount() { + setTimeout(() => { + if (sessionStorage.getItem('iduser')) { + const { updateIsAuth } = this.props; + updateIsAuth(); + } + }, 1000); + } + + handleChange = ({ target: { value, name } }) => { + this.setState({ [name]: value }); + } + + handleSubmit = (e) => { + e.preventDefault(); + const { updateIsAuth } = this.props; + const { email, password } = this.state; + const passwordHash = sha256().update(password).digest('hex'); + + this.setState({ errorMsg: '' }); + + axios.post('/api/login', { email, passwordHash }) + .then(({ data: { response } }) => { + if (response.iduser) { + sessionStorage.setItem('iduser', response.iduser); + sessionStorage.setItem('role', response.role); + updateIsAuth(); + } else { + this.setState({ errorMsg: `*${response}` }); + } + }); + } + + render() { + const { email, password, loginSuccess, errorMsg } = this.state; + + if (loginSuccess) { + return (); + } + + return ( +
+
+
+ + +
+ +
+ + +
+
+ Create an account +
+
+ {errorMsg && {errorMsg}} +
+ ); + } +} + +SignInForm.propTypes = { + updateIsAuth: PropTypes.func.isRequired +}; + +export default SignInForm; diff --git a/client/components/LoginCard/Form/SignUpForm.jsx b/client/components/LoginCard/Form/SignUpForm.jsx new file mode 100644 index 0000000..578cf7f --- /dev/null +++ b/client/components/LoginCard/Form/SignUpForm.jsx @@ -0,0 +1,128 @@ +import React, { Component } from 'react'; +import { Redirect } from 'react-router-dom'; +import axios from 'axios'; +import { sha256 } from 'hash.js'; +import { toast } from 'materialize-css'; + +class SignUpForm extends Component { + constructor() { + super(); + + this.state = { + email: '', + password: '', + fname: '', + lname: '', + isEmailExist: false, + goLogin: false + }; + } + + handleChange = ({ target: { value, name } }) => { + this.setState({ [name]: value }); + } + + handleSubmit = (e) => { + const { email } = this.state; + e.preventDefault(); + this.setState({ isEmailExist: false }); + + axios.post('api/checkEmailExistence', { email }) + .then(({ data }) => { + if (data === 'emailExist') { + this.setState({ isEmailExist: true }); + } else if (data === 'emailDoNotExist') { + this.registerNewUser(); + } + }) + .catch(err => console.log(err)); + } + + registerNewUser = () => { + const { fname, lname, email, password } = this.state; + const credentials = { fname, lname, email }; + credentials.password = sha256().update(password).digest('hex'); + axios.post('/api/register', credentials) + .then(({ data }) => { + if (data === 'registrationSuccesul') { + toast({ html: 'Please confirm email' }); + this.setState({ goLogin: true }); + } + }) + .catch(err => console.log(err)); + } + + render() { + const { + fname, lname, email, password, isEmailExist, goLogin + } = this.state; + if (goLogin) { + return ; + } + + return ( +
+
+
+ + +
+
+ + +
+
+ + + {isEmailExist && *Email already exists} +
+
+ + +
+
+ +
+
+
+ ); + } +} + +export default SignUpForm; diff --git a/client/components/LoginCard/LoginCard.jsx b/client/components/LoginCard/LoginCard.jsx new file mode 100644 index 0000000..b0c76f6 --- /dev/null +++ b/client/components/LoginCard/LoginCard.jsx @@ -0,0 +1,32 @@ +import React, { Component } from 'react'; +import { Route, Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import './LoginCard.scss'; +import SignUpForm from './Form/SignUpForm'; +import SignInForm from './Form/SignInForm'; + +class LoginCard extends Component { + render() { + const { updateIsAuth } = this.props; + + return ( +
+
+
+
+ Sign In + Sign Up +
+ + } /> +
+
+ ); + } +} + +LoginCard.propTypes = { + updateIsAuth: PropTypes.func.isRequired +}; + +export default LoginCard; diff --git a/client/components/LoginCard/LoginCard.scss b/client/components/LoginCard/LoginCard.scss new file mode 100644 index 0000000..a3538b7 --- /dev/null +++ b/client/components/LoginCard/LoginCard.scss @@ -0,0 +1,54 @@ +.Auth_Card { + display: flex; + color: white; + margin: -10px -15px; + background-image: url('~@images/direction.png'); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + height: calc(100vh - 50px); +} + +.Auth__Aside { + width: 80%; +} + +.Auth__Form { + width: 50%; + background-color: #edf5f4; + padding: 25px 20px; + overflow: auto; +} + +.PageSwitcher { + display: flex; + justify-content: flex-end; + margin-bottom: 10%; + + &__Item { + background-color: #4c5768; + color: #fff; + padding: 10px 25px; + cursor: pointer; + font-size: 0.9em; + border: none; + outline: none; + display: inline-block; + text-decoration: none; + + &:first-child { + border-top-left-radius: 25px; + border-bottom-left-radius: 25px; + } + + &:last-child { + border-top-right-radius: 25px; + border-bottom-right-radius: 25px; + } + + &__Item--Active { + background-color: #5ed0c0; + color: white; + } + } +} diff --git a/client/components/MapDropdown/MapDropdown.jsx b/client/components/MapDropdown/MapDropdown.jsx new file mode 100644 index 0000000..e3c0f60 --- /dev/null +++ b/client/components/MapDropdown/MapDropdown.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './MapDropdown.scss'; + +const MapDropdown = ({ position, calcRouteFn }) => { + const style = position ? { + display: 'block', + opacity: position.show, + top: position.y, + left: position.x + 290 + } : {}; + + return ( + + ); +}; + +MapDropdown.propTypes = { + position: PropTypes.objectOf(PropTypes.any), + calcRouteFn: PropTypes.func.isRequired +}; + +MapDropdown.defaultProps = { + position: undefined +}; + +export default MapDropdown; diff --git a/client/components/MapDropdown/MapDropdown.scss b/client/components/MapDropdown/MapDropdown.scss new file mode 100644 index 0000000..7643c1e --- /dev/null +++ b/client/components/MapDropdown/MapDropdown.scss @@ -0,0 +1,8 @@ +.map-dropdown { + right: auto; + left: auto; + + &_item { + color: #232e3b !important; + } +} diff --git a/client/components/MapDropdown/package.json b/client/components/MapDropdown/package.json new file mode 100644 index 0000000..c315df1 --- /dev/null +++ b/client/components/MapDropdown/package.json @@ -0,0 +1,3 @@ +{ + "main": "MapDropdown.jsx" +} \ No newline at end of file diff --git a/client/components/Menu/Menu.jsx b/client/components/Menu/Menu.jsx new file mode 100644 index 0000000..22c96fa --- /dev/null +++ b/client/components/Menu/Menu.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Navigation from './components/Navigation'; +import './Menu.scss'; + +const Menu = ({ isAuth }) => ( +
+ {isAuth && } +
+); + +Menu.propTypes = { + isAuth: PropTypes.bool.isRequired +}; + +export default Menu; diff --git a/client/components/Menu/Menu.scss b/client/components/Menu/Menu.scss new file mode 100644 index 0000000..fc6f37d --- /dev/null +++ b/client/components/Menu/Menu.scss @@ -0,0 +1,4 @@ +.menu { + background-color: #252d3a; + width: 110px; +} diff --git a/client/components/Menu/components/Navigation/Navigation.jsx b/client/components/Menu/components/Navigation/Navigation.jsx new file mode 100644 index 0000000..d86c295 --- /dev/null +++ b/client/components/Menu/components/Navigation/Navigation.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import './Navigation.scss'; +import AddIco from 'images/add.svg'; +import UserMenuIco from 'images/user-menu.svg'; +import EnvelopeIco from 'images/envelope.svg'; +import DashbordIco from 'images/dashbord.svg'; + +const MENU_ITEM = [ + { name: 'new trip', path: '/new-trip', ico: AddIco }, + { name: 'my profile', path: '/profile', ico: UserMenuIco }, + { name: 'info', path: '/info', ico: EnvelopeIco }, + { name: 'dashboard', path: '/dashboard', ico: DashbordIco } +]; + +const isAdmin = (name) => { + if (name === 'dashboard') { + return sessionStorage.getItem('role') === 'admin' ? null : 'hide'; + } + return null; +}; + +const NavigationItem = ({ name, path, ico }) => ( +
  • + + ico + {name} + +
  • +); + +const Navigation = () => ( + +); + +NavigationItem.propTypes = { + name: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + ico: PropTypes.string.isRequired +}; + +export default Navigation; diff --git a/client/components/Menu/components/Navigation/Navigation.scss b/client/components/Menu/components/Navigation/Navigation.scss new file mode 100644 index 0000000..31e8c7f --- /dev/null +++ b/client/components/Menu/components/Navigation/Navigation.scss @@ -0,0 +1,42 @@ +.navigation { + position: fixed; + z-index: 9999; + height: 100%; + background-color: #252d3a; + top: 50px; + left: 0; + width: 110px; + + ul { + width: 100%; + } + + li { + width: 100%; + } + + &_item { + display: block; + text-align: center; + text-transform: uppercase; + color: #fff; + padding: 15px 5px 20px; + font-size: 11px; + line-height: 1em; + opacity: 0.6; + letter-spacing: 0.82px; + transition: opacity 0.2s linear; + + img { + display: block; + max-width: 100%; + height: auto; + margin: 0 auto 10px; + } + + &:hover, + &.active { + opacity: 1; + } + } +} diff --git a/client/components/Menu/components/Navigation/package.json b/client/components/Menu/components/Navigation/package.json new file mode 100644 index 0000000..af28968 --- /dev/null +++ b/client/components/Menu/components/Navigation/package.json @@ -0,0 +1,3 @@ +{ + "main": "Navigation.jsx" +} \ No newline at end of file diff --git a/client/components/Menu/package.json b/client/components/Menu/package.json new file mode 100644 index 0000000..1c26afa --- /dev/null +++ b/client/components/Menu/package.json @@ -0,0 +1,3 @@ +{ + "main": "Menu.jsx" +} \ No newline at end of file diff --git a/client/components/PersonalInfoCard/PersonalInfoCard.jsx b/client/components/PersonalInfoCard/PersonalInfoCard.jsx new file mode 100644 index 0000000..34c05f6 --- /dev/null +++ b/client/components/PersonalInfoCard/PersonalInfoCard.jsx @@ -0,0 +1,56 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import './PersonalInfoCard.scss'; +import DefaultUserpic from 'images/default-userpic.png'; + +class PersonalInfoCard extends Component { + render() { + const { + settings: { + name: { last, first }, email, userpic = null, rating = 5, kmTraveled = 10, tripsCount = 4 + } + } = this.props; + + return ( +
    +

    Personal Information

    +
    + userpic +

    {`${first} ${last}`}

    +

    {email}

    +
    +
    + {rating && ( +

    + {rating} + out of 5 rating +

    )} + {tripsCount && ( +

    + {tripsCount} + trips +

    )} + {kmTraveled && ( +

    + {kmTraveled} + km travelled +

    )} +
    +
    +
    + ); + } +} + +PersonalInfoCard.propTypes = { + settings: PropTypes.shape({ + name: PropTypes.object, + email: PropTypes.string, + userpic: PropTypes.string, + rating: PropTypes.number, + kmTraveled: PropTypes.number, + tripsCount: PropTypes.number + }).isRequired +}; + +export default PersonalInfoCard; diff --git a/client/components/PersonalInfoCard/PersonalInfoCard.scss b/client/components/PersonalInfoCard/PersonalInfoCard.scss new file mode 100644 index 0000000..08a5a7d --- /dev/null +++ b/client/components/PersonalInfoCard/PersonalInfoCard.scss @@ -0,0 +1,82 @@ +.personal-info__card { + background-color: #f7f8fa; + text-align: left; + margin-bottom: 35px; + width: 100%; + border-radius: 3px; + box-shadow: 20px 0 40px rgba(74, 77, 101, 0.2); +} + +.personal-info__heading { + color: #252d3a; + font-size: 14px; + line-height: 1em; + font-weight: 600; + margin-bottom: 0; + padding: 32px 15px 31px; + box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1); + border-radius: 3px 3px 0 0; + text-align: center; +} + +.personal-info__body { + padding: 20px 15px 10px; + text-align: center; +} + +.personal-info__userpic { + border-radius: 100%; + width: 100%; + max-width: 70px; + display: block; + height: auto; + margin: 0 auto 15px; +} + +.personal-info__name { + color: #252d3a; + font-size: 18px; + font-weight: 600; + line-height: 1.5em; + margin-bottom: 5px; +} + +.personal-info__email { + color: #252d3a; + font-size: 14px; + font-weight: 400; + line-height: 1.5em; + margin-bottom: 0; +} + +.personal-info__divider { + height: 1px; + background-color: rgba(#252d3a, 0.2); + max-width: 450px; + margin: 20px auto 25px; +} + +.personal-info__stats { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + align-items: flex-start; + max-width: 400px; + margin: 0 auto; +} + +.personal-indo__stat { + margin: 0 10px 15px; + color: #252d3a; + font-size: 14px; + font-weight: 400; + line-height: 1.5em; + + span { + display: block; + color: #252d3a; + font-size: 24px; + font-weight: 600; + line-height: 1.2em; + } +} diff --git a/client/components/PersonalInfoCard/package.json b/client/components/PersonalInfoCard/package.json new file mode 100644 index 0000000..c90aa25 --- /dev/null +++ b/client/components/PersonalInfoCard/package.json @@ -0,0 +1,3 @@ +{ + "main": "PersonalInfoCard.jsx" +} \ No newline at end of file diff --git a/client/components/PreLoader/PreLoader.jsx b/client/components/PreLoader/PreLoader.jsx new file mode 100644 index 0000000..287aa3e --- /dev/null +++ b/client/components/PreLoader/PreLoader.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import Loader from 'react-loader-spinner'; +import './PreLoader.scss'; + +const PreLoader = () => ( +
    + +
    +); + +export default PreLoader; diff --git a/client/components/PreLoader/PreLoader.scss b/client/components/PreLoader/PreLoader.scss new file mode 100644 index 0000000..094d1dd --- /dev/null +++ b/client/components/PreLoader/PreLoader.scss @@ -0,0 +1,26 @@ +.pre-loader { + position: absolute; + right: 0; + z-index: 9999; + left: 0; + width: 100%; + height: 100%; + + & > div { + width: 100px; + margin: auto; + margin-top: calc(50vh - 100px); + } + + &::before { + content: ''; + display: block; + position: absolute; + width: calc(100% + 30px); + height: 100%; + background: url('~@images/profile-page-bg.jpg') top; + background-size: cover; + filter: blur(10px); + z-index: -1; + } +} diff --git a/client/components/PreLoader/package.json b/client/components/PreLoader/package.json new file mode 100644 index 0000000..9c287e2 --- /dev/null +++ b/client/components/PreLoader/package.json @@ -0,0 +1,3 @@ +{ + "main": "PreLoader.jsx" +} \ No newline at end of file diff --git a/client/components/ProfileModal/ProfileModal.jsx b/client/components/ProfileModal/ProfileModal.jsx new file mode 100644 index 0000000..02148fb --- /dev/null +++ b/client/components/ProfileModal/ProfileModal.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; + +import './ProfileModal.scss'; + +const ProfileModal = ({ toClose, isOpen, children }) => ( + + {isOpen && } + +); + +ProfileModal.propTypes = { + toClose: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired +}; + +export default ProfileModal; diff --git a/client/components/ProfileModal/ProfileModal.scss b/client/components/ProfileModal/ProfileModal.scss new file mode 100644 index 0000000..211ab11 --- /dev/null +++ b/client/components/ProfileModal/ProfileModal.scss @@ -0,0 +1,20 @@ +#profile-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: -100px; + background-color: rgba(#252d3a, 0.5); + width: 100%; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: 30px 15px 130px; +} + +.profile-modal-inner { + display: flex; + justify-content: center; + width: 100%; +} diff --git a/client/components/ProfileModal/package.json b/client/components/ProfileModal/package.json new file mode 100644 index 0000000..089c854 --- /dev/null +++ b/client/components/ProfileModal/package.json @@ -0,0 +1,3 @@ +{ + "main": "ProfileModal.jsx" +} \ No newline at end of file diff --git a/client/components/RoadView/RoadView.jsx b/client/components/RoadView/RoadView.jsx new file mode 100644 index 0000000..dcacf4f --- /dev/null +++ b/client/components/RoadView/RoadView.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { GoogleApiWrapper, Map } from 'google-maps-react'; + +const KEY = 'AIzaSyBA3gUpJSVxQ3Hu51l3XB7C6fcpObXSQ80'; + +const calculateRoute = (google, startPoint, endPoint) => { + if (google) { + const map = document.getElementById('road-view'); + const directionsService = new google.maps.DirectionsService(); + const directionsDisplay = new google.maps.DirectionsRenderer(); + const start = new google.maps.LatLng(startPoint); + const end = new google.maps.LatLng(endPoint); + const setNewMap = new google.maps.Map(map, { + zoom: 7, + start, + mapTypeControl: false, + streetViewControl: false, + zoomControl: false, + fullscreenControl: false, + draggable: false + }); + directionsDisplay.setMap(setNewMap); + const request = { + origin: start, + destination: end, + travelMode: 'DRIVING' + }; + directionsService.route(request, (result, status) => { + if (status === 'OK') { + directionsDisplay.setDirections(result); + } + }); + } +}; + +const styleBloc = { + minHeight: 'calc(100vh - 50px)' // need to set up height of element +}; + + +// +// For this componet send in props startPoint & endPoint object with lat, lng property +// +const RoadView = ({ google, startPoint, endPoint }) => ( +
    + calculateRoute(google, startPoint, endPoint)} + /> +
    +); + +RoadView.propTypes = { + google: PropTypes.objectOf(PropTypes.any).isRequired, + startPoint: PropTypes.objectOf(PropTypes.any).isRequired, + endPoint: PropTypes.objectOf(PropTypes.any).isRequired +}; + +export default GoogleApiWrapper({ + apiKey: (KEY), + language: 'en' +})(RoadView); diff --git a/client/components/RoadView/package.json b/client/components/RoadView/package.json new file mode 100644 index 0000000..614d673 --- /dev/null +++ b/client/components/RoadView/package.json @@ -0,0 +1,3 @@ +{ + "main": "RoadView.jsx" +} \ No newline at end of file diff --git a/client/components/SearchRouteStart/SearchRouteResult/ActiveRouteCard/ActiveRouteCard.jsx b/client/components/SearchRouteStart/SearchRouteResult/ActiveRouteCard/ActiveRouteCard.jsx new file mode 100644 index 0000000..b51d9a2 --- /dev/null +++ b/client/components/SearchRouteStart/SearchRouteResult/ActiveRouteCard/ActiveRouteCard.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ActiveRouteCard = ({ + routeData: { + name, rating, startPoint, endPoint, date, seats, price, currency + } +}) => ( + +
    +

    {startPoint}
    {endPoint}

    +

    {date}

    +

    {name}, {rating} out of 5

    +
    +
    +

    + {price} + {currency} +

    +

    {seats} seat(s) available

    +
    + +
    +); + +ActiveRouteCard.propTypes = { + routeData: PropTypes.shape({ + name: PropTypes.string, + rating: PropTypes.number, + startPoint: PropTypes.string, + endPoint: PropTypes.string, + date: PropTypes.string, + seats: PropTypes.number, + price: PropTypes.number, + currency: PropTypes.string + }).isRequired +}; + +export default ActiveRouteCard; diff --git a/client/components/SearchRouteStart/SearchRouteResult/ActiveRouteCard/package.json b/client/components/SearchRouteStart/SearchRouteResult/ActiveRouteCard/package.json new file mode 100644 index 0000000..12cd351 --- /dev/null +++ b/client/components/SearchRouteStart/SearchRouteResult/ActiveRouteCard/package.json @@ -0,0 +1,3 @@ +{ + "main": "ActiveRouteCard.jsx" +} diff --git a/client/components/SearchRouteStart/SearchRouteResult/RoutesFilters/RoutesFilters.jsx b/client/components/SearchRouteStart/SearchRouteResult/RoutesFilters/RoutesFilters.jsx new file mode 100644 index 0000000..ca6c946 --- /dev/null +++ b/client/components/SearchRouteStart/SearchRouteResult/RoutesFilters/RoutesFilters.jsx @@ -0,0 +1,67 @@ +import React, { Component } from 'react'; +import { Datepicker } from 'materialize-css'; +import PropTypes from 'prop-types'; + +class RoutersFilters extends Component { + constructor() { + super(); + this.datepicker = React.createRef(); + this.filterValues = { + date: undefined, + passengers: undefined, + minPrice: undefined, + maxPrice: undefined + }; + } + + componentDidMount() { + Datepicker.init(this.datepicker.current, { + format: 'mm/dd/yyyy', onSelect: this.handleDataPickerSelect, autoClose: true + }); + this.timer = null; + } + + handleFilterChange = (e) => { + clearTimeout(this.timer); + const { updateFilterValues } = this.props; + this.filterValues[e.target.name] = e.target.value; + this.timer = setTimeout(() => updateFilterValues(this.filterValues), 500); + } + + handleDataPickerSelect = (date) => { + const { updateFilterValues } = this.props; + const newDate = date ? new Intl.DateTimeFormat('en-US').format(date) : undefined; + this.filterValues.date = newDate; + updateFilterValues(this.filterValues); + } + + render() { + return ( +
    +

    Filters

    +
    + this.handleDataPickerSelect(undefined)} /> + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + ); + } +} + +RoutersFilters.propTypes = { + updateFilterValues: PropTypes.func.isRequired +}; + +export default RoutersFilters; diff --git a/client/components/SearchRouteStart/SearchRouteResult/RoutesFilters/package.json b/client/components/SearchRouteStart/SearchRouteResult/RoutesFilters/package.json new file mode 100644 index 0000000..1b4aa3f --- /dev/null +++ b/client/components/SearchRouteStart/SearchRouteResult/RoutesFilters/package.json @@ -0,0 +1,3 @@ +{ + "main": "RoutesFilters.jsx" +} diff --git a/client/components/SearchRouteStart/SearchRouteResult/SearchRouteResult.jsx b/client/components/SearchRouteStart/SearchRouteResult/SearchRouteResult.jsx new file mode 100644 index 0000000..ceacf98 --- /dev/null +++ b/client/components/SearchRouteStart/SearchRouteResult/SearchRouteResult.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import ActiveRouteCard from './ActiveRouteCard'; +import RoutesFilters from './RoutesFilters'; + +const Spinner = () => ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +); + +const SearchRouteResult = ({ isActive, routesData, updateFilterValues }) => ( +
    + {routesData && } + + {routesData + ? routesData.map((routeData, i) => ) + : isActive &&

    No Routes Found

    } +
    + + {!isActive && } + +
    +); + +SearchRouteResult.defaultProps = { + routesData: null +}; + +SearchRouteResult.propTypes = { + isActive: PropTypes.bool.isRequired, + routesData: PropTypes.arrayOf(PropTypes.object), + updateFilterValues: PropTypes.func.isRequired +}; + +export default SearchRouteResult; diff --git a/client/components/SearchRouteStart/SearchRouteResult/package.json b/client/components/SearchRouteStart/SearchRouteResult/package.json new file mode 100644 index 0000000..7a9f6f0 --- /dev/null +++ b/client/components/SearchRouteStart/SearchRouteResult/package.json @@ -0,0 +1,3 @@ +{ + "main": "SearchRouteResult.jsx" +} diff --git a/client/components/SearchRouteStart/SearchRouteStart.jsx b/client/components/SearchRouteStart/SearchRouteStart.jsx new file mode 100644 index 0000000..1d8aae8 --- /dev/null +++ b/client/components/SearchRouteStart/SearchRouteStart.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Geosuggest from 'react-geosuggest'; + +const SearchRouteStart = ({ setStartPoint, setEndPoint, handleSearchSubmit, isGooleApiLoded }) => ( +
    +

    Where do you want to go?

    +
    +
    +
    + {isGooleApiLoded && } + +
    +
    + {isGooleApiLoded && } + +
    + +
    +
    +
    +); + +SearchRouteStart.propTypes = { + setStartPoint: PropTypes.func.isRequired, + setEndPoint: PropTypes.func.isRequired, + handleSearchSubmit: PropTypes.func.isRequired, + isGooleApiLoded: PropTypes.bool.isRequired +}; + +export default SearchRouteStart; diff --git a/client/components/SearchRouteStart/package.json b/client/components/SearchRouteStart/package.json new file mode 100644 index 0000000..291b7b3 --- /dev/null +++ b/client/components/SearchRouteStart/package.json @@ -0,0 +1,3 @@ +{ + "main": "SearchRouteStart.jsx" +} diff --git a/client/components/Sidebar/Sidebar.jsx b/client/components/Sidebar/Sidebar.jsx new file mode 100644 index 0000000..2327e90 --- /dev/null +++ b/client/components/Sidebar/Sidebar.jsx @@ -0,0 +1,82 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import axios from 'axios'; +import { toast } from 'materialize-css'; +import { Redirect } from 'react-router-dom'; +import './Sidebar.scss'; + +import TripInfo from './components/TripInfo'; +import TripPoint from './components/TripPoint'; + + +class Sidebar extends Component { + constructor(props) { + super(props); + this.state = { + start: 'Enter start point', + end: 'Enter end point' + }; + } + + saveTrip = (data) => { + const id = sessionStorage.getItem('iduser'); + axios.post(`/api/trips/${id}/addTrip`, { data }) + .then(() => { + toast({ html: `Your trip ${data.name} has been added!` }); + setTimeout(() => this.setState({ redirect: true }), 2000); + }) + .catch(() => toast({ html: `Your trip ${data.name} has NOT been added!` })); + } + + render() { + const { tripInfo, changeName, points, changePoint, create, calcRouteFn } = this.props; + const { start, end, redirect } = this.state; + if (redirect) { + return ; + } + return ( +
    +
    + +
    +

    Search distance from route

    + +
    +
    +
    + {!points.length + ? + : points.map(point => ) } + {points.length < 2 && } +
    + {!tripInfo.distance && create && Create} + {tripInfo.distance && this.saveTrip(tripInfo)}>Save} +
    + ); + } +} + +Sidebar.propTypes = { + points: PropTypes.arrayOf(PropTypes.any).isRequired, + tripInfo: PropTypes.objectOf(PropTypes.any), + changeName: PropTypes.func.isRequired, + changePoint: PropTypes.func.isRequired, + create: PropTypes.bool.isRequired, + calcRouteFn: PropTypes.func.isRequired +}; + +Sidebar.defaultProps = { + tripInfo: { + name: 'New Trip', + duration: undefined, + time: undefined, + fuel: undefined, + color: '#fff' + } +}; + +export default Sidebar; diff --git a/client/components/Sidebar/Sidebar.scss b/client/components/Sidebar/Sidebar.scss new file mode 100644 index 0000000..35413a2 --- /dev/null +++ b/client/components/Sidebar/Sidebar.scss @@ -0,0 +1,20 @@ +.sidebar { + min-height: calc(100vh - 50px); + width: 300px; + z-index: 1; + margin: -10px 0 0 -15px; + /* stylelint-disable */ + box-shadow: 7px 0px 35px -1px rgba(122, 118, 122, 1); + /* stylelint-enable */ + + .btn { + width: 100%; + border-radius: 0; + } +} + +.trip-point { + &:not(:last-child) { + border-bottom: none; + } +} diff --git a/client/components/Sidebar/components/TripInfo/TripInfo.jsx b/client/components/Sidebar/components/TripInfo/TripInfo.jsx new file mode 100644 index 0000000..c080e11 --- /dev/null +++ b/client/components/Sidebar/components/TripInfo/TripInfo.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './TripInfo.scss'; + + +const TripInfo = ({ + name, duration, time, fuel, color, onSave +}) => ( +
    +
    {name}
    +
    + {duration} + {time} + $ {fuel} +
    +
    +); + +TripInfo.propTypes = { + name: PropTypes.string.isRequired, + duration: PropTypes.string, + time: PropTypes.string, + fuel: PropTypes.string, + color: PropTypes.string.isRequired, + onSave: PropTypes.func.isRequired +}; + +TripInfo.defaultProps = { + duration: '0 км', + time: '0', + fuel: '0' +}; + +export default TripInfo; diff --git a/client/components/Sidebar/components/TripInfo/TripInfo.scss b/client/components/Sidebar/components/TripInfo/TripInfo.scss new file mode 100644 index 0000000..7771b6c --- /dev/null +++ b/client/components/Sidebar/components/TripInfo/TripInfo.scss @@ -0,0 +1,64 @@ +.trip-info { + background: white; + display: flex; + flex-direction: column; + align-items: center; + + & > * { + padding: 10px; + } + + &_title { + padding-bottom: 0; + outline: none; + } + + &_icon { + width: 100%; + display: flex; + justify-content: space-around; + } + + &_radius { + width: 100%; + background-color: #252d3a; + padding: 10px 15px 0; + text-align: center; + text-transform: uppercase; + + p { + color: white; + opacity: 0.7; + } + + input[type="range"] { + border: none; + color: white; + margin-top: 0; + + &::-webkit-slider-thumb { + background-color: #33afe0; + } + + &::before, + &::after { + display: inline; + opacity: 0.7; + } + + &::before { + content: "-"; + margin-right: 10px; + } + + &::after { + content: "+"; + margin-left: 10px; + } + } + + &_input { + width: 100%; + } + } +} diff --git a/client/components/Sidebar/components/TripInfo/package.json b/client/components/Sidebar/components/TripInfo/package.json new file mode 100644 index 0000000..7f24210 --- /dev/null +++ b/client/components/Sidebar/components/TripInfo/package.json @@ -0,0 +1,3 @@ +{ + "main": "TripInfo.jsx" +} \ No newline at end of file diff --git a/client/components/Sidebar/components/TripPoint/TripPoint.jsx b/client/components/Sidebar/components/TripPoint/TripPoint.jsx new file mode 100644 index 0000000..a95a502 --- /dev/null +++ b/client/components/Sidebar/components/TripPoint/TripPoint.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import Geosuggest from 'react-geosuggest'; +import './TripPoint.scss'; +import PropTypes from 'prop-types'; + +const TripPoint = ({ name, point, onSave }) => ( +
    + {window.google + ? location && onSave(location, point)} autoComplete="off" /> + :

    {name}

    } +
    +); + +TripPoint.propTypes = { + name: PropTypes.string.isRequired, + point: PropTypes.string.isRequired, + onSave: PropTypes.func.isRequired +}; + +export default TripPoint; diff --git a/client/components/Sidebar/components/TripPoint/TripPoint.scss b/client/components/Sidebar/components/TripPoint/TripPoint.scss new file mode 100644 index 0000000..d4a3a4e --- /dev/null +++ b/client/components/Sidebar/components/TripPoint/TripPoint.scss @@ -0,0 +1,31 @@ +.trip-point { + margin: 10px; + + &__wrapper { + padding: 20px 10px; + position: relative; + } + + &__item { + margin-bottom: 10px; + } + + input[type=text]:not(.browser-default) { + font-size: 1.2em; + background-color: transparent; + margin: 0; + border-color: transparent; + + &::placeholder { + color: #252d3a; + } + } + + .geosuggest__suggests { + background: #f7f8fa; + top: calc(100% - 19px); + border-top: 1px; + margin-left: 10px; + margin-right: 10px; + } +} diff --git a/client/components/Sidebar/components/TripPoint/package.json b/client/components/Sidebar/components/TripPoint/package.json new file mode 100644 index 0000000..03cb9e9 --- /dev/null +++ b/client/components/Sidebar/components/TripPoint/package.json @@ -0,0 +1,3 @@ +{ + "main": "TripPoint.jsx" +} \ No newline at end of file diff --git a/client/components/Sidebar/package.json b/client/components/Sidebar/package.json new file mode 100644 index 0000000..08c76c5 --- /dev/null +++ b/client/components/Sidebar/package.json @@ -0,0 +1,3 @@ +{ + "main": "Sidebar.jsx" +} \ No newline at end of file diff --git a/client/components/UserList/UserList.jsx b/client/components/UserList/UserList.jsx new file mode 100644 index 0000000..dcff76d --- /dev/null +++ b/client/components/UserList/UserList.jsx @@ -0,0 +1,103 @@ +import React, { Component } from 'react'; +import axios from 'axios'; +import { toast } from 'materialize-css'; +import ListItem from './components/ListItem'; +import './UserList.scss'; + +class UserList extends Component { + constructor() { + super(); + + this.state = { + userList: [], + initialState: [] + }; + } + + componentDidMount() { + this.getAllUsers(); + } + + getAllUsers = () => { + axios.get('/api/allUsers') + .then(({ data }) => this.setState({ userList: data, initialState: data })); + } + + setUserStatus = (id, status) => { + axios.post('/api/user/unblock', { iduser: id, status }) + .then(() => { + toast({ html: 'Account had been blocked' }); + this.getAllUsers(); + }) + .catch(err => console.log(err)); + } + + deleteUser = (id) => { + axios.post('/api/user/delete', { iduser: id }) + .then(() => { + toast({ html: 'Account had been deleted' }); + this.getAllUsers(); + }) + .catch(err => console.log(err)); + } + + sortListASC = () => { + const { initialState } = this.state; + this.setState({ userList: initialState.sort((a, b) => (a.name.last < b.name.last ? -1 : 1)) }); + }; + + sortListDESC = () => { + const { initialState } = this.state; + this.setState({ userList: initialState.sort((a, b) => (a.name.last > b.name.last ? -1 : 1)) }); + }; + + filterListActive = () => { + const { initialState } = this.state; + this.setState({ userList: initialState.filter(person => person.acount_status === true) }); + }; + + filterListBlock = () => { + const { initialState } = this.state; + this.setState({ userList: initialState.filter(person => person.acount_status === false) }); + }; + + initState = () => { + this.getAllUsers(); + }; + + render() { + const { userList } = this.state; + return ( +
    + {/* Add user */} +
    +
    + Clear filters and sorting +

    Sort alphabetically(by last name):

    + Ascending + Descending +

    Filter by:

    + User is active + User is block +
    +
    + User + Actions +
    +
    + {userList.map((user, i) => ( + + ))} +
    +
    +
    + ); + } +} + +export default UserList; diff --git a/client/components/UserList/UserList.scss b/client/components/UserList/UserList.scss new file mode 100644 index 0000000..60507fc --- /dev/null +++ b/client/components/UserList/UserList.scss @@ -0,0 +1,109 @@ +.userlist { + &__header { + display: flex; + justify-content: space-around; + + span { + min-width: 30%; + } + } + + &__item { + display: flex; + justify-content: space-around; + margin-bottom: 5px; + + & > div { + min-width: 40%; + min-height: 80px; + } + } + + &__content { + max-width: 1024px; + } + + &__info { + display: flex; + align-items: center; + } + + &__img { + margin-right: 30px; + border-radius: 50%; + height: 55px; + overflow: hidden; + } + + &__column { + display: flex; + flex-direction: column; + justify-content: space-around; + + &--margin { + margin-left: -5%; + } + } + + &__actions { + display: flex; + justify-content: flex-end; + align-items: center; + + & > :not(:last-child) { + margin-right: 5px; + } + } + + &__status { + display: flex; + align-items: center; + margin-left: 5px; + margin-top: -7px; + margin-bottom: 14px; + } + + .main-btn { + min-width: 100px; + } + + h5 { + margin: 5px 0; + } +} + +.menu-buttons { + margin-right: 10px; + margin-bottom: 5px; +} + +.options-list { + margin-bottom: 10px; + background-color: #fcfcfd; + display: flex; + padding: 10px; +} + +.filter-title { + margin-left: 10px; + padding-top: 6px; + font-weight: 600; +} + +.filter-button { + display: inline-block; + padding: 10px 15px; + margin: 0 10px; + border-radius: 3px; + background-color: #33afe0; + color: #fff; + font-size: 12px; + font-weight: 600; + line-height: 1em; + letter-spacing: 0.6px; + cursor: pointer; + transition: background-color 0.15s linear; + text-align: center; + vertical-align: middle; +} + diff --git a/client/components/UserList/components/ListItem/ListItem.jsx b/client/components/UserList/components/ListItem/ListItem.jsx new file mode 100644 index 0000000..fd72496 --- /dev/null +++ b/client/components/UserList/components/ListItem/ListItem.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import DefaultUserpic from 'images/default-userpic.png'; + + +const ListItem = ({ + iduser, + avatar, + role, + name: { first, last }, + acount_status: acountStatus, + setUserStatus, + deleteUser +}) => ( + +); + +ListItem.propTypes = { + role: PropTypes.string.isRequired, + iduser: PropTypes.number.isRequired, + setUserStatus: PropTypes.func.isRequired, + deleteUser: PropTypes.func.isRequired, + avatar: PropTypes.string, + name: PropTypes.shape({ + first: PropTypes.string.isRequired, + last: PropTypes.string.isRequired + }).isRequired, + acount_status: PropTypes.bool.isRequired +}; + +ListItem.defaultProps = { + avatar: null +}; + +export default ListItem; diff --git a/client/components/UserList/components/ListItem/package.json b/client/components/UserList/components/ListItem/package.json new file mode 100644 index 0000000..dd0ae58 --- /dev/null +++ b/client/components/UserList/components/ListItem/package.json @@ -0,0 +1,3 @@ +{ + "main": "ListItem.jsx" +} \ No newline at end of file diff --git a/client/components/UserList/package.json b/client/components/UserList/package.json new file mode 100644 index 0000000..ab154b5 --- /dev/null +++ b/client/components/UserList/package.json @@ -0,0 +1,3 @@ +{ + "main": "UserList.jsx" +} \ No newline at end of file diff --git a/client/helpers/index.js b/client/helpers/index.js new file mode 100644 index 0000000..ba1dd24 --- /dev/null +++ b/client/helpers/index.js @@ -0,0 +1,19 @@ +export function random(min, max) { return Math.round(min + Math.random() * (max - min)); } +export const colors = [ + 'red', + 'pink', + 'purple', + 'blue', + 'teal', + 'light-green', + 'lime', + 'orange' +]; +export function getDateFromTimestamp(string) { + return string.split('') + .splice(0, 10) + .join('') + .split('-') + .reverse() + .join('/'); +} diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..c3e33a8 --- /dev/null +++ b/client/index.js @@ -0,0 +1,18 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter } from 'react-router-dom'; +import axios from 'axios'; +import App from './App'; +import 'materialize-css'; +import './index.scss'; + +axios.defaults.baseURL = 'http://localhost:3000'; + +ReactDOM.render( + + + , + document.getElementById('app') +); + +module.hot.accept(); // Used for React Hot Module Replacement diff --git a/client/index.scss b/client/index.scss new file mode 100644 index 0000000..ad1dd3e --- /dev/null +++ b/client/index.scss @@ -0,0 +1,27 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +// What alternative for this +html, +body, +#app, +.menu, +.main { + min-height: calc(100vh - 50px); +} + +a { + text-decoration: none; +} + +img { + height: 100%; + flex-shrink: 0; +} + +ul { + list-style: none; +} diff --git a/client/pages/Dashbord/Dashbord.jsx b/client/pages/Dashbord/Dashbord.jsx new file mode 100644 index 0000000..3d9bb83 --- /dev/null +++ b/client/pages/Dashbord/Dashbord.jsx @@ -0,0 +1,15 @@ +import React, { Fragment } from 'react'; +import { Redirect } from 'react-router-dom'; +import UserList from '../../components/UserList'; + +const Dashbord = () => ( + sessionStorage.getItem('role') === 'admin' + ? ( + +

    Dashbord

    + +
    ) + : +); + +export default Dashbord; diff --git a/client/pages/Dashbord/package.json b/client/pages/Dashbord/package.json new file mode 100644 index 0000000..2892496 --- /dev/null +++ b/client/pages/Dashbord/package.json @@ -0,0 +1,3 @@ +{ + "main": "Dashbord.jsx" +} \ No newline at end of file diff --git a/client/pages/Info/Info.jsx b/client/pages/Info/Info.jsx new file mode 100644 index 0000000..7df721a --- /dev/null +++ b/client/pages/Info/Info.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import './Info.scss'; + + +const Info = () => ( +
    +
    +

    Here will be map

    +
    +
    +

    Contacts

    +

    Our phone 66-666-666

    +

    Our email ad@666.gmail.com

    +
    +
    +
    +
    +
    +