(
+ 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 && (
+
+ )}
+
+
+ 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 (
+
+ );
+ }
+}
+
+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 }) => (
+
+);
+
+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 = () => (
+
+
+
+);
+
+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 (
+
+
+ {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 (
+
+ );
+ }
+}
+
+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 }) => (
+
+
+
+ {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
+
+

+
{`${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 && {children}
}
+
+);
+
+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 }) => (
+
+
+);
+
+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 (
+
+ );
+ }
+}
+
+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?
+
+
+);
+
+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 */}
+
+
+
+ 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
+}) => (
+
+
+
+

+
+
+
{`${first} ${last}`}
+
+
+
+
+);
+
+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 = () => (
+
+
+
+
Contacts
+
Our phone 66-666-666
+
Our email ad@666.gmail.com
+
+
+
+
+);
+
+export default Info;
diff --git a/client/pages/Info/Info.scss b/client/pages/Info/Info.scss
new file mode 100644
index 0000000..1b0ce62
--- /dev/null
+++ b/client/pages/Info/Info.scss
@@ -0,0 +1,62 @@
+
+.main-info {
+ background-color: #f7f8fa;
+ display: flex;
+ flex-direction: column;
+}
+
+.Map {
+ width: 80%;
+ height: 300px;
+ border: 1px, solid, black;
+ margin: 0 auto;
+ background-color: lightblue;
+}
+
+.contacts {
+ text-align: center;
+}
+
+.form {
+ width: 50%;
+ margin: 0 auto;
+ text-align: center;
+}
+
+footer {
+ margin-top: 50px;
+ height: 100px;
+ width: 100%;
+ background-color: rgb(140, 188, 243);
+ display: flex;
+ justify-content: center;
+ flex-wrap: nowrap;
+}
+
+.copyright {
+ margin-top: 40px;
+ width: 30%;
+}
+
+.footer-menu {
+ margin-top: 40px;
+ width: 30%;
+
+ ul {
+ display: flex;
+ flex-direction: row;
+
+ li {
+ margin-right: 30px;
+ margin-left: 60px;
+ padding: 5px;
+ }
+
+ li:hover {
+ background-color: chocolate;
+ padding: 5px;
+ transition: 0.2s;
+ }
+ }
+}
+
diff --git a/client/pages/Info/package.json b/client/pages/Info/package.json
new file mode 100644
index 0000000..657679f
--- /dev/null
+++ b/client/pages/Info/package.json
@@ -0,0 +1,3 @@
+{
+ "main": "Info.jsx"
+}
\ No newline at end of file
diff --git a/client/pages/Login/Login.jsx b/client/pages/Login/Login.jsx
new file mode 100644
index 0000000..798a9ea
--- /dev/null
+++ b/client/pages/Login/Login.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Redirect } from 'react-router-dom';
+import LoginCard from '../../components/LoginCard/LoginCard';
+
+const Login = ({ updateIsAuth, isAuth }) => (
+ isAuth ? :
+);
+
+Login.propTypes = {
+ updateIsAuth: PropTypes.func.isRequired,
+ isAuth: PropTypes.bool.isRequired
+};
+
+export default Login;
diff --git a/client/pages/Login/package.json b/client/pages/Login/package.json
new file mode 100644
index 0000000..53f2f06
--- /dev/null
+++ b/client/pages/Login/package.json
@@ -0,0 +1,3 @@
+{
+ "main": "Login.jsx"
+}
\ No newline at end of file
diff --git a/client/pages/NewTrip/NewTrip.jsx b/client/pages/NewTrip/NewTrip.jsx
new file mode 100644
index 0000000..cf797fb
--- /dev/null
+++ b/client/pages/NewTrip/NewTrip.jsx
@@ -0,0 +1,236 @@
+import React, { Component } from 'react';
+import { Map, Marker } from 'google-maps-react';
+import { toast } from 'materialize-css';
+import SideBar from '../../components/Sidebar';
+import PreLoader from '../../components/PreLoader';
+import { random, colors } from '../../helpers';
+import './NewTrip.scss';
+
+class NewTrip extends Component {
+ constructor() {
+ super();
+ this.state = {
+ load: true,
+ markers: [],
+ defaultZoom: 11,
+ tripInfo: {
+ name: 'New Trip',
+ color: colors[random(0, colors.length - 1)]
+ }
+ };
+ this.FLAGS = {
+ A: 'A',
+ B: 'B'
+ };
+ this.MAX_POINT_NUMBERS = 2;
+ }
+
+ componentDidMount() {
+ this.getLocation()
+ .then(({ coords }) => this.setState({
+ location: {
+ lat: coords.latitude,
+ lng: coords.longitude
+ }
+ }));
+ }
+
+ componentDidUpdate() {
+ const { location, markers } = this.state;
+ if (!markers.length && location) {
+ this.addMarkers(location);
+ }
+ }
+
+ getLocation = () => {
+ const { geolocation } = navigator;
+ const location = new Promise((resolve, reject) => {
+ if (!geolocation) {
+ reject(new Error('Not Supported'));
+ }
+ geolocation.getCurrentPosition((position) => {
+ resolve(position);
+ }, () => {
+ toast({ html: 'Your geolocation is desible
Please enter your start point' });
+ });
+ });
+ return location;
+ };
+
+ setPoints = (latlng, point) => {
+ const { A, B } = this.FLAGS;
+ if (point === A) {
+ this.setState({
+ location: {
+ lat: latlng.lat,
+ lng: latlng.lng
+ }
+ });
+ }
+ if (point === B) {
+ this.setState({
+ end: {
+ lat: latlng.lat,
+ lng: latlng.lng
+ }
+ });
+ }
+ }
+
+ getParsetNameFromGeocode = (data) => {
+ const res = [];
+ Object.values(data.address_components).map((item) => {
+ switch (item.types[0]) {
+ case 'route':
+ if (item.long_name === 'Unnamed Road') {
+ res.toString();
+ break;
+ }
+ res.push(item.long_name);
+ break;
+ case 'locality':
+ res.push(item.long_name);
+ break;
+ case 'administrative_area_level_2':
+ res.push(item.long_name);
+ break;
+ case 'administrative_area_level_1':
+ res.push(item.long_name);
+ break;
+ default: res.toString();
+ }
+ return item;
+ });
+ return res.join(', ');
+ }
+
+ getPointName = (startPoint) => {
+ const name = new Promise((resolve, reject) => {
+ if (!window.google) {
+ reject(new Error('API is undefined'));
+ }
+ const geocoder = new window.google.maps.Geocoder();
+ geocoder.geocode({ location: startPoint }, res => resolve(
+ this.getParsetNameFromGeocode(res[0])
+ ));
+ });
+ return name;
+ }
+
+ addMarkers = (latLng, pointFlag) => {
+ const { markers } = this.state;
+ const { A, B } = this.FLAGS;
+ let flag = !markers.length ? A : B;
+ if (pointFlag) {
+ flag = pointFlag;
+ this.setPoints(latLng, pointFlag);
+ }
+ if (pointFlag === A) {
+ this.getPointName(latLng)
+ .then(res => this.setState(prevState => prevState.markers.splice(0, 1, {
+ name: res,
+ title: res,
+ position: latLng,
+ point: flag
+ })));
+ } else {
+ if (markers.length > 1) {
+ this.setState(prevState => prevState.markers.pop());
+ }
+ this.getPointName(latLng)
+ .then(res => this.setState(prevState => prevState.markers.push({
+ name: res,
+ title: res,
+ position: latLng,
+ point: flag
+ })));
+ }
+ }
+
+ calculateRoute = () => {
+ this.eventLoader();
+ if (window.google) {
+ const { location, end } = this.state;
+ const map = document.getElementById('map');
+ const directionsService = new window.google.maps.DirectionsService();
+ const directionsDisplay = new window.google.maps.DirectionsRenderer();
+ const startPoint = new window.google.maps.LatLng(location);
+ const endPoint = new window.google.maps.LatLng(end);
+ const setNewMap = new window.google.maps.Map(map, {
+ zoom: 7,
+ startPoint
+ });
+ directionsDisplay.setMap(setNewMap);
+ const request = {
+ origin: startPoint,
+ destination: endPoint,
+ travelMode: 'DRIVING'
+ };
+ directionsService.route(request, (result, status) => {
+ if (status === 'OK') {
+ directionsDisplay.setDirections(result);
+ this.setState(prevState => ({
+ tripInfo: {
+ ...prevState.tripInfo,
+ duration: result.routes[0].legs[0].distance.text,
+ time: result.routes[0].legs[0].duration.text,
+ start_address: result.routes[0].legs[0].start_address,
+ end_address: result.routes[0].legs[0].end_address,
+ distance: {
+ start: prevState.location,
+ end: prevState.end
+ }
+ },
+ load: false
+ }));
+ }
+ });
+ }
+ }
+
+ eventLoader = () => {
+ this.setState(prevState => ({ load: !prevState.load }));
+ }
+
+ eventChangeName = ({ currentTarget: { textContent } }) => {
+ this.setState(prevState => ({
+ tripInfo: {
+ ...prevState.tripInfo,
+ name: textContent
+ }
+ }));
+ }
+
+
+ render() {
+ const { load, markers, location, defaultZoom, tripInfo } = this.state;
+ return (
+
+
+ {load &&
}
+
+
+
+
+ );
+ }
+}
+
+
+export default NewTrip;
diff --git a/client/pages/NewTrip/NewTrip.scss b/client/pages/NewTrip/NewTrip.scss
new file mode 100644
index 0000000..528b8c3
--- /dev/null
+++ b/client/pages/NewTrip/NewTrip.scss
@@ -0,0 +1,10 @@
+.new-trip {
+ display: flex;
+ position: relative;
+
+ .pre-loader {
+ left: 286px;
+ margin-top: -10px;
+ width: calc(100% - 300px);
+ }
+}
diff --git a/client/pages/NewTrip/package.json b/client/pages/NewTrip/package.json
new file mode 100644
index 0000000..b71930c
--- /dev/null
+++ b/client/pages/NewTrip/package.json
@@ -0,0 +1,3 @@
+{
+ "main": "NewTrip.jsx"
+}
\ No newline at end of file
diff --git a/client/pages/Profile/Profile.jsx b/client/pages/Profile/Profile.jsx
new file mode 100644
index 0000000..f84598b
--- /dev/null
+++ b/client/pages/Profile/Profile.jsx
@@ -0,0 +1,78 @@
+import React, { Component } from 'react';
+import axios from 'axios';
+import './Profile.scss';
+import CarsCard from '../../components/CarsCard';
+import PersonalInfoCard from '../../components/PersonalInfoCard';
+import FeedbacksCard from '../../components/FeedbacksCard';
+import HistoryWrap from '../../components/HistoryWrap';
+
+class Profile extends Component {
+ constructor() {
+ super();
+
+ this.state = {
+ userInfo: null,
+ carsInfo: [],
+ feedbacksInfo: [],
+ allHistory: []
+ };
+ this.USER_ID = sessionStorage.getItem('iduser');
+ }
+
+ componentDidMount() {
+ this.fetchFeedbacksData();
+ this.fetchUserData();
+ this.updateCarData();
+ this.fetchHistoryData();
+ }
+
+ fetchFeedbacksData = () => {
+ axios.get(`/api/feedbacks/${this.USER_ID}`)
+ .then(({ data }) => this.setState({ feedbacksInfo: data }))
+ .catch(e => console.error(e));
+ }
+
+ fetchHistoryData = () => {
+ axios.get(`/api/trips/${this.USER_ID}/byId`)
+ .then(({ data }) => this.setState({ allHistory: data }))
+ .catch(e => console.error(e));
+ }
+
+ fetchUserData = () => {
+ axios.get(`api/user/${this.USER_ID}`)
+ .then(({ data }) => this.setState({ userInfo: data[0] }))
+ .catch(e => console.error(e));
+ }
+
+ updateCarData = () => {
+ axios.get(`api/user/${this.USER_ID}/cars`)
+ .then(({ data }) => this.setState({ carsInfo: data }))
+ .catch(e => console.error(e));
+ }
+
+ render() {
+ const {
+ userInfo, carsInfo, feedbacksInfo, allHistory
+ } = this.state;
+
+ return (
+
+ );
+ }
+}
+
+export default Profile;
diff --git a/client/pages/Profile/Profile.scss b/client/pages/Profile/Profile.scss
new file mode 100644
index 0000000..2b98ff8
--- /dev/null
+++ b/client/pages/Profile/Profile.scss
@@ -0,0 +1,89 @@
+.profile {
+ min-height: calc(100vh - 50px);
+ margin: -10px -15px;
+ padding-bottom: 40px;
+ text-align: center;
+ background: url('~@images/profile-page-bg.jpg') top;
+ background-size: cover;
+}
+
+.toast {
+ background-color: #252d3a;
+}
+
+.profile__header {
+ color: #252d3a;
+ font-size: 30px;
+ font-weight: 600;
+ line-height: 1em;
+ padding: 40px 15px 30px;
+ background-color: rgba(#fff, 0.5);
+ margin-bottom: 20px;
+ margin-top: 0;
+}
+
+.profile-content-wrap {
+ margin-right: auto;
+ margin-left: auto;
+ padding-right: 15px;
+ padding-left: 15px;
+ width: 100%;
+ max-width: 1240px;
+}
+
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: -15px;
+ margin-left: -15px;
+}
+
+.col-12,
+.col-md-6 {
+ position: relative;
+ width: 100%;
+ min-height: 1px;
+ padding-right: 15px;
+ padding-left: 15px;
+}
+
+.col-12 {
+ flex: 0 0 100%;
+ max-width: 100%;
+}
+
+@media (min-width: 992px) {
+ .col-md-6 {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+}
+
+.deleteCar-modal {
+ background-color: #f7f8fa;
+ text-align: left;
+ margin-bottom: 35px;
+ width: 100%;
+ max-width: 400px;
+ border-radius: 3px;
+ box-shadow: 20px 0 40px rgba(74, 77, 101, 0.2);
+}
+
+.deleteCar-heading {
+ position: relative;
+ color: #252d3a;
+ font-size: 14px;
+ line-height: 1em;
+ font-weight: 600;
+ margin-bottom: 0;
+ padding: 32px 20px 31px;
+ box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1);
+ border-radius: 3px 3px 0 0;
+ text-align: center;
+}
+
+.deleteCar-body {
+ padding: 8px 10px 10px;
+ text-align: center;
+ overflow: hidden;
+}
diff --git a/client/pages/Profile/package.json b/client/pages/Profile/package.json
new file mode 100644
index 0000000..bd4edd6
--- /dev/null
+++ b/client/pages/Profile/package.json
@@ -0,0 +1,3 @@
+{
+ "main": "Profile.jsx"
+}
\ No newline at end of file
diff --git a/client/pages/SearchRoute/SearchRoute.jsx b/client/pages/SearchRoute/SearchRoute.jsx
new file mode 100644
index 0000000..5df2845
--- /dev/null
+++ b/client/pages/SearchRoute/SearchRoute.jsx
@@ -0,0 +1,170 @@
+import React, { Component } from 'react';
+import { toast } from 'materialize-css';
+import axios from 'axios';
+
+import './SearchRoute.scss';
+import SearchRouteStart from '../../components/SearchRouteStart';
+import SearchRouteResult from '../../components/SearchRouteStart/SearchRouteResult';
+
+const MissingData = () => (
+ {
+ rating: Number.parseFloat((Math.random() * 2).toFixed(1)) + 3,
+ date: `${Math.ceil(Math.random() * 12)}/${Math.ceil(Math.random() * 28)}/2018`,
+ seats: Math.ceil(Math.random() * 5),
+ price: Math.ceil(Math.random() * 10) * 50,
+ currency: 'UAH'
+ }
+);
+
+class SearchRoute extends Component {
+ constructor() {
+ super();
+ this.state = {
+ isGooleApiLoded: false,
+ searchingStageTwo: false,
+ searchResultIsReady: false,
+ searchedRouteData: null,
+ filterValues: {
+ date: undefined,
+ passengers: undefined,
+ minPrice: undefined,
+ maxPrice: undefined
+ }
+ };
+ this.dataToFilter = null;
+ this.searchPoints = { startPoint: null, endPoint: null };
+ }
+
+ componentDidMount() {
+ if (window.google) this.setState({ isGooleApiLoded: true });
+ }
+
+ setStartPoint = ({ location = null } = {}) => {
+ this.searchPoints.startPoint = location;
+ }
+
+ setEndPoint = ({ location = null } = {}) => {
+ this.searchPoints.endPoint = location;
+ }
+
+ updateFilterValues = ({
+ date = undefined, passengers = undefined, minPrice = undefined, maxPrice = undefined
+ }) => {
+ console.log(date, passengers, minPrice, maxPrice);
+ this.setState({
+ filterValues: {
+ date, passengers, minPrice, maxPrice
+ }
+ });
+ setTimeout(() => this.filterData(), 0);
+ }
+
+ handleSearchSubmit = (e) => {
+ e.preventDefault();
+ const { startPoint, endPoint } = this.searchPoints;
+ return (!startPoint && !endPoint)
+ ? toast({ html: 'Please choose at least one place' })
+ : this.handleRouteSearch();
+ }
+
+ handleRouteSearch = () => {
+ this.setState({ searchingStageTwo: true, searchResultIsReady: false });
+ setTimeout(() => this.fetchLocalStorageData(), 1000);
+ }
+
+ isNear = (filterVal, dbVal) => (
+ Math.abs(Number.parseFloat(filterVal) - Number.parseFloat(dbVal)).toFixed(1) <= 0.1
+ )
+
+ fetchLocalStorageData = () => {
+ const { startPoint, endPoint } = this.searchPoints;
+ axios.get('api/trips/all')
+ .then(({ data }) => {
+ const RoutesToReturn = data
+ .filter((route) => {
+ const {
+ start: { lat: startLat, lng: startLng },
+ end: { lat: endLat, lng: endLng }
+ } = route.distance;
+
+ const shouldReturnStart = startPoint
+ ? (this.isNear(startPoint.lat, startLat) && this.isNear(startPoint.lng, startLng))
+ : true;
+
+ const shouldReturnEnd = endPoint
+ ? (this.isNear(endPoint.lat, endLat) && this.isNear(endPoint.lng, endLng))
+ : true;
+
+ return (shouldReturnStart && shouldReturnEnd);
+ })
+ .map(({ start_address: startAddress, end_address: endAddress, username }) => {
+ const missingData = MissingData();
+ missingData.name = `${username.first} ${username.last}`;
+ missingData.startPoint = startAddress;
+ missingData.endPoint = endAddress;
+ return missingData;
+ });
+ this.dataToFilter = RoutesToReturn;
+ this.filterData();
+ })
+ .catch((e) => {
+ this.setState({ searchedRouteData: null, searchResultIsReady: true });
+ toast({ html: 'Bad connection, please try again later!' });
+ console.error(e);
+ });
+ }
+
+ fetchRoutesData = () => {
+ axios.get('public/data/searchResultOfPublishedRoutes.json')
+ .then(({ data }) => {
+ this.dataToFilter = data;
+ this.filterData();
+ });
+ }
+
+ filterData = () => {
+ const {
+ filterValues: {
+ date, passengers, minPrice, maxPrice
+ }
+ } = this.state;
+ let filteredData = this.dataToFilter;
+
+ if (date) filteredData = filteredData.filter(obj => obj.date === date);
+ if (passengers) filteredData = filteredData.filter(obj => obj.seats >= passengers);
+ if (minPrice) filteredData = filteredData.filter(obj => obj.price >= minPrice);
+ if (maxPrice) filteredData = filteredData.filter(obj => obj.price <= maxPrice);
+ this.setState({ searchedRouteData: filteredData, searchResultIsReady: true });
+ }
+
+ render() {
+ const {
+ searchingStageTwo, searchResultIsReady, searchedRouteData, isGooleApiLoded
+ } = this.state;
+
+ return (
+
+
Find Your Trip
+
+
+
+ {searchingStageTwo && (
+
+ )}
+
+
+
+ );
+ }
+}
+
+export default SearchRoute;
diff --git a/client/pages/SearchRoute/SearchRoute.scss b/client/pages/SearchRoute/SearchRoute.scss
new file mode 100644
index 0000000..d99df1a
--- /dev/null
+++ b/client/pages/SearchRoute/SearchRoute.scss
@@ -0,0 +1,331 @@
+.search-route {
+ min-height: calc(100vh - 50px);
+ margin: -10px -15px;
+ padding-bottom: 40px;
+ text-align: center;
+ background: url('~@images/profile-page-bg.jpg') top;
+ background-size: cover;
+
+ &__header {
+ color: #252d3a;
+ font-size: 30px;
+ font-weight: 600;
+ line-height: 1em;
+ padding: 40px 15px 30px;
+ background-color: rgba(#fff, 0.5);
+ margin-bottom: 20px;
+ margin-top: 0;
+ }
+}
+
+.sr-wrap {
+ margin-right: auto;
+ margin-left: auto;
+ width: 100%;
+ padding: 0 15px;
+ max-width: 1240px;
+}
+
+.search-route-start {
+ box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1);
+ border-radius: 3px 3px 0 0;
+ background-color: #fff;
+ padding: 55px 15px;
+}
+
+.sr-inner {
+ box-shadow: 20px 0 40px rgba(74, 77, 101, 0.2);
+ border-radius: 3px;
+ background-color: #f7f8fa;
+}
+
+.srs-h {
+ color: #252d3a;
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 35px;
+}
+
+.sr-form-wrap {
+ max-width: 680px;
+ margin: 0 auto;
+ padding: 0 15px;
+}
+
+.search-start__btn-submit {
+ display: inline-block;
+ padding: 15px 40px 14px;
+ 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;
+ }
+}
+
+.search-start__col {
+ width: calc(50% - 30px);
+ margin-right: 15px;
+ margin-left: 15px;
+ margin-bottom: 20px;
+}
+
+.geosuggest__suggests {
+ position: absolute;
+ top: calc(100% - 4px);
+ left: 0;
+ right: 0;
+ max-height: 25em;
+ padding: 0;
+ margin-top: -1px;
+ background: #fff;
+ border: 1px solid #33afe0;
+ border-top-width: 0;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ list-style: none;
+ z-index: 5;
+ -webkit-transition: max-height 0.2s, border 0.2s;
+ transition: max-height 0.2s, border 0.2s;
+}
+
+.geosuggest__suggests--hidden {
+ max-height: 0;
+ overflow: hidden;
+ border-width: 0;
+}
+
+.geosuggest__item {
+ font-size: 18px;
+ font-size: 1rem;
+ padding: 0.5em 0.65em;
+ cursor: pointer;
+ text-align: left;
+ transition: bachground-color 0.2s ease-in-out;
+}
+
+.geosuggest__item:hover,
+.geosuggest__item:focus {
+ background-color: #f5f5f5;
+}
+
+.geosuggest__item--active {
+ background-color: #33afe0;
+ color: #fff;
+}
+
+.geosuggest__item--active:hover,
+.geosuggest__item--active:focus {
+ background-color: #ccc;
+}
+
+.geosuggest__item__matched-text {
+ font-weight: bold;
+}
+
+.spinner-mainBlue {
+ border-color: #33afe0;
+}
+
+.search-route-result {
+ position: relative;
+ min-height: 150px;
+ padding: 30px 15px;
+}
+
+.absolute-spinner-wrap {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background-color: rgba(#f7f8fa, 0.9);
+}
+
+.absolute-spinner {
+ position: absolute;
+ top: 50px;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.route__userpic {
+ border-radius: 100%;
+ width: 70px;
+ max-width: 70px;
+ display: block;
+ height: auto;
+ margin: 0 15px;
+}
+
+.datepicker-date-display {
+ background-color: #252d3a;
+}
+
+.datepicker-table td.is-selected {
+ background-color: #33afe0;
+}
+
+.datepicker-day-button:focus {
+ background-color: rgba(#33afe0, 0.25);
+}
+
+.modal-overlay {
+ background-color: #252d3a;
+}
+
+.route-card-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 50%;
+ right: 90px;
+ transform: translate(0, -50%);
+ width: 50px;
+ height: 50px;
+ border-radius: 3px;
+ background-color: #33afe0;
+ transition: transform 0.2s ease-in-out;
+
+ &::before {
+ content: '';
+ display: block;
+ width: 8px;
+ height: 14px;
+ background: url('~@images/arrow-right.svg') center;
+ }
+}
+
+.active-route-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ max-width: 850px;
+ box-shadow: 0 20px 40px rgba(89, 100, 191, 0.1);
+ border-radius: 2px;
+ background-color: #fff;
+ margin: 0 auto 5px;
+ padding: 32px 150px 32px 90px;
+ position: relative;
+ text-align: left;
+
+ &:hover {
+ .route-card-btn {
+ transform: translate(5px, -50%);
+ }
+ }
+}
+
+.route-card-inner1 {
+ width: calc(100% - 150px);
+}
+
+.route-card-inner2 {
+ width: 150px;
+}
+
+.route-card-points {
+ color: #333;
+ font-size: 13px;
+ font-weight: 700;
+ line-height: 1.5em;
+ margin-bottom: 10px;
+}
+
+.route-card-date {
+ color: #333;
+ font-size: 15px;
+ font-weight: 600;
+ line-height: 1.4em;
+ margin-bottom: 0;
+}
+
+.route-card-name {
+ color: #252d3a;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 1.2em;
+}
+
+.route-card-rating {
+ color: #33afe0;
+ font-size: 22px;
+ font-weight: 600;
+ line-height: 1.5em;
+ letter-spacing: 1px;
+}
+
+.route-card-sec {
+ color: rgba(#252d3a, 0.8);
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 1.2;
+}
+
+.route-card-price {
+ display: inline-block;
+ margin-right: 5px;
+ color: #33afe0;
+ font-size: 30px;
+ font-weight: 600;
+ line-height: 1.1em;
+ letter-spacing: 1.5px;
+}
+
+.route-card-currency {
+ color: #333;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.5em;
+}
+
+.route-card-seats {
+ color: #333;
+ font-size: 15px;
+ font-weight: 600;
+ line-height: 1.5em;
+}
+
+.mb5 {
+ margin-bottom: 5px;
+}
+
+.filter-card {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 10px 15px;
+ max-width: 850px;
+ margin: 0 auto 30px;
+ position: relative;
+ text-align: left;
+}
+
+.filter-card-p {
+ width: 100%;
+ color: #252d3a;
+ font-size: 14px;
+ line-height: 1.2em;
+ font-weight: 600;
+ padding-bottom: 15px;
+ padding-left: 15px;
+}
+
+.filter-card-input-wrap {
+ width: calc(25% - 30px);
+ margin: 15px;
+
+ @media (max-width: 767px) {
+ width: 100%;
+ }
+}
diff --git a/client/pages/SearchRoute/package.json b/client/pages/SearchRoute/package.json
new file mode 100644
index 0000000..75b52ba
--- /dev/null
+++ b/client/pages/SearchRoute/package.json
@@ -0,0 +1,3 @@
+{
+ "main": "SearchRoute.jsx"
+}
diff --git a/client/routes/index.js b/client/routes/index.js
new file mode 100644
index 0000000..ed7ff59
--- /dev/null
+++ b/client/routes/index.js
@@ -0,0 +1,32 @@
+import Info from '../pages/Info';
+import Profile from '../pages/Profile';
+import Dashboard from '../pages/Dashbord';
+import SearchRoute from '../pages/SearchRoute';
+import NewTrip from '../pages/NewTrip';
+
+
+const Routes = [
+ {
+ path: '/',
+ exact: true,
+ component: SearchRoute
+ },
+ {
+ path: '/profile',
+ component: Profile
+ },
+ {
+ path: '/info',
+ component: Info
+ },
+ {
+ path: '/dashboard',
+ component: Dashboard
+ },
+ {
+ path: '/new-trip',
+ component: NewTrip
+ }
+];
+
+export default Routes;
diff --git a/config/paths.js b/config/paths.js
new file mode 100644
index 0000000..a60ac76
--- /dev/null
+++ b/config/paths.js
@@ -0,0 +1,17 @@
+const fs = require('fs');
+const path = require('path');
+
+
+const appDirectory = fs.realpathSync(process.cwd());
+const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
+
+
+module.exports = {
+ root: appDirectory,
+ appBuild: resolveApp('build'),
+ appHtml: resolveApp('public/index.html'),
+ appIndexJs: resolveApp('client/index.js'),
+ appSrc: resolveApp('client'),
+ appAssets: resolveApp('public'),
+ globalImages: resolveApp('public/images')
+};
diff --git a/config/webpack.common.js b/config/webpack.common.js
new file mode 100644
index 0000000..c9d11cc
--- /dev/null
+++ b/config/webpack.common.js
@@ -0,0 +1,105 @@
+const paths = require('./paths');
+const webpack = require('webpack');
+const autoprefixer = require('autoprefixer');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const styleLintPlugin = require('stylelint-webpack-plugin');
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+
+
+module.exports = {
+ entry: paths.appIndexJs,
+ output: {
+ path: paths.appBuild,
+ filename: 'bundle.js',
+ publicPath: '/'
+ },
+ module: {
+ rules: [
+ {
+ enforce: 'pre',
+ test: /\.(js|jsx)$/,
+ exclude: /node_modules/,
+ loader: require.resolve('eslint-loader'),
+ options: {
+ failOnWarning: true,
+ failOnError: true,
+ }
+ },
+ {
+ test: /\.(js|jsx)$/,
+ exclude: /node_modules/,
+ use: {
+ loader: require.resolve('babel-loader'),
+ options: {
+ presets: ['env', 'react', 'stage-2']
+ }
+ }
+ },
+ {
+ test: /\.(scss)$/,
+ use: [
+ require.resolve('style-loader'),
+ require.resolve('css-hot-loader'),
+ MiniCssExtractPlugin.loader,
+ {
+ loader: require.resolve('css-loader'),
+ options: {
+ importLoaders: 1,
+ alias: {
+ '@images': paths.globalImages,
+ }
+ },
+ },
+ require.resolve('sass-loader'),
+ {
+ loader: require.resolve('postcss-loader'),
+ options: {
+ ident: 'postcss',
+ plugins: () => [
+ autoprefixer({
+ browsers: [
+ '>1%',
+ 'last 4 versions',
+ 'Firefox ESR',
+ 'not ie < 9', // React doesn't support IE8
+ ],
+ flexbox: 'no-2009',
+ })
+ ],
+ },
+ },
+ ],
+ },
+ {
+ test: /\.(png|jpe?g|gif|svg)/,
+ use: [
+ {
+ loader: require.resolve('file-loader'),
+ options: {
+ name: 'images/[name].[ext]'
+ }
+ }
+ ]
+ },
+ ]
+ },
+ resolve: {
+ modules: ['node_modules', paths.appAssets],
+ extensions: ['*', '.js', '.jsx', '.scss', '.css'],
+ },
+ plugins: [
+ new webpack.HotModuleReplacementPlugin(),
+ new styleLintPlugin({
+ files: 'client/**/*.scss',
+ failOnError: false,
+ quiet: false,
+ }),
+ new MiniCssExtractPlugin({
+ filename: 'style.css'
+ }),
+ new HtmlWebpackPlugin({
+ template: paths.appHtml,
+ filename: 'index.html'
+ })
+ ],
+};
diff --git a/config/webpack.dev.js b/config/webpack.dev.js
new file mode 100644
index 0000000..1bbcc00
--- /dev/null
+++ b/config/webpack.dev.js
@@ -0,0 +1,19 @@
+const merge = require('webpack-merge');
+const common = require('./webpack.common');
+
+
+module.exports = merge(common, {
+ mode: 'development',
+ devtool: 'inline-source-map',
+ devServer: {
+ historyApiFallback: true,
+ stats: 'errors-only',
+ port: '8000',
+ hot: true,
+ open: true,
+ overlay:true,
+ proxy: {
+ '/api': 'http://localhost:3000'
+ }
+ }
+})
diff --git a/config/webpack.prod.js b/config/webpack.prod.js
new file mode 100644
index 0000000..fc17c03
--- /dev/null
+++ b/config/webpack.prod.js
@@ -0,0 +1,18 @@
+const paths = require('./paths');
+const merge = require('webpack-merge');
+const common = require('./webpack.common');
+const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+const CleanWebpackPlugin = require('clean-webpack-plugin');
+const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+
+
+module.exports = merge(common, {
+ mode: 'production',
+ plugins: [
+ new CleanWebpackPlugin(['build'], {root: paths.root}),
+ new UglifyJSPlugin({
+ sourceMap: true
+ }),
+ new OptimizeCSSAssetsPlugin()
+ ]
+})
diff --git a/package.json b/package.json
new file mode 100755
index 0000000..feb1bec
--- /dev/null
+++ b/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "trip-assistant",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "express": "nodemon server/app.js",
+ "createTables": "node server/utils/scripts/createTables.js",
+ "clearAllTabless": "node server/utils/scripts/reCreateTables.js",
+ "reCreateTables": "npm run clearAllTabless && npm run createTables",
+ "dbseed": "node server/utils/scripts/dbSeed.js",
+ "dev": "webpack-dev-server --config ./config/webpack.dev.js",
+ "build": "webpack --config ./config/webpack.prod.js",
+ "lint": "eslint client"
+ },
+ "keywords": [],
+ "author": "Dmytro Prokopiuk (-)",
+ "license": "ISC",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Social-projects-Rivne/TripAssistantCore.git"
+ },
+ "bugs": {
+ "url": "https://github.com/Social-projects-Rivne/TripAssistantCore/issues"
+ },
+ "homepage": "https://github.com/Social-projects-Rivne/TripAssistantCore#readme",
+ "dependencies": {
+ "axios": "^0.18.0",
+ "body-parser": "^1.18.3",
+ "cookie-parser": "^1.4.3",
+ "dotenv": "^6.0.0",
+ "express": "^4.16.3",
+ "google-maps-react": "^2.0.2",
+ "hash.js": "^1.1.5",
+ "materialize-css": "^1.0.0-rc.2",
+ "nodemailer": "^4.6.7",
+ "pg": "^7.4.3",
+ "prop-types": "^15.6.2",
+ "react": "^16.3.2",
+ "react-addons-css-transition-group": "^15.6.2",
+ "react-dom": "^16.3.2",
+ "react-geosuggest": "^2.8.0",
+ "react-router-dom": "^4.3.1",
+ "uuid-v4": "^0.1.0",
+ "react-loader-spinner": "^2.1.0"
+ },
+ "devDependencies": {
+ "autoprefixer": "^8.6.4",
+ "babel-core": "^6.26.3",
+ "babel-eslint": "^8.2.5",
+ "babel-loader": "^7.1.4",
+ "babel-preset-env": "^1.7.0",
+ "babel-preset-react": "^6.24.1",
+ "babel-preset-stage-2": "^6.24.1",
+ "clean-webpack-plugin": "^0.1.19",
+ "colors": "^1.3.1",
+ "cowsay": "^1.3.1",
+ "css-hot-loader": "^1.3.9",
+ "css-loader": "^0.28.11",
+ "eslint": "^4.19.1",
+ "eslint-config-airbnb": "^17.0.0",
+ "eslint-loader": "^2.0.0",
+ "eslint-plugin-import": "^2.13.0",
+ "eslint-plugin-jsx-a11y": "^6.0.3",
+ "eslint-plugin-react": "^7.10.0",
+ "file-loader": "^1.1.11",
+ "html-webpack-plugin": "^3.2.0",
+ "mini-css-extract-plugin": "^0.4.1",
+ "node-sass": "^4.9.0",
+ "nodemon": "^1.18.3",
+ "optimize-css-assets-webpack-plugin": "^4.0.3",
+ "postcss-loader": "^2.1.5",
+ "react-hot-loader": "^4.3.3",
+ "sass-loader": "^7.0.3",
+ "style-loader": "^0.21.0",
+ "stylelint": "^9.3.0",
+ "stylelint-config-standard": "^18.2.0",
+ "stylelint-webpack-plugin": "^0.10.5",
+ "webpack": "^4.14.0",
+ "webpack-cli": "^3.0.8",
+ "webpack-dev-server": "^3.1.4",
+ "webpack-merge": "^4.1.3"
+ }
+}
diff --git a/public/data/allHistory.json b/public/data/allHistory.json
new file mode 100644
index 0000000..49d43f1
--- /dev/null
+++ b/public/data/allHistory.json
@@ -0,0 +1,17 @@
+[
+ {
+ "name": "Lviv - Paris",
+ "date": "07/08/2018",
+ "isActive": true
+ },
+ {
+ "name": "Lviv - Rivne",
+ "date": "07/09/2018",
+ "isActive": false
+ },
+ {
+ "name": "Rivne - Lviv",
+ "date": "07/10/2018",
+ "isActive": false
+ }
+]
diff --git a/public/data/feedbacksData.json b/public/data/feedbacksData.json
new file mode 100644
index 0000000..fb51bfa
--- /dev/null
+++ b/public/data/feedbacksData.json
@@ -0,0 +1,23 @@
+[
+ {
+ "name": "from trip where trip id",
+ "rating": 4.7,
+ "feedback": "The driver was drunk! I liked everything!\n\n Quisque varius imperdiet auctor. In et dui elit. Nam metus lorem, imperdiet iaculis imperdiet sit amet, ullamcorper quis ipsum. Sed sed nisi vitae magna vestibulum commodo.",
+ "userName": "from user where create_by",
+ "date": "from trip where trip id"
+ },
+ {
+ "name": "Lviv - Ternopil",
+ "rating": 4.1,
+ "feedback": "The driver was drunk! I liked everything!\n\n Quisque varius imperdiet auctor. In et dui elit. Nam metus lorem, imperdiet iaculis imperdiet sit amet, ullamcorper quis ipsum. Sed sed nisi vitae magna vestibulum commodo.",
+ "userName": "Petro",
+ "date": "07/08/2018"
+ },
+ {
+ "name": "Lviv - Kyiv",
+ "rating": 5,
+ "feedback": "The driver was drunk! I liked everything!\n\n Quisque varius imperdiet auctor. In et dui elit. Nam metus lorem, imperdiet iaculis imperdiet sit amet, ullamcorper quis ipsum. Sed sed nisi vitae magna vestibulum commodo.",
+ "userName": "Petro",
+ "date": "07/08/2018"
+ }
+]
diff --git a/public/data/profileUserData.json b/public/data/profileUserData.json
new file mode 100644
index 0000000..6a11fab
--- /dev/null
+++ b/public/data/profileUserData.json
@@ -0,0 +1,10 @@
+[
+ {
+ "id_user":1,
+ "email":"asda@asdasd.com",
+ "avatar":null,
+ "rating": 4.67,
+ "kmTraveled": 10.357,
+ "tripsCount": 5
+ }
+]
diff --git a/public/data/searchResultOfPublishedRoutes.json b/public/data/searchResultOfPublishedRoutes.json
new file mode 100644
index 0000000..59f19c5
--- /dev/null
+++ b/public/data/searchResultOfPublishedRoutes.json
@@ -0,0 +1,112 @@
+[
+ {
+ "name": "Oleksii Iordatii",
+ "rating": 4,
+ "startPoint": "Rivne",
+ "endPoint": "Lviv",
+ "date": "9/7/2018",
+ "seats": 2,
+ "price": 300,
+ "currency": "UAH"
+ },
+ {
+ "name": "Serhii Kovach",
+ "rating": 4.5,
+ "startPoint": "Rivne",
+ "endPoint": "Kyiv",
+ "date": "12/11/2018",
+ "seats": 1,
+ "price": 300,
+ "currency": "UAH"
+ },
+ {
+ "name": "Olena Kovach",
+ "rating": 4.8,
+ "startPoint": "Rivne",
+ "endPoint": "Luck",
+ "date": "10/12/2018",
+ "seats": 3,
+ "price": 100,
+ "currency": "UAH"
+ },
+ {
+ "name": "Vasilii Melnyk",
+ "rating": 3.8,
+ "startPoint": "Rivne",
+ "endPoint": "Luck",
+ "date": "10/13/2018",
+ "seats": 5,
+ "price": 50,
+ "currency": "UAH"
+ },
+ {
+ "name": "Claus Green",
+ "rating": 5,
+ "startPoint": "Rivne",
+ "endPoint": "Kyiv",
+ "date": "10/14/2018",
+ "seats": 2,
+ "price": 500,
+ "currency": "UAH"
+ },
+ {
+ "name": "Kith Brown",
+ "rating": 4.3,
+ "startPoint": "Rivne",
+ "endPoint": "Kyiv",
+ "date": "10/14/2018",
+ "seats": 1,
+ "price": 500,
+ "currency": "UAH"
+ },
+ {
+ "name": "Katie Luw",
+ "rating": 4.6,
+ "startPoint": "Rivne",
+ "endPoint": "Lviv",
+ "date": "10/15/2018",
+ "seats": 3,
+ "price": 200,
+ "currency": "UAH"
+ },
+ {
+ "name": "Marry Smith",
+ "rating": 2.5,
+ "startPoint": "Rivne",
+ "endPoint": "Lviv",
+ "date": "10/17/2018",
+ "seats": 4,
+ "price": 150,
+ "currency": "UAH"
+ },
+ {
+ "name": "Jake Black",
+ "rating": 5,
+ "startPoint": "Rivne",
+ "endPoint": "Kyiv",
+ "date": "10/18/2018",
+ "seats": 4,
+ "price": 400,
+ "currency": "UAH"
+ },
+ {
+ "name": "Sam Harris",
+ "rating": 5,
+ "startPoint": "Rivne",
+ "endPoint": "Lviv",
+ "date": "10/19/2018",
+ "seats": 1,
+ "price": 100,
+ "currency": "UAH"
+ },
+ {
+ "name": "Seth Green",
+ "rating": 2,
+ "startPoint": "Rivne",
+ "endPoint": "Lviv",
+ "date": "10/20/2018",
+ "seats": 5,
+ "price": 2000,
+ "currency": "UAH"
+ }
+]
diff --git a/public/data/userCarsData.json b/public/data/userCarsData.json
new file mode 100644
index 0000000..f030626
--- /dev/null
+++ b/public/data/userCarsData.json
@@ -0,0 +1,29 @@
+[
+ {
+ "idCars":1,
+ "nameCar":"Peugeot",
+ "tankVolume":50,
+ "maxPassengersCount":2,
+ "avgGasCost":7,
+ "baggageVolume":2,
+ "avgSpeed":80
+ },
+ {
+ "idCars":2,
+ "nameCar":"Lamborghini",
+ "tankVolume":40,
+ "maxPassengersCount":2,
+ "avgGasCost":8,
+ "baggageVolume":1,
+ "avgSpeed":60
+ },
+ {
+ "idCars":3,
+ "nameCar":"Porsche",
+ "tankVolume":80,
+ "maxPassengersCount":1,
+ "avgGasCost":5,
+ "baggageVolume":2,
+ "avgSpeed":90
+ }
+]
diff --git a/public/data/userData.json b/public/data/userData.json
new file mode 100644
index 0000000..20ef5fe
--- /dev/null
+++ b/public/data/userData.json
@@ -0,0 +1,1202 @@
+[
+ {
+ "_id": "5b421eed570b972e7081ed55",
+ "name": {
+ "first": "Oneill",
+ "last": "Long"
+ },
+ "login": "Oneill",
+ "password_hash": "5b421eedc4235617a7ca1c0f",
+ "token": "5b421eed72db3f110fa21e0d",
+ "email": "oneill.long@email.net",
+ "home_point": {
+ "city": "Cutter",
+ "position": [
+ "-71.644535",
+ "-32.810064"
+ ]
+ },
+ "drive_hours_per_day": 15,
+ "drive_stop_period": 7,
+ "eat_stop_period": "3.7",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed5118427866add2b9",
+ "name": {
+ "first": "Odessa",
+ "last": "Mckee"
+ },
+ "login": "Odessa",
+ "password_hash": "5b421eed709809c0814c8946",
+ "token": "5b421eed4126425ce12bab78",
+ "email": "odessa.mckee@email.ca",
+ "home_point": {
+ "city": "Wyoming",
+ "position": [
+ "18.33585",
+ "146.917907"
+ ]
+ },
+ "drive_hours_per_day": 3,
+ "drive_stop_period": 3,
+ "eat_stop_period": "5.7",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedcdf31239b4f72f44",
+ "name": {
+ "first": "Calhoun",
+ "last": "Wilkerson"
+ },
+ "login": "Calhoun",
+ "password_hash": "5b421eedd405874d8a826621",
+ "token": "5b421eed969558ec4ee97d0e",
+ "email": "calhoun.wilkerson@email.tv",
+ "home_point": {
+ "city": "Goochland",
+ "position": [
+ "40.024682",
+ "130.113147"
+ ]
+ },
+ "drive_hours_per_day": 12,
+ "drive_stop_period": 1,
+ "eat_stop_period": 1,
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed7af3a7459e4959ad",
+ "name": {
+ "first": "Alfreda",
+ "last": "Cotton"
+ },
+ "login": "Alfreda",
+ "password_hash": "5b421eed71c4e854c11f797c",
+ "token": "5b421eed96598532ee7005fe",
+ "email": "alfreda.cotton@email.name",
+ "home_point": {
+ "city": "Hendersonville",
+ "position": [
+ "-9.990274",
+ "70.081733"
+ ]
+ },
+ "drive_hours_per_day": 6,
+ "drive_stop_period": 9,
+ "eat_stop_period": 5,
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedc1d2804fbb23f837",
+ "name": {
+ "first": "Pittman",
+ "last": "Kemp"
+ },
+ "login": "Pittman",
+ "password_hash": "5b421eed12297e075212ec91",
+ "token": "5b421eed0f6ca467e34c38b7",
+ "email": "pittman.kemp@email.biz",
+ "home_point": {
+ "city": "Orin",
+ "position": [
+ "-43.501093",
+ "-94.066658"
+ ]
+ },
+ "drive_hours_per_day": 10,
+ "drive_stop_period": 2,
+ "eat_stop_period": "8.3",
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed5d1fdcbff2e90ef9",
+ "name": {
+ "first": "Maxwell",
+ "last": "Vang"
+ },
+ "login": "Maxwell",
+ "password_hash": "5b421eed0cc81d5870faab89",
+ "token": "5b421eed149beb9406d70447",
+ "email": "maxwell.vang@email.com",
+ "home_point": {
+ "city": "Cashtown",
+ "position": [
+ "3.003251",
+ "173.55081"
+ ]
+ },
+ "drive_hours_per_day": 11,
+ "drive_stop_period": 8,
+ "eat_stop_period": 10,
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed1209508f81f437cb",
+ "name": {
+ "first": "Fay",
+ "last": "Martinez"
+ },
+ "login": "Fay",
+ "password_hash": "5b421eedd0010c3293561466",
+ "token": "5b421eed1ac02dbe95d286ae",
+ "email": "fay.martinez@email.info",
+ "home_point": {
+ "city": "Lindisfarne",
+ "position": [
+ "-32.490797",
+ "139.525312"
+ ]
+ },
+ "drive_hours_per_day": 24,
+ "drive_stop_period": 8,
+ "eat_stop_period": "4.8",
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed94017385e6e79c5f",
+ "name": {
+ "first": "Walsh",
+ "last": "Richards"
+ },
+ "login": "Walsh",
+ "password_hash": "5b421eedce4aaf7947523688",
+ "token": "5b421eed1a49696984d2b32f",
+ "email": "walsh.richards@email.us",
+ "home_point": {
+ "city": "Mathews",
+ "position": [
+ "73.559666",
+ "102.654454"
+ ]
+ },
+ "drive_hours_per_day": 13,
+ "drive_stop_period": 8,
+ "eat_stop_period": "3.3",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed7802473660bed3f2",
+ "name": {
+ "first": "Fitzgerald",
+ "last": "Barker"
+ },
+ "login": "Fitzgerald",
+ "password_hash": "5b421eed3c29f8ba32b5b683",
+ "token": "5b421eedecba8786e8087466",
+ "email": "fitzgerald.barker@email.biz",
+ "home_point": {
+ "city": "Mansfield",
+ "position": [
+ "47.545079",
+ "-86.945794"
+ ]
+ },
+ "drive_hours_per_day": 1,
+ "drive_stop_period": 1,
+ "eat_stop_period": "6.6",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedf35e62ab93bdf8a2",
+ "name": {
+ "first": "Consuelo",
+ "last": "English"
+ },
+ "login": "Consuelo",
+ "password_hash": "5b421eedf06740ebe76f66c0",
+ "token": "5b421eed5bc36b9bff6950fb",
+ "email": "consuelo.english@email.org",
+ "home_point": {
+ "city": "Waverly",
+ "position": [
+ "-24.126354",
+ "-1.984272"
+ ]
+ },
+ "drive_hours_per_day": 18,
+ "drive_stop_period": 9,
+ "eat_stop_period": "1.7",
+ "online": true,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed710409654f727ed0",
+ "name": {
+ "first": "Watson",
+ "last": "Stevenson"
+ },
+ "login": "Watson",
+ "password_hash": "5b421eedfd6389e6ab067a2d",
+ "token": "5b421eed65739a1cd2163843",
+ "email": "watson.stevenson@email.me",
+ "home_point": {
+ "city": "Longbranch",
+ "position": [
+ "-21.058443",
+ "13.458204"
+ ]
+ },
+ "drive_hours_per_day": 13,
+ "drive_stop_period": 7,
+ "eat_stop_period": "8.9",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed3dbfd7aa9d2fdb51",
+ "name": {
+ "first": "Lynette",
+ "last": "Solomon"
+ },
+ "login": "Lynette",
+ "password_hash": "5b421eedc4adf4a499345736",
+ "token": "5b421eedbeb991d5e365e5e9",
+ "email": "lynette.solomon@email.co.uk",
+ "home_point": {
+ "city": "Lynn",
+ "position": [
+ "-72.50613",
+ "120.126676"
+ ]
+ },
+ "drive_hours_per_day": 10,
+ "drive_stop_period": 8,
+ "eat_stop_period": "6.6",
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed64a4e4de0e1a79be",
+ "name": {
+ "first": "Chavez",
+ "last": "Oliver"
+ },
+ "login": "Chavez",
+ "password_hash": "5b421eed3728a6a8279fda5f",
+ "token": "5b421eede445161a612694c0",
+ "email": "chavez.oliver@email.net",
+ "home_point": {
+ "city": "Bowden",
+ "position": [
+ "61.518145",
+ "28.84519"
+ ]
+ },
+ "drive_hours_per_day": 19,
+ "drive_stop_period": 9,
+ "eat_stop_period": "5.8",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedcab3008afe91930b",
+ "name": {
+ "first": "Beryl",
+ "last": "Hurley"
+ },
+ "login": "Beryl",
+ "password_hash": "5b421eedcec267489a5daa37",
+ "token": "5b421eed77303a21fb48390c",
+ "email": "beryl.hurley@email.ca",
+ "home_point": {
+ "city": "Freetown",
+ "position": [
+ "8.837669",
+ "-125.007653"
+ ]
+ },
+ "drive_hours_per_day": 17,
+ "drive_stop_period": 2,
+ "eat_stop_period": "2.8",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed3908696b4a119753",
+ "name": {
+ "first": "Jill",
+ "last": "Bond"
+ },
+ "login": "Jill",
+ "password_hash": "5b421eedcd3b23481a92f250",
+ "token": "5b421eed5e08e2e775b8b0f7",
+ "email": "jill.bond@email.tv",
+ "home_point": {
+ "city": "Chical",
+ "position": [
+ "-79.328556",
+ "-16.768403"
+ ]
+ },
+ "drive_hours_per_day": 6,
+ "drive_stop_period": 6,
+ "eat_stop_period": "3.4",
+ "online": true,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eede21798c051366bfb",
+ "name": {
+ "first": "Hobbs",
+ "last": "Salas"
+ },
+ "login": "Hobbs",
+ "password_hash": "5b421eed4fcdaea125b4d803",
+ "token": "5b421eedf1219b47525240c9",
+ "email": "hobbs.salas@email.name",
+ "home_point": {
+ "city": "Noxen",
+ "position": [
+ "-66.644285",
+ "-102.432584"
+ ]
+ },
+ "drive_hours_per_day": 15,
+ "drive_stop_period": 8,
+ "eat_stop_period": "3.3",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedf3df6dc1824caec4",
+ "name": {
+ "first": "Fischer",
+ "last": "Mayo"
+ },
+ "login": "Fischer",
+ "password_hash": "5b421eeddb1b485af38b80de",
+ "token": "5b421eed30344c526d6b2e06",
+ "email": "fischer.mayo@email.biz",
+ "home_point": {
+ "city": "Beaulieu",
+ "position": [
+ "41.962986",
+ "-61.92002"
+ ]
+ },
+ "drive_hours_per_day": 7,
+ "drive_stop_period": 7,
+ "eat_stop_period": 6,
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed20f8eb2053dc7876",
+ "name": {
+ "first": "Benita",
+ "last": "Hays"
+ },
+ "login": "Benita",
+ "password_hash": "5b421eed7404ca9210b7acc4",
+ "token": "5b421eed4dd7230d8c404707",
+ "email": "benita.hays@email.com",
+ "home_point": {
+ "city": "Fairforest",
+ "position": [
+ "-8.898217",
+ "-157.608242"
+ ]
+ },
+ "drive_hours_per_day": 16,
+ "drive_stop_period": 10,
+ "eat_stop_period": "9.3",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed3ef7ba935cffe755",
+ "name": {
+ "first": "Shaffer",
+ "last": "Meyers"
+ },
+ "login": "Shaffer",
+ "password_hash": "5b421eed5be381be37da9b63",
+ "token": "5b421eed16ad7ae7c49d9af7",
+ "email": "shaffer.meyers@email.info",
+ "home_point": {
+ "city": "Chicopee",
+ "position": [
+ "44.590282",
+ "78.947733"
+ ]
+ },
+ "drive_hours_per_day": 16,
+ "drive_stop_period": 7,
+ "eat_stop_period": "5.9",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed73d54f89cb7bf7ae",
+ "name": {
+ "first": "Lawrence",
+ "last": "Wilkinson"
+ },
+ "login": "Lawrence",
+ "password_hash": "5b421eed0c5011cfff9f7d57",
+ "token": "5b421eedf06b671a94604a48",
+ "email": "lawrence.wilkinson@email.us",
+ "home_point": {
+ "city": "Dixie",
+ "position": [
+ "-78.149591",
+ "-147.310898"
+ ]
+ },
+ "drive_hours_per_day": 22,
+ "drive_stop_period": 6,
+ "eat_stop_period": "3.7",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed5d07d43f10721b1b",
+ "name": {
+ "first": "Bobbi",
+ "last": "Kaufman"
+ },
+ "login": "Bobbi",
+ "password_hash": "5b421eed6308bca5769a2d30",
+ "token": "5b421eed6b0a95567b472e9a",
+ "email": "bobbi.kaufman@email.biz",
+ "home_point": {
+ "city": "Rossmore",
+ "position": [
+ "-45.948816",
+ "159.46154"
+ ]
+ },
+ "drive_hours_per_day": 12,
+ "drive_stop_period": 2,
+ "eat_stop_period": "7.4",
+ "online": true,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed911ad6fd2db1d1a8",
+ "name": {
+ "first": "Liza",
+ "last": "Allison"
+ },
+ "login": "Liza",
+ "password_hash": "5b421eed797f414773d0fd2e",
+ "token": "5b421eedcf39aab4fcfad66c",
+ "email": "liza.allison@email.org",
+ "home_point": {
+ "city": "Como",
+ "position": [
+ "89.509691",
+ "176.233502"
+ ]
+ },
+ "drive_hours_per_day": 10,
+ "drive_stop_period": 4,
+ "eat_stop_period": "3.1",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedeb5f4d9867285baa",
+ "name": {
+ "first": "Marylou",
+ "last": "Carrillo"
+ },
+ "login": "Marylou",
+ "password_hash": "5b421eed7c9a75c7e5c2aec9",
+ "token": "5b421eed14ba8b8109a3df4b",
+ "email": "marylou.carrillo@email.me",
+ "home_point": {
+ "city": "Welch",
+ "position": [
+ "75.250415",
+ "48.570575"
+ ]
+ },
+ "drive_hours_per_day": 2,
+ "drive_stop_period": 4,
+ "eat_stop_period": "0.8",
+ "online": true,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed29dea1f9d63c25f8",
+ "name": {
+ "first": "Copeland",
+ "last": "Hopkins"
+ },
+ "login": "Copeland",
+ "password_hash": "5b421eed9449b23a51b2ed75",
+ "token": "5b421eed22c92b44d10cc28b",
+ "email": "copeland.hopkins@email.co.uk",
+ "home_point": {
+ "city": "Coleville",
+ "position": [
+ "-11.704984",
+ "140.966379"
+ ]
+ },
+ "drive_hours_per_day": 20,
+ "drive_stop_period": 9,
+ "eat_stop_period": "0.5",
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed8993d98b7f556f56",
+ "name": {
+ "first": "Bernice",
+ "last": "Richardson"
+ },
+ "login": "Bernice",
+ "password_hash": "5b421eed993a21e24eb30def",
+ "token": "5b421eed897143ea06d16a4d",
+ "email": "bernice.richardson@email.net",
+ "home_point": {
+ "city": "Galesville",
+ "position": [
+ "51.450106",
+ "36.069966"
+ ]
+ },
+ "drive_hours_per_day": 18,
+ "drive_stop_period": 6,
+ "eat_stop_period": "1.7",
+ "online": true,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed35f9b57f9e49de1c",
+ "name": {
+ "first": "Crystal",
+ "last": "Noble"
+ },
+ "login": "Crystal",
+ "password_hash": "5b421eed284c1af87e2874ed",
+ "token": "5b421eed7e15444b6697b7eb",
+ "email": "crystal.noble@email.ca",
+ "home_point": {
+ "city": "Brenton",
+ "position": [
+ "-74.32991",
+ "26.801816"
+ ]
+ },
+ "drive_hours_per_day": 12,
+ "drive_stop_period": 10,
+ "eat_stop_period": "6.3",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed0de3f5e6f05bb2d4",
+ "name": {
+ "first": "Wall",
+ "last": "Huffman"
+ },
+ "login": "Wall",
+ "password_hash": "5b421eed2c25ca166efff077",
+ "token": "5b421eed0d5d58269d6b8f41",
+ "email": "wall.huffman@email.tv",
+ "home_point": {
+ "city": "Rockhill",
+ "position": [
+ "-88.82964",
+ "19.157511"
+ ]
+ },
+ "drive_hours_per_day": 2,
+ "drive_stop_period": 9,
+ "eat_stop_period": "7.5",
+ "online": true,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed6f3990e22de7e7d4",
+ "name": {
+ "first": "Reynolds",
+ "last": "West"
+ },
+ "login": "Reynolds",
+ "password_hash": "5b421eed26ade1f832dfecba",
+ "token": "5b421eedb2a095e4c6aa2ba6",
+ "email": "reynolds.west@email.name",
+ "home_point": {
+ "city": "Oceola",
+ "position": [
+ "45.684673",
+ "-20.417541"
+ ]
+ },
+ "drive_hours_per_day": 10,
+ "drive_stop_period": 3,
+ "eat_stop_period": "8.2",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedc48699a953ff994d",
+ "name": {
+ "first": "Nichole",
+ "last": "Rodriguez"
+ },
+ "login": "Nichole",
+ "password_hash": "5b421eedf9319a7b24dd5054",
+ "token": "5b421eed23382f10a6559cde",
+ "email": "nichole.rodriguez@email.biz",
+ "home_point": {
+ "city": "Bordelonville",
+ "position": [
+ "88.700788",
+ "147.417133"
+ ]
+ },
+ "drive_hours_per_day": 1,
+ "drive_stop_period": 9,
+ "eat_stop_period": "6.6",
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed939efd3e62c020a9",
+ "name": {
+ "first": "Lorrie",
+ "last": "Mcleod"
+ },
+ "login": "Lorrie",
+ "password_hash": "5b421eed698f24b506b352da",
+ "token": "5b421eed66a3ae198127fff4",
+ "email": "lorrie.mcleod@email.com",
+ "home_point": {
+ "city": "Brooktrails",
+ "position": [
+ "-4.345954",
+ "-151.595014"
+ ]
+ },
+ "drive_hours_per_day": 23,
+ "drive_stop_period": 1,
+ "eat_stop_period": "1.7",
+ "online": true,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eede31c1af3bc9949bd",
+ "name": {
+ "first": "Lenora",
+ "last": "Alvarado"
+ },
+ "login": "Lenora",
+ "password_hash": "5b421eed1e0ebd7c9e70df24",
+ "token": "5b421eedd55dfc693c69cb17",
+ "email": "lenora.alvarado@email.info",
+ "home_point": {
+ "city": "Shrewsbury",
+ "position": [
+ "-5.432549",
+ "156.166834"
+ ]
+ },
+ "drive_hours_per_day": 8,
+ "drive_stop_period": 2,
+ "eat_stop_period": "8.1",
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed3342327d2f8ea2e6",
+ "name": {
+ "first": "Garrett",
+ "last": "Ochoa"
+ },
+ "login": "Garrett",
+ "password_hash": "5b421eed445faf27dd43a2a0",
+ "token": "5b421eed07ba759b1a862b50",
+ "email": "garrett.ochoa@email.us",
+ "home_point": {
+ "city": "Waukeenah",
+ "position": [
+ "-88.262838",
+ "-140.127274"
+ ]
+ },
+ "drive_hours_per_day": 22,
+ "drive_stop_period": 2,
+ "eat_stop_period": "9.7",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed477e1422e905f877",
+ "name": {
+ "first": "Morales",
+ "last": "Acosta"
+ },
+ "login": "Morales",
+ "password_hash": "5b421eed53aa81c637b63719",
+ "token": "5b421eed91f4da2490500f3e",
+ "email": "morales.acosta@email.biz",
+ "home_point": {
+ "city": "Belleview",
+ "position": [
+ "72.945244",
+ "82.799071"
+ ]
+ },
+ "drive_hours_per_day": 9,
+ "drive_stop_period": 10,
+ "eat_stop_period": "9.4",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedc91686b08a250039",
+ "name": {
+ "first": "Hunt",
+ "last": "Robles"
+ },
+ "login": "Hunt",
+ "password_hash": "5b421eedc41c9ed2c6c9ae56",
+ "token": "5b421eeded43cd27a42eb706",
+ "email": "hunt.robles@email.org",
+ "home_point": {
+ "city": "Johnsonburg",
+ "position": [
+ "46.294296",
+ "30.40412"
+ ]
+ },
+ "drive_hours_per_day": 12,
+ "drive_stop_period": 10,
+ "eat_stop_period": "0.6",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eede93f441cf6f9c813",
+ "name": {
+ "first": "Bridgette",
+ "last": "Luna"
+ },
+ "login": "Bridgette",
+ "password_hash": "5b421eed44c3713248c4fbcc",
+ "token": "5b421eedae3348643394a112",
+ "email": "bridgette.luna@email.me",
+ "home_point": {
+ "city": "Efland",
+ "position": [
+ "-27.322038",
+ "-36.995213"
+ ]
+ },
+ "drive_hours_per_day": 19,
+ "drive_stop_period": 9,
+ "eat_stop_period": 7,
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed04cf2fa180fbb7e2",
+ "name": {
+ "first": "Lancaster",
+ "last": "Foreman"
+ },
+ "login": "Lancaster",
+ "password_hash": "5b421eed45922d444a12237a",
+ "token": "5b421eed28e9aab3d804a03c",
+ "email": "lancaster.foreman@email.co.uk",
+ "home_point": {
+ "city": "Shaft",
+ "position": [
+ "82.464137",
+ "-113.911916"
+ ]
+ },
+ "drive_hours_per_day": 16,
+ "drive_stop_period": 4,
+ "eat_stop_period": "8.7",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed5a7d5fee2a0521e2",
+ "name": {
+ "first": "Clark",
+ "last": "Burns"
+ },
+ "login": "Clark",
+ "password_hash": "5b421eed0cdcc28c868134dc",
+ "token": "5b421eedd819f79caf80adc3",
+ "email": "clark.burns@email.net",
+ "home_point": {
+ "city": "Wollochet",
+ "position": [
+ "79.057779",
+ "107.013442"
+ ]
+ },
+ "drive_hours_per_day": 12,
+ "drive_stop_period": 3,
+ "eat_stop_period": "8.2",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed2166948c432fbbd4",
+ "name": {
+ "first": "Samantha",
+ "last": "Leonard"
+ },
+ "login": "Samantha",
+ "password_hash": "5b421eed3773ed2d7497eb69",
+ "token": "5b421eedfd9359fc0571c69b",
+ "email": "samantha.leonard@email.ca",
+ "home_point": {
+ "city": "Idamay",
+ "position": [
+ "43.385886",
+ "-23.163019"
+ ]
+ },
+ "drive_hours_per_day": 4,
+ "drive_stop_period": 2,
+ "eat_stop_period": "6.5",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed6b3ababbc6d3b38f",
+ "name": {
+ "first": "Lillie",
+ "last": "Fields"
+ },
+ "login": "Lillie",
+ "password_hash": "5b421eed585a5991207a5f19",
+ "token": "5b421eed1331fc8f4b570e34",
+ "email": "lillie.fields@email.tv",
+ "home_point": {
+ "city": "Soudan",
+ "position": [
+ "-14.279184",
+ "22.105884"
+ ]
+ },
+ "drive_hours_per_day": 7,
+ "drive_stop_period": 3,
+ "eat_stop_period": "8.3",
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed771fdefa3c0cbc1a",
+ "name": {
+ "first": "Tania",
+ "last": "Sykes"
+ },
+ "login": "Tania",
+ "password_hash": "5b421eedc96aa5b48efc2325",
+ "token": "5b421eedfe57185e86868f8d",
+ "email": "tania.sykes@email.name",
+ "home_point": {
+ "city": "Coaldale",
+ "position": [
+ "48.415986",
+ "17.856789"
+ ]
+ },
+ "drive_hours_per_day": 24,
+ "drive_stop_period": 1,
+ "eat_stop_period": "7.3",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed069bf1742945f0b4",
+ "name": {
+ "first": "Carrillo",
+ "last": "Flynn"
+ },
+ "login": "Carrillo",
+ "password_hash": "5b421eed984e05c559db4d73",
+ "token": "5b421eed974eb5ca5c6cfbbc",
+ "email": "carrillo.flynn@email.biz",
+ "home_point": {
+ "city": "Cuylerville",
+ "position": [
+ "40.936513",
+ "122.609616"
+ ]
+ },
+ "drive_hours_per_day": 10,
+ "drive_stop_period": 1,
+ "eat_stop_period": "6.5",
+ "online": false,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed863845b562d12423",
+ "name": {
+ "first": "Castillo",
+ "last": "Riggs"
+ },
+ "login": "Castillo",
+ "password_hash": "5b421eede27516a485126efe",
+ "token": "5b421eed13909ebcc89fe9a4",
+ "email": "castillo.riggs@email.com",
+ "home_point": {
+ "city": "Sanford",
+ "position": [
+ "69.307536",
+ "-45.585612"
+ ]
+ },
+ "drive_hours_per_day": 6,
+ "drive_stop_period": 2,
+ "eat_stop_period": "0.7",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed540851017ab6dbaa",
+ "name": {
+ "first": "Young",
+ "last": "Sexton"
+ },
+ "login": "Young",
+ "password_hash": "5b421eed02a543410932da18",
+ "token": "5b421eedc8dcc9a0d33eba7d",
+ "email": "young.sexton@email.info",
+ "home_point": {
+ "city": "Winfred",
+ "position": [
+ "-59.435744",
+ "-110.847874"
+ ]
+ },
+ "drive_hours_per_day": 14,
+ "drive_stop_period": 2,
+ "eat_stop_period": "5.8",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed6bbc5a864941573d",
+ "name": {
+ "first": "Agnes",
+ "last": "Schultz"
+ },
+ "login": "Agnes",
+ "password_hash": "5b421eede6465bcface29194",
+ "token": "5b421eed504e12ab1dfe5b63",
+ "email": "agnes.schultz@email.us",
+ "home_point": {
+ "city": "Deseret",
+ "position": [
+ "-38.102422",
+ "121.625208"
+ ]
+ },
+ "drive_hours_per_day": 20,
+ "drive_stop_period": 2,
+ "eat_stop_period": "2.1",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedeb8ad511750e840e",
+ "name": {
+ "first": "Chang",
+ "last": "Holder"
+ },
+ "login": "Chang",
+ "password_hash": "5b421eed61bcc426f7b05684",
+ "token": "5b421eedd16ea5f5396b3928",
+ "email": "chang.holder@email.biz",
+ "home_point": {
+ "city": "Darlington",
+ "position": [
+ "61.321813",
+ "172.291113"
+ ]
+ },
+ "drive_hours_per_day": 16,
+ "drive_stop_period": 3,
+ "eat_stop_period": "4.3",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed568aca4982ced5aa",
+ "name": {
+ "first": "Josie",
+ "last": "Hicks"
+ },
+ "login": "Josie",
+ "password_hash": "5b421eedc15e78c1df564c04",
+ "token": "5b421eed3fd580f8664bb181",
+ "email": "josie.hicks@email.org",
+ "home_point": {
+ "city": "Kenwood",
+ "position": [
+ "-83.931054",
+ "93.527776"
+ ]
+ },
+ "drive_hours_per_day": 9,
+ "drive_stop_period": 6,
+ "eat_stop_period": "2.3",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed894c72b4e8bf668a",
+ "name": {
+ "first": "Tanner",
+ "last": "Houston"
+ },
+ "login": "Tanner",
+ "password_hash": "5b421eedfee2f95f15a01db1",
+ "token": "5b421eed7e43b70f3417e4b1",
+ "email": "tanner.houston@email.me",
+ "home_point": {
+ "city": "Allamuchy",
+ "position": [
+ "24.926965",
+ "135.618575"
+ ]
+ },
+ "drive_hours_per_day": 5,
+ "drive_stop_period": 8,
+ "eat_stop_period": "4.9",
+ "online": true,
+ "acount_status": false,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eed7f809f9a22fa8115",
+ "name": {
+ "first": "Higgins",
+ "last": "Velasquez"
+ },
+ "login": "Higgins",
+ "password_hash": "5b421eed83a341a2dde30378",
+ "token": "5b421eed94e496a014a5c270",
+ "email": "higgins.velasquez@email.co.uk",
+ "home_point": {
+ "city": "Bend",
+ "position": [
+ "86.775289",
+ "31.497948"
+ ]
+ },
+ "drive_hours_per_day": 12,
+ "drive_stop_period": 5,
+ "eat_stop_period": "3.7",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedfa29d456711eeb16",
+ "name": {
+ "first": "Kimberly",
+ "last": "Sellers"
+ },
+ "login": "Kimberly",
+ "password_hash": "5b421eeddf26df1bd616fafb",
+ "token": "5b421eed4ad8df4e21bff080",
+ "email": "kimberly.sellers@email.net",
+ "home_point": {
+ "city": "Edgewater",
+ "position": [
+ "-30.217443",
+ "-22.112985"
+ ]
+ },
+ "drive_hours_per_day": 23,
+ "drive_stop_period": 2,
+ "eat_stop_period": "6.6",
+ "online": true,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ },
+ {
+ "_id": "5b421eedc5156774570e1219",
+ "name": {
+ "first": "Dorsey",
+ "last": "Lloyd"
+ },
+ "login": "Dorsey",
+ "password_hash": "5b421eed6f9465e088ca85ae",
+ "token": "5b421eedffc1dfbe1541885b",
+ "email": "dorsey.lloyd@email.ca",
+ "home_point": {
+ "city": "Jacumba",
+ "position": [
+ "-37.666216",
+ "-115.117113"
+ ]
+ },
+ "drive_hours_per_day": 17,
+ "drive_stop_period": 3,
+ "eat_stop_period": "6.8",
+ "online": false,
+ "acount_status": true,
+ "picture": "http://placehold.it/32x32"
+ }
+]
diff --git a/public/images/add.svg b/public/images/add.svg
new file mode 100644
index 0000000..77faa63
--- /dev/null
+++ b/public/images/add.svg
@@ -0,0 +1,69 @@
+
+
+
diff --git a/public/images/arrow-down.svg b/public/images/arrow-down.svg
new file mode 100644
index 0000000..4d8103b
--- /dev/null
+++ b/public/images/arrow-down.svg
@@ -0,0 +1,38 @@
+
+
+
+
diff --git a/public/images/arrow-right.svg b/public/images/arrow-right.svg
new file mode 100644
index 0000000..462e87f
--- /dev/null
+++ b/public/images/arrow-right.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/public/images/dashbord.svg b/public/images/dashbord.svg
new file mode 100644
index 0000000..7d52a52
--- /dev/null
+++ b/public/images/dashbord.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/images/default-userpic.png b/public/images/default-userpic.png
new file mode 100644
index 0000000..29623e7
Binary files /dev/null and b/public/images/default-userpic.png differ
diff --git a/public/images/direction.png b/public/images/direction.png
new file mode 100644
index 0000000..0cf011c
Binary files /dev/null and b/public/images/direction.png differ
diff --git a/public/images/envelope.svg b/public/images/envelope.svg
new file mode 100644
index 0000000..9e7d9fb
--- /dev/null
+++ b/public/images/envelope.svg
@@ -0,0 +1,38 @@
+
+
+
+
diff --git a/public/images/logo.svg b/public/images/logo.svg
new file mode 100644
index 0000000..6b60c10
--- /dev/null
+++ b/public/images/logo.svg
@@ -0,0 +1,7 @@
+
diff --git a/public/images/map-exemple.png b/public/images/map-exemple.png
new file mode 100644
index 0000000..43a39b2
Binary files /dev/null and b/public/images/map-exemple.png differ
diff --git a/public/images/profile-page-bg.jpg b/public/images/profile-page-bg.jpg
new file mode 100644
index 0000000..703a099
Binary files /dev/null and b/public/images/profile-page-bg.jpg differ
diff --git a/public/images/user-menu.svg b/public/images/user-menu.svg
new file mode 100644
index 0000000..f3dad81
--- /dev/null
+++ b/public/images/user-menu.svg
@@ -0,0 +1,39 @@
+
+
+
diff --git a/public/images/user.svg b/public/images/user.svg
new file mode 100644
index 0000000..05a66ab
--- /dev/null
+++ b/public/images/user.svg
@@ -0,0 +1,37 @@
+
+
+
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..9de429a
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Trip Assistant
+
+
+
+
+
diff --git a/server/.env.sample b/server/.env.sample
new file mode 100644
index 0000000..98d0455
--- /dev/null
+++ b/server/.env.sample
@@ -0,0 +1,8 @@
+PORT=3000
+PGHOST='localhost'
+PGDATABASE='tablename'
+PGUSER='username'
+PGPASSWORD=null
+PGPORT=5432
+PGDEFAULTSCHEMA='public'
+COOKIE_SECRET='crypto_cat'
diff --git a/server/app.js b/server/app.js
new file mode 100644
index 0000000..02875ef
--- /dev/null
+++ b/server/app.js
@@ -0,0 +1,34 @@
+const cowsay = require("cowsay");
+const express = require('express');
+const userRoutes = require('./routes/user');
+const signupRoutes = require('./routes/signup');
+const signinRoutes = require('./routes/signin');
+const tripsRoutes = require('./routes/trips');
+const feedbacksRoutes = require('./routes/feedback');
+const bodyParser = require('body-parser');
+const cookieParser = require('cookie-parser');
+require('dotenv').config({path: 'server/.env'})
+
+
+const app = express();
+
+app.use((req, res, next) => {
+ res.header("Access-Control-Allow-Origin", "*");
+ res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
+ next();
+});
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: true }));
+app.use(cookieParser(process.env.COOKIE_SECRET));
+app.use(userRoutes);
+app.use(signupRoutes);
+app.use(signinRoutes);
+app.use(tripsRoutes);
+app.use(feedbacksRoutes);
+
+app.listen(process.env.PORT || 3000, () => console.log(cowsay.say({
+ text : `API server listening on ${process.env.PORT || 3000} port`,
+ e : "oO",
+ T : "U "
+})));
diff --git a/server/controllers/feedback.js b/server/controllers/feedback.js
new file mode 100644
index 0000000..f6df622
--- /dev/null
+++ b/server/controllers/feedback.js
@@ -0,0 +1,14 @@
+const db = require('../db');
+const FEEDBACK_MODEL = require('../models/feedback');
+
+
+module.exports = {
+ getFeedbackById: ({ params: { id } }, res ) => {
+ db.query(FEEDBACK_MODEL.getById(id))
+ .then(({ rows }) => res.json(rows))
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+ }
+}
diff --git a/server/controllers/signinController.js b/server/controllers/signinController.js
new file mode 100644
index 0000000..a950b30
--- /dev/null
+++ b/server/controllers/signinController.js
@@ -0,0 +1,55 @@
+const db = require('../db');
+const SigninModel = require('../models/signinModel');
+
+const signinController = {};
+signinController.sessions = {};
+
+signinController.login = ({ body: { email, passwordHash } }, res) => {
+
+ db.query(SigninModel.findUserByEmail(email))
+ .then(({ rows }) => {
+ const result = rows[0];
+ if (!result) {
+ res.json({ response: 'No such email' });
+ } else if (result.passwordhash !== passwordHash) {
+ res.json({ response: 'Email and password do not match' });
+ } else if (!result.is_activated) {
+ res.json({ response: 'Email is not activated' });
+ } else if (!result['acount_status']) {
+ res.json({ response: 'Account has been BLOCKED' });
+ } else {
+ response = 'ok';
+ res.json({ response: { iduser: result.iduser, role: result.role } });
+ }
+ })
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+signinController.isAuth = ({ signedCookies: { iduser } }, res) => {
+ if (!iduser) {
+ res.json({ isAuth: false });
+ } else {
+ db.query(SigninModel.getUserId(iduser))
+ .then(({ rows }) => {
+ if (rows[0].iduser) {
+ res.json({ isAuth: true });
+ } else {
+ res.json({ isAuth: false });
+ }
+ })
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+ }
+};
+
+signinController.logout = (req, res) => {
+ res.clearCookie('iduser');
+ res.redirect('/');
+}
+
+module.exports = signinController;
diff --git a/server/controllers/signupController.js b/server/controllers/signupController.js
new file mode 100644
index 0000000..f75aa08
--- /dev/null
+++ b/server/controllers/signupController.js
@@ -0,0 +1,61 @@
+const db = require('../db');
+const SignupModel = require('../models/signupModel');
+var uuid = require('uuid-v4');
+const nodemailer = require('nodemailer');
+
+const signupController = {};
+
+signupController.checkEmailExistence = ({ body: { email } }, res) => {
+ db.query(SignupModel.getEmail(email))
+ .then(({ rows }) => {
+ rows[0]
+ ? res.send('emailExist')
+ : res.send('emailDoNotExist');
+ })
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+signupController.register = (req, res) => {
+ const { body: { fname, lname, email, password } } = req;
+ const activationId = uuid();
+ db.query(SignupModel.inserNewUser(fname, lname, email, password, activationId))
+ .then(() => {
+ const fullUrl = `${req.get('origin')}?${activationId}`;
+ const transporter = nodemailer.createTransport({
+ service: 'gmail',
+ auth: {
+ user: 'trip.assistant.noreply@gmail.com',
+ pass: '!trip!assistant'
+ }
+ });
+
+ let mailOptions = {
+ from: 'trip.assistant.noreply@gmail.com',
+ to: email,
+ subject: 'Confirm registration',
+ text: fullUrl
+ };
+
+ transporter.sendMail(mailOptions, err => err && console.log(err))
+
+ res.send('registrationSuccesul');
+ })
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ })
+};
+
+signupController.confirmEmail = ({ params: { confirmEmail } }, res) => {
+ db.query(SignupModel.updateIsActivated(confirmEmail))
+ .then(({ rows }) => res.send(rows[0]))
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ })
+};
+
+module.exports = signupController;
diff --git a/server/controllers/trips.js b/server/controllers/trips.js
new file mode 100644
index 0000000..247e492
--- /dev/null
+++ b/server/controllers/trips.js
@@ -0,0 +1,32 @@
+const db = require('../db');
+const TRIPS_MODEL = require('../models/trips');
+
+
+module.exports = {
+ addNewTrip: ({ params: { id }, body: { data } }, res) => {
+ db.query(TRIPS_MODEL.addNew(data, id))
+ .then(res.send())
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+ },
+
+ getAllTrips: ({}, res) => {
+ db.query(TRIPS_MODEL.getAll())
+ .then(({ rows }) => res.json(rows))
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+ },
+
+ getTripById: ({ params: { id } }, res ) => {
+ db.query(TRIPS_MODEL.getById(id))
+ .then(({ rows }) => res.json(rows))
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+ }
+}
diff --git a/server/controllers/userController.js b/server/controllers/userController.js
new file mode 100644
index 0000000..deb6648
--- /dev/null
+++ b/server/controllers/userController.js
@@ -0,0 +1,78 @@
+const db = require('../db');
+const UserModel = require('../models/userModel');
+
+const userController = {};
+
+userController.getUser = ({ params: { iduser } }, res) => {
+ db.query(UserModel.getUser(iduser))
+ .then(({ rows }) => res.json(rows))
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+userController.getAllUsers = (req, res) => {
+ db.query(UserModel.getAllUsers())
+ .then(({ rows }) => res.json(rows))
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+userController.unblockUser = ({ body: { iduser, status } }, res) => {
+ db.query(UserModel.unblockUser(iduser, status))
+ .then(() => res.send('Ok'))
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+userController.deleteUser = ({ body: { iduser } }, res) => {
+ db.query(UserModel.deleteUser(iduser))
+ .then(() => res.send('Ok'))
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+userController.getUserCars = ({ params: { iduser } }, res) => {
+ db.query(UserModel.getUserCars(iduser))
+ .then(({ rows }) => res.json(rows))
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+userController.addNewCar = ({ body: { formData } }, res) => {
+ db.query(UserModel.addNewCar(formData))
+ .then(res.send())
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+userController.deleteCar = ({ body: { data } }, res) => {
+ db.query(UserModel.deleteCar(data))
+ .then(res.send())
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+userController.updateCar = ({ body: { newCarData } }, res) => {
+ db.query(UserModel.updateCar(newCarData))
+ .then(res.send())
+ .catch(err => {
+ res.status(500).end();
+ console.error(err);
+ });
+};
+
+module.exports = userController;
diff --git a/server/db/index.js b/server/db/index.js
new file mode 100644
index 0000000..9bf774e
--- /dev/null
+++ b/server/db/index.js
@@ -0,0 +1,35 @@
+const { Pool } = require('pg');
+require('colors');
+
+
+const pool = new Pool();
+
+// the pool with emit an error on behalf of any idle clients
+// it contains if a backend error or network partition happens
+pool.on('error', (err, client) => {
+ console.error('Unexpected error on idle client'.red, err)
+ process.exit(-1)
+})
+
+
+module.exports = {
+ query: (sql, params) => {
+ return new Promise((resolve, reject) => {
+ pool.connect()
+ .then(client => {
+ return client.query(sql, params)
+ .then(res => {
+ client.release();
+ console.info('SQL:'.green, `${sql.text || sql}`.blue);
+ params || sql.values && console.info('\nPARAMS:'.green, `${params || sql.values}`.yellow)
+ res.rowCount >= 0 && resolve(res);
+ })
+ .catch(err => {
+ client.release();
+ console.error(err.stack);
+ reject(err.stack);
+ })
+ })
+ });
+ }
+}
diff --git a/server/models/feedback.js b/server/models/feedback.js
new file mode 100644
index 0000000..cae8e63
--- /dev/null
+++ b/server/models/feedback.js
@@ -0,0 +1,8 @@
+module.exports = {
+ getById: (id) => {
+ return {
+ text: `SELECT trips.name AS trip_name, feedback.rating, feedback.text, users.name AS user_name, trips.date FROM feedback INNER JOIN trips ON feedback.trip_id = trips.id INNER JOIN users ON feedback.create_by = users.iduser WHERE feedback.user_id=$1`,
+ values: [id]
+ }
+ }
+}
diff --git a/server/models/signinModel.js b/server/models/signinModel.js
new file mode 100644
index 0000000..ae51a41
--- /dev/null
+++ b/server/models/signinModel.js
@@ -0,0 +1,17 @@
+const SigninModel = {};
+
+SigninModel.findUserByEmail = (email) => {
+ const query = {
+ text: `SELECT iduser, email, passwordhash, is_activated, role, acount_status FROM users WHERE email = '${email}'`,
+ };
+ return query;
+};
+
+SigninModel.getUserId = (iduser) => {
+ const query = {
+ text: `SELECT iduser FROM users WHERE iduser = ${iduser}`
+ };
+ return query;
+}
+
+module.exports = SigninModel;
diff --git a/server/models/signupModel.js b/server/models/signupModel.js
new file mode 100644
index 0000000..8b5cf33
--- /dev/null
+++ b/server/models/signupModel.js
@@ -0,0 +1,19 @@
+const SignupModel = {};
+
+SignupModel.getEmail = (email) => {
+ const query = {
+ text: `SELECT email FROM users WHERE email = '${email}'`,
+ };
+ return query;
+};
+
+SignupModel.inserNewUser = (fname, lname, email, password, activationId) => {
+ const query = {
+ text: `INSERT INTO users (name, email, passwordHash, is_activated, activation_id) VALUES ('{ "first": "${fname}", "last": "${lname}" }', '${email}', '${password}', FALSE, '${activationId}')`,
+ };
+ return query;
+};
+
+SignupModel.updateIsActivated = activationId => `UPDATE users SET is_activated='true' WHERE activation_id='${activationId}' RETURNING iduser, role`;
+
+module.exports = SignupModel;
diff --git a/server/models/trips.js b/server/models/trips.js
new file mode 100644
index 0000000..28a111d
--- /dev/null
+++ b/server/models/trips.js
@@ -0,0 +1,28 @@
+module.exports = {
+ getAll: () => {
+ return `SELECT trips.*, users.name username FROM trips INNER JOIN users ON (trips.user_id = users.iduser);`
+ },
+
+ getById: (id) => {
+ return {
+ text: `SELECT date, active, name, color FROM trips WHERE user_id=$1`,
+ values: [id]
+ }
+ },
+
+ addNew: ({
+ name,
+ color,
+ distance,
+ start_address,
+ end_address,
+ duration,
+ time
+ },
+ user_id) => {
+ return {
+ text: `INSERT INTO trips (user_id, name, color, distance, start_address, end_address, duration, time) VALUES($1, $2, $3, $4, $5, $6, $7, $8)`,
+ values: [user_id, name, color, distance, start_address, end_address, duration, time]
+ }
+ }
+}
diff --git a/server/models/userModel.js b/server/models/userModel.js
new file mode 100644
index 0000000..721a58d
--- /dev/null
+++ b/server/models/userModel.js
@@ -0,0 +1,75 @@
+const UserModel = {};
+
+UserModel.getUser = (idUser) => {
+ const query = {
+ text: `SELECT name, email, homePoint, driveHoursPerDay, driveStopPeriod, eatStopPeriod, online, acount_status, is_activated, activation_id, avatar FROM users WHERE idUser=${idUser}`,
+ };
+ return query;
+};
+
+UserModel.getAllUsers = () => {
+ const query = {
+ text: `SELECT iduser, name, avatar, acount_status, role FROM users`,
+ };
+ return query;
+};
+
+UserModel.deleteUser = (iduser) => {
+ const query = {
+ text: `DELETE FROM users WHERE iduser = ${iduser}`,
+ };
+ return query;
+};
+
+UserModel.unblockUser = (iduser, status) => {
+ const query = {
+ text: `UPDATE users SET acount_status = ${status} WHERE iduser = ${iduser}`,
+ };
+ return query;
+};
+
+UserModel.getUserCars = (idUser) => {
+ const query = {
+ text: `SELECT cars.idCar, cars.nameCar, cars.tankVolume, cars.maxPassengersCount, cars.avgGasCost, cars.baggageVolume, cars.avgSpeed FROM cars WHERE cars.userId = ${idUser} ORDER BY cars.idCar DESC`
+ };
+ return query;
+};
+
+UserModel.addNewCar = ({
+ iduser,
+ nameCar,
+ tankVolume,
+ maxPassengersCount,
+ avgGasCost,
+ baggageVolume,
+ avgSpeed
+}) => {
+ const query = {
+ text: `INSERT INTO cars (userId, nameCar, tankVolume, maxPassengersCount, avgGasCost, baggageVolume, avgSpeed) VALUES(${iduser}, '${nameCar}', ${tankVolume}, ${maxPassengersCount}, ${avgGasCost}, ${baggageVolume}, ${avgSpeed})`
+ };
+ return query;
+};
+
+UserModel.deleteCar = (idCar) => {
+ const query = {
+ text: `DELETE FROM cars WHERE cars.idCar = ${idCar}`
+ };
+ return query;
+};
+
+UserModel.updateCar = ({
+ idCar,
+ nameCar,
+ tankVolume,
+ maxPassengersCount,
+ avgGasCost,
+ baggageVolume,
+ avgSpeed
+}) => {
+ const query = {
+ text: `UPDATE cars SET nameCar = '${nameCar}', tankVolume = ${tankVolume}, maxPassengersCount = ${maxPassengersCount}, avgGasCost = ${avgGasCost}, baggageVolume = ${baggageVolume}, avgSpeed = ${avgSpeed} WHERE idCar = ${idCar}`
+ };
+ return query;
+}
+
+module.exports = UserModel;
diff --git a/server/routes/feedback.js b/server/routes/feedback.js
new file mode 100644
index 0000000..8947a2b
--- /dev/null
+++ b/server/routes/feedback.js
@@ -0,0 +1,11 @@
+const express = require('express');
+const FeedbackController = require('../controllers/feedback');
+
+
+const router = express.Router();
+
+
+router.get('/api/feedbacks/:id', FeedbackController.getFeedbackById);
+
+
+module.exports = router;
diff --git a/server/routes/signin.js b/server/routes/signin.js
new file mode 100644
index 0000000..980c05d
--- /dev/null
+++ b/server/routes/signin.js
@@ -0,0 +1,10 @@
+const express = require('express');
+const signinController = require('../controllers/signinController');
+
+const router = express.Router();
+
+router.post('/api/login', signinController.login);
+router.get('/api/is-auth', signinController.isAuth);
+router.get('/api/logout', signinController.logout);
+
+module.exports = router;
diff --git a/server/routes/signup.js b/server/routes/signup.js
new file mode 100644
index 0000000..be4f17c
--- /dev/null
+++ b/server/routes/signup.js
@@ -0,0 +1,10 @@
+const express = require('express');
+const signupController = require('../controllers/signupController');
+
+const router = express.Router();
+
+router.post('/api/checkEmailExistence', signupController.checkEmailExistence);
+router.post('/api/register', signupController.register);
+router.get('/api/register/:confirmEmail', signupController.confirmEmail);
+
+module.exports = router;
diff --git a/server/routes/trips.js b/server/routes/trips.js
new file mode 100644
index 0000000..dfe819d
--- /dev/null
+++ b/server/routes/trips.js
@@ -0,0 +1,13 @@
+const express = require('express');
+const TripsController = require('../controllers/trips');
+
+
+const router = express.Router();
+
+router.get('/api/trips/all', TripsController.getAllTrips);
+router.get('/api/trips/:id/byId', TripsController.getTripById);
+
+router.post('/api/trips/:id/addTrip', TripsController.addNewTrip);
+
+
+module.exports = router;
diff --git a/server/routes/user.js b/server/routes/user.js
new file mode 100644
index 0000000..d10c18d
--- /dev/null
+++ b/server/routes/user.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const userController = require('../controllers/userController');
+
+const router = express.Router();
+
+router.get('/api/user/:iduser', userController.getUser);
+router.get('/api/user/:iduser/cars', userController.getUserCars);
+router.get('/api/allUsers', userController.getAllUsers);
+
+router.post('/api/user/addCar', userController.addNewCar);
+router.post('/api/user/deleteCar', userController.deleteCar);
+router.post('/api/user/unblock', userController.unblockUser);
+router.post('/api/user/delete', userController.deleteUser);
+
+router.post('/api/user/updateCar', userController.updateCar);
+
+module.exports = router;
diff --git a/server/utils/PgBaseUtils.js b/server/utils/PgBaseUtils.js
new file mode 100644
index 0000000..ce58185
--- /dev/null
+++ b/server/utils/PgBaseUtils.js
@@ -0,0 +1,24 @@
+const db = require('../db');
+require('dotenv').config({path: 'server/.env'})
+
+module.exports = {
+ createTable: (data) => {
+ const query = 'CREATE TABLE IF NOT EXISTS';
+ data.forEach(({ tableName, column }) => db.query(`${query} ${tableName} (${column});`));
+ },
+ insert: (data) => {
+ const { table, fields, values } = data;
+ values.forEach(val => db.query(`INSERT INTO ${table} (${fields}) VALUES (${val})`));
+ },
+ reCreate: () => {
+ const { PGUSER } = process.env;
+ const SQL = `
+ DROP SCHEMA public CASCADE;
+ CREATE SCHEMA public;
+ GRANT ALL ON SCHEMA public TO ${PGUSER};
+ GRANT ALL ON SCHEMA public TO public;
+ `
+ db.query(SQL)
+ .catch(e => console.error(e));
+ }
+};
diff --git a/server/utils/scripts/createData.js b/server/utils/scripts/createData.js
new file mode 100644
index 0000000..b7efebd
--- /dev/null
+++ b/server/utils/scripts/createData.js
@@ -0,0 +1,60 @@
+module.exports = [
+ {
+ tableName: 'users',
+ column: `
+ idUser serial NOT NULL PRIMARY KEY,
+ name json NOT NULL,
+ passwordHash text NOT NULL,
+ email char(50) NOT NULL,
+ homePoint json,
+ driveHoursPerDay double precision,
+ driveStopPeriod double precision,
+ eatStopPeriod double precision,
+ online boolean,
+ acount_status boolean DEFAULT true,
+ is_activated boolean,
+ activation_id text,
+ avatar char(100) DEFAULT NULL,
+ role text DEFAULT 'user'
+ `
+ },
+ {
+ tableName: 'cars',
+ column: `
+ idCar SERIAL PRIMARY KEY NOT NULL,
+ userId INT NOT NULL,
+ nameCar CHAR(30),
+ tankVolume INT,
+ maxPassengersCount INT,
+ avgGasCost DECIMAL,
+ baggageVolume INT,
+ avgSpeed INT`
+ },
+ {
+ tableName: 'trips',
+ column: `
+ id SERIAL PRIMARY KEY NOT NULL,
+ user_id INT NOT NULL,
+ name TEXT,
+ color TEXT,
+ distance json,
+ start_address TEXT,
+ end_address TEXT,
+ duration TEXT,
+ time TEXT,
+ active boolean NOT NULL DEFAULT true,
+ date TIMESTAMPTZ NOT NULL DEFAULT NOW()
+ `
+ },
+ {
+ tableName: 'feedback',
+ column: `
+ id SERIAL PRIMARY KEY NOT NULL,
+ user_id INT,
+ trip_id INT,
+ create_by INT,
+ rating NUMERIC,
+ text TEXT
+ `
+ }
+];
\ No newline at end of file
diff --git a/server/utils/scripts/createTables.js b/server/utils/scripts/createTables.js
new file mode 100644
index 0000000..4a3ab9e
--- /dev/null
+++ b/server/utils/scripts/createTables.js
@@ -0,0 +1,6 @@
+const Query = require('../PgBaseUtils');
+const data = require('./createData');
+
+module.exports = (function create() {
+ Query.createTable(data);
+}());
diff --git a/server/utils/scripts/dbSeed.js b/server/utils/scripts/dbSeed.js
new file mode 100644
index 0000000..12c0aec
--- /dev/null
+++ b/server/utils/scripts/dbSeed.js
@@ -0,0 +1,8 @@
+const Query = require('../PgBaseUtils');
+const tablesData = require('./insertData');
+
+module.exports = (function seed() {
+ tablesData.forEach(data => {
+ Query.insert(data)
+ });
+}());
diff --git a/server/utils/scripts/insertData.js b/server/utils/scripts/insertData.js
new file mode 100644
index 0000000..8dad9ee
--- /dev/null
+++ b/server/utils/scripts/insertData.js
@@ -0,0 +1,50 @@
+
+const users = {
+ table: 'users',
+ fields: 'name, passwordHash, email, homePoint, driveHoursPerDay, driveStopPeriod, eatStopPeriod, online, acount_status, is_activated, activation_id, role',
+ values: [
+ `'{ "first": "Admin", "last": "Adminovich" }', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918', 'admin@email.com', null, null, null, null, true, true, true, 'qert45qtegwb34wb3645b645e45g4b5', 'admin'`,
+ `'{ "first": "Oneill", "last": "Long" }', '5b421eedc4235617a7ca1c0f', 'oneill.long@email.net', '{ "city": "Cutter", "position": ["-71.644535", "-32.810064"] }', 15, 7, 3.7, true, true, true, 'qert45qtegwb34wb3645b645e45g4b5', 'user'`,
+ `'{ "first": "Odessa", "last": "Mckee" }', '5b421eedc4235617a7ca1c0f', 'oneill.long@email.net', '{ "city": "Cutter", "position": ["-71.644535", "-32.810064"] }', 15, 7, 3.7, true, true, true, 'qert45qtegwb34wb3645b645e45g4b5', 'user'`,
+ `'{ "first": "Calhoun", "last": "Wilkerson" }', '5b421eedc4235617a7ca1c0f', 'oneill.long@email.net', '{ "city": "Cutter", "position": ["-71.644535", "-32.810064"] }', 15, 7, 3.7, true, true, true, 'qert45qtegwb34wb3645b645e45g4b5', 'user'`,
+ `'{ "first": "Rajesh", "last": "Koothrappali" }', '65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5', 'email@mail.com', '{ "city": "Cutter", "position": ["-71.644535", "-32.810064"] }', 15, 7, 3.7, true, true, true, 'qert45qtegwb34wb3645b645e45g4b5', 'user'`,
+ ]
+};
+
+const cars = {
+ table: 'cars',
+ fields: 'userId, nameCar, tankVolume, maxPassengersCount, avgGasCost, baggageVolume, avgSpeed',
+ values: [
+ `1, 'Peugeot', 50, 2, 7, 2, 80`,
+ `1, 'Ford', 50, 2, 7, 2, 80`,
+ `2, 'Peugeot', 50, 2, 7, 2, 80`,
+ `2, 'Ford', 50, 2, 7, 2, 80`,
+ ]
+}
+
+const trips = {
+ table: 'trips',
+ fields: 'user_id, name, color, distance, start_address, end_address, duration, time',
+ values: [
+ `1, 'Big trip to Lviv', 'teal', '{ "end": { "lat": 49.839683, "lng": 24.029717000000005 }, "start": { "lat": 50.5905728, "lng": 26.1526353 } }', 'М06 & Е40, Rivnenska oblast, Ukraine', 'Miskevycha Square, 9, Lviv, Lvivska oblast, Ukraine, 79000', '203 km', '2 hours 38 mins'`,
+ `1, 'Fun trip to Lviv', 'purple', '{ "end": { "lat": 49.839683, "lng": 24.029717000000005 }, "start": { "lat": 50.5905728, "lng": 26.1526353 } }', 'М06 & Е40, Rivnenska oblast, Ukraine', 'Miskevycha Square, 9, Lviv, Lvivska oblast, Ukraine, 79000', '203 km', '2 hours 38 mins'`,
+ `1, 'Fast trip to Lviv', 'green', '{ "end": { "lat": 49.839683, "lng": 24.029717000000005 }, "start": { "lat": 50.5905728, "lng": 26.1526353 } }', 'М06 & Е40, Rivnenska oblast, Ukraine', 'Miskevycha Square, 9, Lviv, Lvivska oblast, Ukraine, 79000', '203 km', '2 hours 38 mins'`
+ ]
+}
+
+const feedback = {
+ table: 'feedback',
+ fields: 'user_id, trip_id, create_by, rating, text',
+ values: [
+ `1, 2, 2, 4.7, 'The driver was drunk! I liked everything! Quisque varius imperdiet auctor. In et dui elit. Nam metus lorem, imperdiet iaculis imperdiet sit amet, ullamcorper quis ipsum. Sed sed nisi vitae magna vestibulum commodo.'`,
+ `6, 4, 3, 3, 'The driver was drunk! I liked everything! Quisque varius imperdiet auctor. In et dui elit. Nam metus lorem, imperdiet iaculis imperdiet sit amet, ullamcorper quis ipsum. Sed sed nisi vitae magna vestibulum commodo.'`,
+ `6, 5, 5, 5, 'The driver was drunk! I liked everything! Quisque varius imperdiet auctor. In et dui elit. Nam metus lorem, imperdiet iaculis imperdiet sit amet, ullamcorper quis ipsum. Sed sed nisi vitae magna vestibulum commodo.'`
+ ]
+}
+
+module.exports = [
+ users,
+ cars,
+ trips,
+ feedback
+];
diff --git a/server/utils/scripts/reCreateTables.js b/server/utils/scripts/reCreateTables.js
new file mode 100644
index 0000000..d259fa6
--- /dev/null
+++ b/server/utils/scripts/reCreateTables.js
@@ -0,0 +1,5 @@
+const Query = require('../PgBaseUtils');
+
+module.exports = (function drop() {
+ Query.reCreate();
+}());