├── src
├── js
│ ├── components
│ │ ├── core
│ │ │ ├── text-label.js
│ │ │ ├── link.js
│ │ │ ├── text-input.js
│ │ │ └── editable-text.js
│ │ ├── way-point-details.js
│ │ ├── way-point-info.js
│ │ ├── app-bar.js
│ │ ├── route.js
│ │ ├── routes.js
│ │ ├── way-point.js
│ │ ├── way-points.js
│ │ ├── route-info.js
│ │ ├── route-visual.js
│ │ └── map.js
│ ├── services
│ │ ├── storage.js
│ │ └── api.js
│ ├── middlewares
│ │ └── state-storage.js
│ ├── main.js
│ ├── reducers
│ │ ├── routes.js
│ │ ├── way-points.js
│ │ ├── way-point.js
│ │ ├── route.js
│ │ └── app.js
│ ├── util.js
│ ├── containers
│ │ ├── routes.js
│ │ └── way-points.js
│ ├── selectors
│ │ └── app.js
│ ├── actions
│ │ ├── types.js
│ │ └── index.js
│ ├── app.js
│ └── sagas
│ │ └── index.js
├── scss
│ ├── _way-point-details.scss
│ ├── _variables.scss
│ ├── fonts
│ │ ├── icomoon.eot
│ │ ├── icomoon.ttf
│ │ ├── icomoon.woff
│ │ └── icomoon.svg
│ ├── _text-input.scss
│ ├── _route.scss
│ ├── _app.scss
│ ├── _routes.scss
│ ├── _way-point-info.scss
│ ├── _route-info.scss
│ ├── _app-bar.scss
│ ├── _reset.scss
│ ├── _waypoints.scss
│ ├── _icons.scss
│ └── base.scss
├── img
│ ├── bg1.jpg
│ ├── bg10.jpg
│ ├── bg11.jpg
│ ├── bg12.jpg
│ ├── bg2.jpg
│ ├── bg3.jpg
│ ├── bg4.jpg
│ ├── bg5.jpg
│ ├── bg6.jpg
│ ├── bg7.jpg
│ ├── bg8.jpg
│ └── bg9.jpg
└── views
│ └── index.html
├── test
├── tests.webpack.js
└── hello-spec.js
├── webpack.test.js
├── .gitignore
├── webpack.prod.js
├── karma.conf.js
├── README.md
├── gruntfile.js
├── package.json
├── webpack.config.js
└── npm-shrinkwrap.json
/src/js/components/core/text-label.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/scss/_way-point-details.scss:
--------------------------------------------------------------------------------
1 | .way-point-details {
2 |
3 | }
--------------------------------------------------------------------------------
/src/img/bg1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg1.jpg
--------------------------------------------------------------------------------
/src/img/bg10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg10.jpg
--------------------------------------------------------------------------------
/src/img/bg11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg11.jpg
--------------------------------------------------------------------------------
/src/img/bg12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg12.jpg
--------------------------------------------------------------------------------
/src/img/bg2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg2.jpg
--------------------------------------------------------------------------------
/src/img/bg3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg3.jpg
--------------------------------------------------------------------------------
/src/img/bg4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg4.jpg
--------------------------------------------------------------------------------
/src/img/bg5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg5.jpg
--------------------------------------------------------------------------------
/src/img/bg6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg6.jpg
--------------------------------------------------------------------------------
/src/img/bg7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg7.jpg
--------------------------------------------------------------------------------
/src/img/bg8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg8.jpg
--------------------------------------------------------------------------------
/src/img/bg9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/img/bg9.jpg
--------------------------------------------------------------------------------
/src/scss/_variables.scss:
--------------------------------------------------------------------------------
1 | $pad: 10px;
2 | $widgetWidth: 400px;
3 | $textColor: #333333;
--------------------------------------------------------------------------------
/src/scss/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/scss/fonts/icomoon.eot
--------------------------------------------------------------------------------
/src/scss/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/scss/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/src/scss/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vraa/route-planner/HEAD/src/scss/fonts/icomoon.woff
--------------------------------------------------------------------------------
/test/tests.webpack.js:
--------------------------------------------------------------------------------
1 | var context = require.context(".", true, /-spec\.js$/);
2 | context.keys().forEach(context);
--------------------------------------------------------------------------------
/src/scss/_text-input.scss:
--------------------------------------------------------------------------------
1 | .text-input {
2 |
3 | input {
4 | background-color: transparent;
5 | }
6 |
7 | }
--------------------------------------------------------------------------------
/webpack.test.js:
--------------------------------------------------------------------------------
1 | let baseConfig = require('./webpack.config');
2 |
3 | module.exports = Object.assign({}, baseConfig, {});
--------------------------------------------------------------------------------
/test/hello-spec.js:
--------------------------------------------------------------------------------
1 | describe('hello test', () => {
2 | it('should pass', () => {
3 | expect(true).toBe(true);
4 | });
5 | });
--------------------------------------------------------------------------------
/src/scss/_route.scss:
--------------------------------------------------------------------------------
1 | .route {
2 | background-color: rgba(255,255,255,.8);
3 | }
4 |
5 | .route .route-name {
6 | margin-bottom: $pad;
7 | }
--------------------------------------------------------------------------------
/src/scss/_app.scss:
--------------------------------------------------------------------------------
1 | .app {
2 | background-size: cover;
3 | }
4 |
5 | .route-planner {
6 | display: grid;
7 | grid-template-rows: 100vh;
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**
2 | resources/*.html
3 | resources/js/*.*
4 | resources/css/*.css
5 | resources/css/*.css.map
6 | *.iml
7 | .sass-cache/**
8 | .idea
9 | bash.exe.stackdump
10 |
--------------------------------------------------------------------------------
/src/scss/_routes.scss:
--------------------------------------------------------------------------------
1 | .routes {
2 | display: grid;
3 | grid-template-columns: 1fr;
4 | grid-template-rows: 1fr 1fr;
5 | box-shadow: 0 0 5px #3d3d3d;
6 | }
7 |
8 | @media screen and (min-width: 600px) {
9 | .routes {
10 | grid-template-columns: 400px 1fr;
11 | grid-template-rows: 1fr;
12 | margin: 100px 10%;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/js/services/storage.js:
--------------------------------------------------------------------------------
1 | const db = window.localStorage;
2 |
3 | let storage = {
4 |
5 | set: (key, val) => {
6 | if (key && val) {
7 | db.setItem(key, val);
8 | }
9 | },
10 |
11 | get: (key) => {
12 | let val = db.getItem(key);
13 | return val ? JSON.parse(val) : null;
14 | }
15 | };
16 |
17 | export default storage;
--------------------------------------------------------------------------------
/src/js/components/way-point-details.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class WayPointDetails extends Component {
4 | render() {
5 | return (
6 |
7 |
Way Point Details
8 |
9 |
10 | )
11 | }
12 | }
13 |
14 | export default WayPointDetails;
--------------------------------------------------------------------------------
/src/js/components/core/link.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | class Link extends React.Component {
4 |
5 | handleClick(e) {
6 | e.preventDefault();
7 | this.props.onClick();
8 | }
9 |
10 | render() {
11 | return (
12 | {this.props.children}
13 | )
14 | }
15 | }
16 |
17 | module.exports = Link;
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | let baseConfig = require('./webpack.config');
2 | let webpack = require('webpack');
3 |
4 | module.exports = Object.assign({}, baseConfig, {
5 | plugins: [].concat(
6 | [
7 | new webpack.DefinePlugin({
8 | 'process.env': {
9 | 'NODE_ENV': JSON.stringify('production')
10 | }
11 | })
12 | ],
13 | baseConfig.plugins
14 | ),
15 | devtool: 'none'
16 | });
--------------------------------------------------------------------------------
/src/js/middlewares/state-storage.js:
--------------------------------------------------------------------------------
1 | import storage from "../services/storage";
2 |
3 | let DB_NAME = "appState";
4 |
5 | let stateStorage = ({getState}) => next => action => {
6 | let returnVal = next(action);
7 | if (action.saveState) {
8 | storage.set(DB_NAME, JSON.stringify(getState()));
9 | }
10 | return returnVal;
11 | };
12 |
13 | let createStorageMiddleware = (dbName) => {
14 | DB_NAME = dbName;
15 | return stateStorage;
16 | };
17 | export default createStorageMiddleware;
--------------------------------------------------------------------------------
/src/js/components/way-point-info.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | class WayPointInfo extends React.Component {
4 |
5 | render() {
6 | return (
7 |
8 | {this.props.distance}
9 | {this.props.duration}
10 |
11 | )
12 | }
13 |
14 | }
15 |
16 | module.exports = WayPointInfo;
--------------------------------------------------------------------------------
/src/js/main.js:
--------------------------------------------------------------------------------
1 | require('../scss/base.scss');
2 |
3 | let React = require('react');
4 | let ReactDOM = require('react-dom');
5 | let App = require('./app');
6 | let domReady = require('domready');
7 | let GoogleMaps = require('google-maps');
8 |
9 | domReady(()=> {
10 | GoogleMaps.LIBRARIES = ['places'];
11 | GoogleMaps.KEY='AIzaSyAkpb6H_VzRih_zDtUnFJF9tgEG46S3hqU';
12 | GoogleMaps.load((google)=> {
13 | ReactDOM.render(,
14 | document.getElementById('app'));
15 | });
16 |
17 | });
18 |
--------------------------------------------------------------------------------
/src/js/reducers/routes.js:
--------------------------------------------------------------------------------
1 | let Immutable = require('immutable');
2 | let ActionTypes = require('../actions/types');
3 | let Actions = require('../actions');
4 | let route = require('./route');
5 |
6 | let DEFAULTS = Immutable.List.of(route(undefined, Actions.addRoute(0)));
7 |
8 | const routes = (state = DEFAULTS, action) => {
9 | switch (action.type) {
10 | case ActionTypes.ADD_ROUTE:
11 | return state.push(route(undefined, action));
12 | case ActionTypes.ADD_WAY_POINT_TO_ROUTE:
13 | return state;
14 | default:
15 | return state;
16 | }
17 |
18 | };
19 |
20 | module.exports = routes;
--------------------------------------------------------------------------------
/src/scss/_way-point-info.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .way-point-info {
4 | margin-top: 2 * $pad;
5 | margin-left: 4 * $pad;
6 | font-size: .8rem;
7 | color: #6d6d6d;
8 |
9 | .distance,
10 | .duration {
11 | padding: 2px 5px;
12 | color: #fff;
13 | border:1px solid;
14 | border-radius: 3px;
15 | letter-spacing: 1px;
16 | }
17 |
18 | .distance {
19 | background-color: #F8BBD0;
20 | color: #880E4F;
21 | border-color: #F48FB1;
22 | }
23 |
24 | .duration {
25 | margin-left: 2 * $pad;
26 | background-color: #E1BEE7;
27 | color: #4A148C;
28 | border-color: #CE93D8;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/js/components/app-bar.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class AppBar extends Component {
4 |
5 | render() {
6 | return (
7 |
14 | )
15 | }
16 | }
17 |
18 | export default AppBar;
--------------------------------------------------------------------------------
/src/js/reducers/way-points.js:
--------------------------------------------------------------------------------
1 | var ActionTypes = require('../actions/types');
2 | var Actions = require('../actions');
3 | var wayPoint = require('./way-point');
4 |
5 | var DEFAULTS = [];
6 |
7 | const wayPoints = (state = DEFAULTS, action) => {
8 |
9 | switch (action.type) {
10 | case ActionTypes.ADD_WAY_POINT:
11 | return [
12 | ...state,
13 | wayPoint(undefined, action)
14 | ];
15 | break;
16 | case ActionTypes.REMOVE_WAY_POINT:
17 | return state.filter((wp)=> {
18 | return wp.id !== action.id
19 | });
20 | default:
21 | return state;
22 | }
23 |
24 | };
25 |
26 | module.exports = wayPoints;
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 |
3 | config.set({
4 |
5 | captureTimeout: 60000,
6 | browserNoActivityTimeout: 60000,
7 | colors: true,
8 | port: 9876,
9 |
10 | files: [
11 | './test/tests.webpack.js'
12 | ],
13 |
14 | preprocessors: {
15 | './test/tests.webpack.js': ['webpack']
16 | },
17 | browsers: ['Chrome'],
18 |
19 | singleRun: true,
20 |
21 | autoWatch: false,
22 |
23 | frameworks: ['jasmine'],
24 |
25 | webpack: {},
26 |
27 | webpackMiddleware: {
28 | noInfo: true,
29 | stats: {
30 | colors: true
31 | }
32 | }
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/src/js/util.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | var Util = {
4 | guid: () => {
5 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
6 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
7 | return v.toString(16);
8 | });
9 | },
10 |
11 | transformSavedState: (st) => {
12 | if (!st) {
13 | return null;
14 | }
15 | let routes = st.routes.map((r) => {
16 | return Object.assign({}, r, {
17 | wayPoints: Immutable.List(r.wayPoints)
18 | });
19 | });
20 | let transformed = Object.assign({}, st, {
21 | routes: Immutable.List(routes),
22 | wayPoints: Immutable.List(st.wayPoints)
23 | });
24 | return transformed;
25 | }
26 | };
27 |
28 | module.exports = Util;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Route Planner using Google Maps, built with React JS, Redux and Immutable.js
2 |
3 | **[Route Planner](http://veerasundar.com/route-planner)** is a simple app that helps you to plan your travel routes.
4 |
5 | * Add upto 8 way points between start and end destinations of your route.
6 | * See distance and time details for each way points and whole route.
7 |
8 | ## How to run locally?
9 |
10 | 1. Clone and cd into the repo.
11 | 2. Run `npm install` to download dependencies.
12 | 3. Run `npm start` to start the development server.
13 | 4. Open your browser and point to http://localhost:8080/
14 |
15 | The application has a web pack dev server, so as you make your changes in your code, the changes will be immeditely reflected in your browser.
16 |
17 | ## Screenshot
18 |
19 | [](http://veerasundar.com/route-planner)
20 |
21 |
--------------------------------------------------------------------------------
/src/js/reducers/way-point.js:
--------------------------------------------------------------------------------
1 | var ActionTypes = require('../actions/types');
2 | var util = require('../util');
3 | let DEFAULT_NAME = '';
4 |
5 | const wayPoint = (state = {}, action) => {
6 | switch (action.type) {
7 | case ActionTypes.ADD_WAY_POINT:
8 | return {
9 | id: util.guid(),
10 | name: DEFAULT_NAME
11 | };
12 | case ActionTypes.CHANGE_WAY_POINT_NAME:
13 | return Object.assign({}, state, {
14 | name: (action.newName.trim() || DEFAULT_NAME)
15 | });
16 | case ActionTypes.OPEN_WAY_POINT_DETAILS:
17 | return Object.assign({}, state, {
18 | detailsOpen: true
19 | });
20 | case ActionTypes.CLOSE_WAY_POINT_DETAILS:
21 | return Object.assign({}, state, {
22 | detailsOpen: false
23 | });
24 | default:
25 | return state;
26 | }
27 | };
28 |
29 | module.exports = wayPoint;
--------------------------------------------------------------------------------
/src/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Route Planner - Google Maps
9 |
10 |
11 |
12 |
13 |
14 |
24 |
25 |
--------------------------------------------------------------------------------
/src/scss/_route-info.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .route-info {
4 | display: grid;
5 | grid-template-columns: 1fr;
6 | justify-content: center;
7 | height: 130px;
8 | width: 90%;
9 | margin: 0 auto;
10 | padding: 20px 0;
11 | font-weight: 300;
12 |
13 | h2 {
14 | font-size: .6rem;
15 | text-transform: uppercase;
16 | letter-spacing: 2pt;
17 | margin-bottom: 10px;
18 | }
19 |
20 | .route-visual {
21 | text-align: center;
22 | margin-top: 20px;
23 | }
24 |
25 | .textual {
26 | display: grid;
27 | grid-template-columns: 1fr 1fr;
28 | }
29 |
30 | .value {
31 | font-size: 2.2rem;
32 | font-weight: 300;
33 | }
34 |
35 | .unit {
36 | font-size: .9rem;
37 | text-transform: lowercase;
38 | letter-spacing: 1pt;
39 | }
40 |
41 | .distance {
42 | text-align: center;
43 | color: #249991;
44 | }
45 |
46 | .duration {
47 | text-align: center;
48 | color: #953163;
49 | li {
50 | display: inline-block;
51 | margin-right: 2* $pad;
52 |
53 | &:last-child {
54 | margin-right: 0;
55 | }
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/src/js/components/route.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var EditableText = require('./core/editable-text');
3 | var RouteInfo = require('./route-info');
4 | var WayPoints = require('../containers/way-points');
5 | import AppBar from "./app-bar";
6 |
7 | class Route extends React.Component {
8 |
9 | constructor(props) {
10 | super(props);
11 | }
12 |
13 | handleRouteNameChange(newName) {
14 | this.props.onNameChange(this.props.route.id, newName);
15 | }
16 |
17 |
18 | renderRouteName() {
19 | var route = this.props.route;
20 | return (
21 |
22 |
25 |
26 | )
27 | }
28 |
29 | render() {
30 | var route = this.props.route;
31 | return (
32 |
37 | )
38 |
39 | }
40 | }
41 |
42 | module.exports = Route;
--------------------------------------------------------------------------------
/src/scss/_app-bar.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .app-bar {
4 | display: grid;
5 | grid-template-columns: 1fr 1fr;
6 | padding: 10px;
7 | }
8 |
9 | .app-bar h1 {
10 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHRpdGxlLz48cGF0aCBkPSJNMjU2IDBDMTY3LjYzNCAwIDk2IDcxLjYzNCA5NiAxNjBjMCAxNjAgMTYwIDM1MiAxNjAgMzUyczE2MC0xOTIgMTYwLTM1MkM0MTYgNzEuNjM0IDM0NC4zNjUgMCAyNTYgMHptMCAyNThjLTU0LjEyNCAwLTk4LTQzLjg3Ni05OC05OHM0My44NzYtOTggOTgtOTggOTggNDMuODc2IDk4IDk4LTQzLjg3NiA5OC05OCA5OHptLTYyLTk4YzAtMzQuMjQyIDI3Ljc1OC02MiA2Mi02MnM2MiAyNy43NTggNjIgNjJjMCAzNC4yNDItMjcuNzYgNjItNjIgNjJzLTYyLTI3Ljc1OC02Mi02MnoiLz48L3N2Zz4=');
11 | background-position: top left;
12 | background-size: 20px;
13 | background-repeat: no-repeat;
14 | font-size: .8rem;
15 | text-transform: uppercase;
16 | color: #000;
17 | font-weight: bold;
18 | letter-spacing: 2pt;
19 | padding-left: 25px;
20 | }
21 |
22 | .app-bar h1 a {
23 | text-decoration: none;
24 | color: #000;
25 | }
26 |
27 | .app-bar .author-details {
28 | font-size: .8rem;
29 | color: #6d6d6d;
30 | text-align: right;
31 | }
32 |
33 | .app-bar a {
34 | color: #4d4d4d;
35 | }
--------------------------------------------------------------------------------
/src/js/containers/routes.js:
--------------------------------------------------------------------------------
1 | var {connect} = require('react-redux');
2 | var Actions = require('../actions');
3 | var Routes = require('../components/routes');
4 |
5 | const getActiveRoute = (state) => {
6 | if (state && state.routes) {
7 | return state.routes.find((r) => {
8 | return r.id === state.activeRouteID;
9 | });
10 | }
11 | };
12 |
13 | const mapStateToProps = (state, ownProps) => {
14 |
15 | return {
16 | routes: state.routes,
17 | activeRoute: getActiveRoute(state),
18 | mapService: ownProps.mapService,
19 | mapData: state.mapData
20 | }
21 | };
22 |
23 | const mapDispatchToProps = (dispatch) => {
24 | return {
25 | onAdd: () => {
26 | dispatch(Actions.addRouteRequested())
27 | },
28 | onRemove: (id) => {
29 | dispatch(Actions.removeRoute(id))
30 | },
31 | onChangeRoute: (id) => {
32 | dispatch(Actions.changeActiveRoute(id))
33 | },
34 | onChangeRouteName: (routeId, newName) => {
35 | dispatch(Actions.changeRouteName(routeId, newName))
36 | }
37 | }
38 | };
39 |
40 | const RoutesContainer = connect(
41 | mapStateToProps,
42 | mapDispatchToProps
43 | )(Routes);
44 |
45 | module.exports = RoutesContainer;
46 |
--------------------------------------------------------------------------------
/src/scss/_reset.scss:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 | /* HTML5 display-role reset for older browsers */
27 | article, aside, details, figcaption, figure,
28 | footer, header, hgroup, menu, nav, section {
29 | display: block;
30 | }
31 | body {
32 | line-height: 1;
33 | }
34 | ol, ul {
35 | list-style: none;
36 | }
37 | blockquote, q {
38 | quotes: none;
39 | }
40 | blockquote:before, blockquote:after,
41 | q:before, q:after {
42 | content: '';
43 | content: none;
44 | }
45 | table {
46 | border-collapse: collapse;
47 | border-spacing: 0;
48 | }
--------------------------------------------------------------------------------
/src/scss/_waypoints.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | ul.way-points {
4 | margin: 2 * $pad;
5 | position: relative;
6 | max-height: 300px;
7 | overflow: auto;
8 | a {
9 | text-decoration: none;
10 | border-bottom: 1px solid #19B5FE;
11 | &:link, &:hover, &:visited, &:active {
12 | color: $textColor;
13 | }
14 | }
15 |
16 | &.empty a {
17 | margin-left: 15px;
18 | }
19 |
20 | li {
21 | margin-bottom: 2 * $pad;
22 | position: relative;
23 | z-index: 10;
24 |
25 | .way-point-icon {
26 | position: absolute;
27 | top: 0;
28 | left: 0;
29 | color: #19B5FE;
30 | z-index: 10;
31 | }
32 |
33 | &:first-child .way-point-icon {
34 | color: #2ECC71;
35 | }
36 |
37 | &:last-child .way-point-icon {
38 | color: #CF000F;
39 | }
40 |
41 | &.add-place {
42 | margin-top: 4 * $pad;
43 | }
44 | }
45 |
46 | &:before {
47 | content: '';
48 | position: absolute;
49 | top: 0;
50 | left: 8px;
51 | height: 100%;
52 | width: 1px;
53 | border-left: 1px dashed #ddd;
54 | z-index: 5;
55 | }
56 |
57 | &.empty:before {
58 | content: none;
59 | }
60 |
61 | &.max-way-points {
62 | .add-place,
63 | .insert-way-point {
64 | display: none;
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/src/scss/_icons.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'icomoon';
3 | src: url('./fonts/icomoon.eot?rgc6vh');
4 | src: url('./fonts/icomoon.eot?rgc6vh#iefix') format('embedded-opentype'),
5 | url('./fonts/icomoon.ttf?rgc6vh') format('truetype'),
6 | url('./fonts/icomoon.woff?rgc6vh') format('woff'),
7 | url('./fonts/icomoon.svg?rgc6vh#icomoon') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | }
11 |
12 | [class^="icon-"], [class*=" icon-"] {
13 | /* use !important to prevent issues with browser extensions that change fonts */
14 | font-family: 'icomoon' !important;
15 | speak: none;
16 | font-style: normal;
17 | font-weight: normal;
18 | font-variant: normal;
19 | text-transform: none;
20 | line-height: 1;
21 |
22 | /* Better Font Rendering =========== */
23 | -webkit-font-smoothing: antialiased;
24 | -moz-osx-font-smoothing: grayscale;
25 | }
26 |
27 | .icon-directions_car:before {
28 | content: "\e901";
29 | }
30 | .icon-clock:before {
31 | content: "\e014";
32 | }
33 | .icon-disc:before {
34 | content: "\e019";
35 | }
36 | .icon-map:before {
37 | content: "\e072";
38 | }
39 | .icon-flag:before {
40 | content: "\e108";
41 | }
42 | .icon-trash:before {
43 | content: "\e109";
44 | }
45 | .icon-plus:before {
46 | content: "\e114";
47 | }
48 | .icon-minus:before {
49 | content: "\e115";
50 | }
51 | .icon-cross:before {
52 | content: "\e117";
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 |
3 | grunt.initConfig({
4 | pkg: grunt.file.readJSON('package.json'),
5 | watch: {
6 | options: {
7 | atBegin: true
8 | },
9 | sass: {
10 | files: ['src/scss/**/*.scss'],
11 | tasks: ['sass']
12 | }
13 | },
14 | browserify: {
15 | publish: {
16 | files: {
17 | 'resources/js/routeplanner.js': ['src/js/index.js']
18 | },
19 | options : {
20 | transform : ['reactify']
21 | }
22 | }
23 | },
24 | sass: {
25 | dist: {
26 | files: {
27 | 'resources/css/main.css': 'src/scss/main.scss'
28 | }
29 | }
30 | },
31 | connect: {
32 | server: {
33 | options: {
34 | port: 8888,
35 | keepalive: true
36 | }
37 | },
38 | keepalive: true
39 | }
40 | });
41 |
42 | grunt.loadNpmTasks('grunt-contrib-sass');
43 | grunt.loadNpmTasks('grunt-contrib-watch');
44 | grunt.loadNpmTasks('grunt-contrib-connect');
45 | grunt.loadNpmTasks('grunt-browserify');
46 |
47 | grunt.registerTask('default', ['sass','browserify:publish']);
48 |
49 | }
--------------------------------------------------------------------------------
/src/js/selectors/app.js:
--------------------------------------------------------------------------------
1 | var AppSelectors = {
2 |
3 | recentlyAddedRoute: (state) => {
4 | let routes = state.routes;
5 | if (routes.size > 0) {
6 | return routes.last();
7 | }
8 | },
9 |
10 | firstRoute: (state) => {
11 | let routes = state.routes;
12 | if (routes.size > 0) {
13 | return routes.first();
14 | }
15 | },
16 |
17 | recentlyAddedWayPoint: (state) => {
18 | let wp = state.wayPoints;
19 | if (wp.length > 0) {
20 | return wp[wp.length - 1];
21 | }
22 | },
23 |
24 | activeRouteID: (state) => {
25 | return state.activeRouteID;
26 | },
27 |
28 | activeRoute: (state) => {
29 | if (state.routes) {
30 | let activeRouteID = state.activeRouteID;
31 | return state.routes.find((r) => r.id === activeRouteID);
32 | }
33 | },
34 |
35 | activeWayPoints: (state) => {
36 | let wayPoints = [];
37 | let activeRouteID = state.activeRouteID;
38 | let activeRoute = state.routes.find((r) => r.id === activeRouteID);
39 |
40 | if (activeRoute) {
41 | activeRoute.wayPoints.forEach((wpID) => {
42 | let wayPoint = state.wayPoints.find((wp) => wp.id === wpID);
43 | if (wayPoint) {
44 | wayPoints.push(wayPoint);
45 | }
46 | });
47 | }
48 | return wayPoints;
49 | }
50 |
51 |
52 | };
53 |
54 | module.exports = AppSelectors;
--------------------------------------------------------------------------------
/src/js/actions/types.js:
--------------------------------------------------------------------------------
1 | var ActionTypes = {
2 |
3 | ADD_ROUTE: 'ADD_ROUTE',
4 | ADD_ROUTE_REQUESTED: 'ADD_ROUTE_REQUESTED',
5 | REMOVE_ROUTE: 'REMOVE_ROUTE',
6 | REMOVE_ROUTE_REQUESTED: 'REMOVE_ROUTE_REQUESTED',
7 | CHANGE_ROUTE_NAME: 'CHANGE_ROUTE_NAME',
8 | CHANGE_ACTIVE_ROUTE: 'CHANGE_ACTIVE_ROUTE',
9 | ADD_WAY_POINT_TO_ROUTE: 'ADD_WAY_POINT_TO_ROUTE',
10 | REMOVE_WAY_POINT_FROM_ROUTE: 'REMOVE_WAY_POINT_FROM_ROUTE',
11 |
12 | ADD_WAY_POINT: 'ADD_WAY_POINT',
13 | ADD_WAY_POINT_REQUESTED: 'ADD_WAY_POINT_REQUESTED',
14 | ADD_WAY_POINT_SUCCEEDED: 'ADD_WAY_POINT_SUCCEEDED',
15 | REMOVE_WAY_POINT: 'REMOVE_WAY_POINT',
16 | REMOVE_WAY_POINT_REQUESTED: 'REMOVE_WAY_POINT_REQUESTED',
17 | REMOVE_WAY_POINT_SUCCEEDED: 'REMOVE_WAY_POINT_SUCCEEDED',
18 | CHANGE_WAY_POINT_NAME: 'CHANGE_WAY_POINT_NAME',
19 | CHANGE_WAY_POINT_NAME_REQUESTED: 'CHANGE_WAY_POINT_NAME_REQUESTED',
20 | CHANGE_WAY_POINT_NAME_SUCCEEDED: 'CHANGE_WAY_POINT_NAME_SUCCEEDED',
21 | SET_EDITING_WAY_POINT: 'SET_EDITING_WAY_POINT',
22 | UNSET_EDITING_WAY_POINT: 'UNSET_EDITING_WAY_POINT',
23 | OPEN_WAY_POINT_DETAILS: 'OPEN_WAY_POINT_DETAILS',
24 | CLOSE_WAY_POINT_DETAILS: 'CLOSE_WAY_POINT_DETAILS',
25 |
26 | REFRESH_MAP: 'REFRESH_MAP',
27 | LOAD_MAP: 'LOAD_MAP',
28 |
29 | API_FETCH_ROUTES: 'API_FETCH_ROUTES',
30 | API_FETCH_ROUTES_REQUESTED: 'API_FETCH_ROUTES_REQUESTED',
31 | API_FETCH_ROUTES_SUCCEEDED: 'API_FETCH_ROUTES_SUCCEEDED',
32 | API_FETCH_ROUTES_FAILED: 'API_FETCH_ROUTES_FAILED'
33 | };
34 |
35 | module.exports = ActionTypes;
--------------------------------------------------------------------------------
/src/js/services/api.js:
--------------------------------------------------------------------------------
1 | let mapService;
2 | let directionsService;
3 |
4 | let API = {
5 |
6 | initMapService: (google) => {
7 | mapService = google;
8 | directionsService = new mapService.maps.DirectionsService();
9 | },
10 |
11 | fetchRoutes: (wayPoints) => {
12 | return new Promise((resolve, reject)=> {
13 | if (wayPoints.length < 2) {
14 | resolve();
15 | } else {
16 | let request = {
17 | origin: wayPoints[0].name,
18 | destination: wayPoints[wayPoints.length - 1].name,
19 | travelMode: mapService.maps.TravelMode.DRIVING
20 | };
21 | let wayPointsInBetween = [];
22 | let noOfWayPointsInBetween = wayPoints.length - 2;
23 |
24 | if (noOfWayPointsInBetween > 0) {
25 | for (let i = 1; i <= noOfWayPointsInBetween; i++) {
26 | wayPointsInBetween.push({location: wayPoints[i].name});
27 | }
28 | request.waypoints = wayPointsInBetween;
29 | }
30 |
31 | directionsService.route(request, (response, status) => {
32 | if (status === 'OK') {
33 | resolve({
34 | response,
35 | status
36 | });
37 | } else {
38 | reject(status);
39 | }
40 | });
41 | }
42 | });
43 | }
44 |
45 | };
46 |
47 | module.exports = API;
--------------------------------------------------------------------------------
/src/js/reducers/route.js:
--------------------------------------------------------------------------------
1 | var ActionTypes = require('../actions/types');
2 | var util = require('../util');
3 | var Immutable = require('immutable');
4 |
5 | const route = (state = {}, action) => {
6 |
7 | switch (action.type) {
8 | case ActionTypes.ADD_ROUTE:
9 | return {
10 | id: util.guid(),
11 | name: 'New Route',
12 | wayPoints: Immutable.List()
13 | };
14 | case ActionTypes.ADD_WAY_POINT_TO_ROUTE:
15 | let wayPoints = state.wayPoints;
16 | let pos = wayPoints.findIndex((wp)=> wp === action.tgtWayPoint);
17 | if (pos === -1) {
18 | wayPoints = wayPoints.push(action.newWayPoint);
19 | } else {
20 | wayPoints = wayPoints.insert(pos + 1, action.newWayPoint);
21 | }
22 | return Object.assign({}, state, {
23 | wayPoints: wayPoints
24 | });
25 | case ActionTypes.REMOVE_WAY_POINT_FROM_ROUTE:
26 | return Object.assign({}, state, {
27 | wayPoints: state.wayPoints.filter((wp)=> {
28 | return wp !== action.wayPointID;
29 | })
30 | });
31 | case ActionTypes.CHANGE_ROUTE_NAME:
32 | return Object.assign({}, state, {
33 | name: action.newName
34 | });
35 | case ActionTypes.API_FETCH_ROUTES_SUCCEEDED:
36 | return Object.assign({}, state, {
37 | directions: action.routes
38 | });
39 | default:
40 | return state;
41 | }
42 | };
43 |
44 | module.exports = route;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "routeplanner",
3 | "version": "2.0.0",
4 | "description": "Route Planner using Google Maps",
5 | "main": "index.js",
6 | "author": "Veera",
7 | "scripts": {
8 | "start": "webpack-dev-server --inline --colors --content-base resources/",
9 | "dev": "npm run start",
10 | "build": "webpack --config ./webpack.config.js",
11 | "build-prod": "rm -rf ./resources && webpack --config ./webpack.prod.js --optimize-minimize ",
12 | "test": "karma start"
13 | },
14 | "devDependencies": {
15 | "babel-core": "6.23.1",
16 | "babel-loader": "6.3.2",
17 | "babel-polyfill": "6.23.0",
18 | "babel-preset-es2015": "6.22.0",
19 | "babel-preset-react": "6.23.0",
20 | "babel-preset-stage-0": "6.22.0",
21 | "css-loader": "0.26.1",
22 | "file-loader": "0.10.0",
23 | "html-loader": "^0.4.4",
24 | "html-webpack-harddisk-plugin": "^0.1.0",
25 | "html-webpack-plugin": "^2.28.0",
26 | "jasmine-core": "^2.5.2",
27 | "karma": "^1.6.0",
28 | "karma-chrome-launcher": "^2.0.0",
29 | "karma-jasmine": "^1.1.0",
30 | "karma-phantomjs-launcher": "^1.0.4",
31 | "karma-webpack": "^2.0.3",
32 | "node-sass": "4.5.0",
33 | "sass-loader": "6.0.1",
34 | "style-loader": "0.13.1",
35 | "url-loader": "0.5.7",
36 | "webpack": "2.2.1",
37 | "webpack-dev-server": "2.4.1"
38 | },
39 | "dependencies": {
40 | "classnames": "2.2.5",
41 | "domready": "1.0.8",
42 | "google-maps": "3.2.1",
43 | "immutable": "3.8.1",
44 | "lodash": "4.17.4",
45 | "react": "15.4.2",
46 | "react-dom": "15.4.2",
47 | "react-redux": "5.0.2",
48 | "redux": "3.6.0",
49 | "redux-saga": "0.14.3"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/js/components/core/text-input.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var classNames = require('classnames');
3 |
4 | const KEYS = {
5 | TAB: 9,
6 | ENTER: 13,
7 | ESC: 27
8 | };
9 |
10 | class TextInput extends React.Component {
11 |
12 | constructor(props) {
13 | super(props);
14 | }
15 |
16 | handleKeyDown(e) {
17 | switch (e.which) {
18 | case KEYS.ENTER:
19 | case KEYS.TAB:
20 | this.props.onSave(e.target.value);
21 | break;
22 | case KEYS.ESC:
23 | this.props.onCancel(this.props.oldValue);
24 | default:
25 |
26 | }
27 | }
28 |
29 | handleBlur() {
30 | this.props.onCancelEdit();
31 | }
32 |
33 | focus(input) {
34 | if (this.props.domElm) {
35 | this.props.domElm(input);
36 | }
37 | if (input != null) {
38 | input.focus();
39 | }
40 | }
41 |
42 | render() {
43 | var cx = classNames('text-input', this.props.className);
44 | return (
45 |
46 |
53 |
54 | )
55 | }
56 | }
57 |
58 | TextInput.defaultProps = {
59 | value: '',
60 | onSave: ()=> {
61 | },
62 | onCancel: (oldValue)=> {
63 | }
64 | };
65 |
66 | module.exports = TextInput;
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | let path = require('path');
2 | let HtmlWebpackPlugin = require('html-webpack-plugin');
3 | let HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
4 |
5 | module.exports = {
6 | entry: ['babel-polyfill', './src/js/main.js'],
7 | output: {
8 | path: path.join(__dirname, 'resources', 'js'),
9 | filename: 'bundle.[hash].js',
10 | publicPath: 'resources'
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.js$/,
16 | exclude: /node_modules/,
17 | use: [
18 | {
19 | loader: 'babel-loader',
20 | options: {
21 | presets: ['es2015', 'react']
22 | }
23 | }
24 | ]
25 | },
26 | {
27 | test: /\.scss$/,
28 | use: ['style-loader', 'css-loader', 'sass-loader']
29 | },
30 | {
31 | test: /\.woff(2)?(\?[a-z0-9]+)?$/,
32 | use: {
33 | loader: "url-loader?limit=10000&mimetype=application/font-woff"
34 | }
35 | }, {
36 | test: /\.(ttf|eot|svg)(\?[a-z0-9]+)?$/,
37 | use: ["file-loader"]
38 | }, {
39 | test: /\.html$/,
40 | use: {
41 | loader: 'html-loader'
42 | }
43 | }
44 | ]
45 | },
46 | plugins: [
47 | new HtmlWebpackPlugin({
48 | template: 'src/views/index.html',
49 | filename: '../index.html',
50 | alwaysWriteToDisk: true
51 | }),
52 | new HtmlWebpackHarddiskPlugin()
53 | ],
54 | devtool: 'source-map'
55 | };
--------------------------------------------------------------------------------
/src/js/components/routes.js:
--------------------------------------------------------------------------------
1 | let React = require('react');
2 | let Link = require('./core/link');
3 | let Route = require('./route');
4 | let Map = require('./map');
5 |
6 |
7 | class Routes extends React.Component {
8 |
9 | shouldComponentUpdate(nextProps) {
10 | return (this.props.routes == nextProps.routes);
11 | }
12 |
13 | handleAddRoute(e) {
14 | e.preventDefault();
15 | this.props.onAdd();
16 | }
17 |
18 | handleChangeRoute(id) {
19 | this.props.onChangeRoute(id);
20 | }
21 |
22 | handleChangeRouteName(routeId, newName) {
23 | this.props.onChangeRouteName(routeId, newName);
24 | }
25 |
26 | renderRouteTabs() {
27 | return (
28 |
29 | {
30 | this.props.routes.map((r, idx) => {
31 | return (
32 | -
33 | {idx + 1}
34 |
35 | )
36 | })
37 | }
38 | -
39 | + Add Route
40 |
41 |
42 | )
43 | }
44 |
45 | render() {
46 |
47 | return (
48 |
49 |
54 |
56 | );
57 | }
58 | }
59 | ;
60 |
61 | module.exports = Routes;
--------------------------------------------------------------------------------
/src/js/containers/way-points.js:
--------------------------------------------------------------------------------
1 | let {connect} = require('react-redux');
2 | let Actions = require('../actions');
3 | let WayPoints = require('../components/way-points');
4 | let AppSelector = require('../selectors/app');
5 |
6 | const getWayPointsToShow = (state) => {
7 | let activeRoute = AppSelector.activeRoute(state);
8 | let activeWayPointsIDs = activeRoute.wayPoints;
9 | let wayPoints = [];
10 |
11 | if (activeWayPointsIDs.size === 0) {
12 | return [];
13 | } else {
14 | activeWayPointsIDs.forEach((wpID) => {
15 | let wp = state.wayPoints.find((wp) => wp.id === wpID);
16 | if (wp) {
17 | wayPoints.push(wp);
18 | }
19 | });
20 |
21 | return wayPoints;
22 | }
23 | }
24 |
25 | const mapStateToProps = (state, ownProps) => {
26 | return {
27 | editingWayPoint: state.editingWayPoint,
28 | wayPoints: getWayPointsToShow(state),
29 | mapService: ownProps.mapService,
30 | route: ownProps.route
31 | }
32 | };
33 |
34 | const mapDispatchToProps = (dispatch, ownProps) => {
35 | return {
36 | onDetailsOpen: (id) => {
37 | dispatch(Actions.openWayPointDetails(id));
38 | },
39 | onDetailsClose: (id) => {
40 | dispatch(Actions.closeWayPointDetails(id));
41 | },
42 | onAdd: (id) => {
43 | dispatch(Actions.addWayPointRequested(id));
44 | },
45 | onRemove: (id) => {
46 | dispatch(Actions.removeWayPointRequested(id));
47 | },
48 | onChangeWayPointName: (wayPointID, newName) => {
49 | dispatch(Actions.changeWayPointNameRequested(wayPointID, newName, ownProps.mapService));
50 | }
51 | }
52 | };
53 |
54 | const WayPointsContainer = connect(
55 | mapStateToProps,
56 | mapDispatchToProps
57 | )(WayPoints);
58 |
59 | module.exports = WayPointsContainer;
--------------------------------------------------------------------------------
/src/js/components/core/editable-text.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var classNames = require('classnames');
3 | var TextInput = require('./text-input');
4 |
5 | class EditableText extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | editing: false
11 | }
12 | }
13 |
14 | componentDidMount() {
15 | if (this.props.edit) {
16 | this.setState({
17 | editing: true
18 | });
19 | }
20 | }
21 |
22 | edit() {
23 | this.setState({
24 | editing: true
25 | });
26 | }
27 |
28 | save(value) {
29 | if (this.props.onSave) {
30 | this.props.onSave(value);
31 | }
32 | this.setState({
33 | editing: false
34 | });
35 | }
36 |
37 | cancel() {
38 | if (this.props.onSave) {
39 | this.props.onSave(this.props.value);
40 | }
41 | this.cancelEdit();
42 | }
43 |
44 | cancelEdit() {
45 | this.setState({
46 | editing: false
47 | });
48 | }
49 |
50 | render() {
51 | var textInputElm;
52 | var cx = classNames('editable-text', this.props.className, {editing: this.state.editing});
53 | var cxLabel = classNames('text-label');
54 |
55 | if (this.state.editing) {
56 | textInputElm =
64 | } else {
65 | textInputElm = {this.props.value}
66 | }
67 |
68 |
69 | return (
70 |
71 | {textInputElm}
72 |
73 | )
74 | }
75 | }
76 |
77 | EditableText.defaultProps = {
78 | value: 'Editable text'
79 | }
80 |
81 | module.exports = EditableText;
--------------------------------------------------------------------------------
/src/js/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createSagaMiddleware from "redux-saga";
3 | import createStorageMiddleware from "./middlewares/state-storage";
4 | import Actions from "./actions";
5 |
6 | let {Provider} = require('react-redux');
7 | let {createStore, applyMiddleware, compose} = require('redux');
8 | let appReducer = require('./reducers/app');
9 | let Routes = require('./containers/routes');
10 |
11 | let rootSagas = require('./sagas');
12 | let APIService = require('./services/api');
13 |
14 | const TOTAL_BACKGROUND_IMAGES = 12;
15 | const DB_NAME = 'appState';
16 |
17 | const sagaMiddleware = createSagaMiddleware();
18 | const storageMiddleware = createStorageMiddleware(DB_NAME);
19 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
20 |
21 | const store = createStore(
22 | appReducer,
23 | composeEnhancers(applyMiddleware(sagaMiddleware, storageMiddleware))
24 | );
25 | sagaMiddleware.run(rootSagas);
26 |
27 | class App extends React.Component {
28 |
29 | componentWillMount() {
30 | APIService.initMapService(this.props.mapService);
31 | this.loadBackground = this.loadBackground.bind(this);
32 | this.applyBackground = this.applyBackground.bind(this);
33 | }
34 |
35 | componentDidMount() {
36 | this.loadBackground();
37 | store.dispatch(Actions.loadMap());
38 | }
39 |
40 | loadBackground() {
41 | let img = new Image();
42 | let location = window.location;
43 | let randomBg = Math.floor(Math.random() * (TOTAL_BACKGROUND_IMAGES - 1 + 1)) + 1;
44 | img.onload = () => {
45 | this.applyBackground(img);
46 | };
47 | img.src = `${location.protocol}//${location.hostname}${location.pathname}/img/bg${randomBg}.jpg`;
48 | }
49 |
50 | applyBackground(img) {
51 | let elm = document.querySelector('.app');
52 | elm.style.backgroundImage = `url(${img.src})`;
53 | }
54 |
55 | render() {
56 | return (
57 |
58 |
59 |
60 |
61 |
62 | )
63 | }
64 | }
65 |
66 | module.exports = App;
--------------------------------------------------------------------------------
/src/js/components/way-point.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import WayPointDetails from "./way-point-details";
3 |
4 | var EditableText = require('./core/editable-text');
5 | var Link = require('./core/link');
6 |
7 | class WayPoint extends Component {
8 |
9 | componentWillUnmount() {
10 | this.autoComplete = null;
11 | }
12 |
13 | cacheWayPointDomElm(elm) {
14 | let google = this.props.mapService;
15 | if (elm) {
16 | this.autoComplete = new google.maps.places.Autocomplete(elm);
17 | this.autoComplete.addListener('place_changed', this.autoCompletePlaceChange.bind(this));
18 | }
19 | }
20 |
21 | handleWayPointNameChange(newName) {
22 | this.props.onNameChange(this.props.wayPoint.id, newName);
23 | }
24 |
25 | autoCompletePlaceChange() {
26 | this.handleWayPointNameChange(this.autoComplete.getPlace().formatted_address);
27 | }
28 |
29 | renderWayPointName() {
30 | return (
31 |
32 |
38 |
39 | )
40 | }
41 |
42 | renderWayPointDetails() {
43 | return
44 | }
45 |
46 | render() {
47 | let {wayPoint} = this.props;
48 | return (
49 |
50 | {this.renderWayPointName()}
51 |
52 | details
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {wayPoint.detailsOpen? this.renderWayPointDetails() : null}
61 |
62 | )
63 | }
64 | }
65 |
66 | module.exports = WayPoint;
--------------------------------------------------------------------------------
/src/js/sagas/index.js:
--------------------------------------------------------------------------------
1 | import {takeEvery} from "redux-saga";
2 | import {select, put, call} from "redux-saga/effects";
3 | let Actions = require('../actions');
4 | let ActionTypes = require('../actions/types');
5 | let AppSelectors = require('../selectors/app');
6 | let APIService = require('../services/api');
7 |
8 | function* addRoute() {
9 | yield put(Actions.addRoute());
10 | let newRoute = yield select(AppSelectors.recentlyAddedRoute);
11 | yield put(Actions.changeActiveRoute(newRoute.id));
12 | }
13 |
14 | function* addWayPoint(action) {
15 | yield put(Actions.addWayPoint(action.id));
16 | let newWayPoint = yield select(AppSelectors.recentlyAddedWayPoint);
17 | yield put(Actions.addWayPointToRoute(action.id, newWayPoint.id));
18 | yield put(Actions.setEditingWayPoint(newWayPoint.id));
19 | }
20 |
21 | function* removeWayPoint(action) {
22 | yield put(Actions.removeWayPointFromRoute(action.id));
23 | yield put(Actions.removeWayPoint(action.id));
24 | yield* fetchDirections(action);
25 | }
26 |
27 | function* fetchDirections(action) {
28 | let activeRouteID = yield select(AppSelectors.activeRouteID);
29 | let activeWayPoints = yield select(AppSelectors.activeWayPoints);
30 | let data = yield call(APIService.fetchRoutes, activeWayPoints, action.mapService);
31 | if (data && data.status === 'OK') {
32 | yield put(Actions.API.fetchRoutesSucceeded(activeRouteID, data.response.routes));
33 | yield put(Actions.refreshMap(data.response));
34 | } else {
35 | console.log('fetch direction failed', data && data.status);
36 | }
37 | }
38 |
39 | function* changeWayPointName(action) {
40 | yield put(Actions.changeWayPointName(action.wayPointID, action.newName));
41 | yield put(Actions.unsetEditingWayPoint());
42 | if (action.newName.trim() === '') {
43 | yield* removeWayPoint(Actions.removeWayPointRequested(action.wayPointID));
44 | }
45 | yield* fetchDirections(action);
46 | }
47 |
48 | function* rootSaga() {
49 | yield* [
50 | takeEvery(ActionTypes.LOAD_MAP, fetchDirections),
51 | takeEvery(ActionTypes.ADD_ROUTE_REQUESTED, addRoute),
52 | takeEvery(ActionTypes.ADD_WAY_POINT_REQUESTED, addWayPoint),
53 | takeEvery(ActionTypes.REMOVE_WAY_POINT_REQUESTED, removeWayPoint),
54 | takeEvery(ActionTypes.CHANGE_WAY_POINT_NAME_REQUESTED, changeWayPointName)
55 | ];
56 |
57 | }
58 |
59 | module.exports = rootSaga;
60 |
61 |
--------------------------------------------------------------------------------
/src/scss/base.scss:
--------------------------------------------------------------------------------
1 | @import "reset";
2 | @import "icons";
3 | @import "variables";
4 | @import "text-input";
5 | @import "app";
6 | @import "app-bar";
7 | @import "routes";
8 | @import "route";
9 | @import "route-info";
10 | @import "waypoints";
11 | @import "way-point-info";
12 | @import "way-point-details";
13 |
14 | body {
15 | background-color: #ECF0F1;
16 | font: 1em "Open Sans", Sans-Serif;
17 | color: #333333;
18 | }
19 |
20 | .app {
21 |
22 | }
23 |
24 | .app-name {
25 | font-weight: 300;
26 | font-size: 1rem;
27 | text-align: center;
28 | text-transform: uppercase;
29 | color: #999999;
30 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHRpdGxlLz48cGF0aCBkPSJNMjU2IDBDMTY3LjYzNCAwIDk2IDcxLjYzNCA5NiAxNjBjMCAxNjAgMTYwIDM1MiAxNjAgMzUyczE2MC0xOTIgMTYwLTM1MkM0MTYgNzEuNjM0IDM0NC4zNjUgMCAyNTYgMHptMCAyNThjLTU0LjEyNCAwLTk4LTQzLjg3Ni05OC05OHM0My44NzYtOTggOTgtOTggOTggNDMuODc2IDk4IDk4LTQzLjg3NiA5OC05OCA5OHptLTYyLTk4YzAtMzQuMjQyIDI3Ljc1OC02MiA2Mi02MnM2MiAyNy43NTggNjIgNjJjMCAzNC4yNDItMjcuNzYgNjItNjIgNjJzLTYyLTI3Ljc1OC02Mi02MnoiLz48L3N2Zz4=');
31 | background-repeat: no-repeat;
32 | background-position: top left;
33 | background-size: 20px;
34 | padding-left: 25px;
35 | }
36 |
37 | .author {
38 | margin: $pad 0;
39 | font-size: .8rem;
40 | color: #aaaaaa;
41 | text-align: center;
42 |
43 | a {
44 | &:link, &:hover, &:visited, &:active {
45 | color: #aaaaaa;
46 | }
47 | }
48 | }
49 |
50 | .editable-text {
51 |
52 | .text-input input {
53 | width: 100%;
54 | border: none;
55 | font: 1rem "Open Sans", Sans-Serif;
56 |
57 | &:focus {
58 | outline: none;
59 | }
60 | }
61 |
62 | .text-label {
63 | text-overflow: ellipsis;
64 | white-space: nowrap;
65 | overflow: hidden;
66 | transition: margin-left .2s ease-in-out;
67 |
68 | &:hover {
69 | margin-left: 2 * $pad;
70 | }
71 | }
72 |
73 | }
74 |
75 | .routes .tabs {
76 | margin-bottom: 20px;
77 |
78 | li {
79 | display: inline-block;
80 | margin-right: $pad;
81 | }
82 | }
83 |
84 | .way-point {
85 | position: relative;
86 | padding-left: 30px;
87 |
88 | .way-point-name {
89 | padding-right: 50px;
90 | text-overflow: ellipsis;
91 | }
92 |
93 | .cta {
94 | position: absolute;
95 | top: 50%;
96 | right: 0%;
97 | transform: translateY(-50%);
98 |
99 | a {
100 | text-align: center;
101 | border-bottom: none;
102 | margin-left: 2 * $pad;
103 | display: inline-block;
104 | }
105 | }
106 | }
107 |
108 | @media screen and (min-width: 600px) {
109 | ul.way-points {
110 | max-height: 700px;
111 | overflow: auto;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/js/reducers/app.js:
--------------------------------------------------------------------------------
1 | import storageService from "../services/storage";
2 | import util from "../util";
3 |
4 | let ActionTypes = require('../actions/types');
5 | let wayPoints = require('./way-points');
6 | let wayPoint = require('./way-point');
7 | let routes = require('./routes');
8 | let route = require('./route');
9 |
10 | let savedState = util.transformSavedState(storageService.get('appState'));
11 |
12 | let defaultRoutes = routes(undefined, ActionTypes.ADD_ROUTE);
13 | let activeRouteID = defaultRoutes.first().id;
14 | let DEFAULTS = {
15 | activeRouteID: activeRouteID,
16 | routes: defaultRoutes
17 | };
18 |
19 | const app = (state = savedState || DEFAULTS, action) => {
20 |
21 | switch (action.type) {
22 | case ActionTypes.ADD_ROUTE:
23 | return Object.assign({}, state, {
24 | routes: routes(state.routes, action)
25 | });
26 | case ActionTypes.CHANGE_ACTIVE_ROUTE:
27 | return Object.assign({}, state, {
28 | activeRouteID: action.id
29 | });
30 | case ActionTypes.ADD_WAY_POINT_TO_ROUTE:
31 | case ActionTypes.REMOVE_WAY_POINT_FROM_ROUTE:
32 | return Object.assign({}, state, {
33 | routes: state.routes.map((r) => {
34 | if (r.id === state.activeRouteID) {
35 | return route(r, action);
36 | }
37 | return r;
38 | })
39 | });
40 | case ActionTypes.API_FETCH_ROUTES_SUCCEEDED:
41 | case ActionTypes.CHANGE_ROUTE_NAME:
42 | return Object.assign({}, state, {
43 | routes: state.routes.map((r) => {
44 | if (r.id === action.routeID) {
45 | return route(r, action);
46 | }
47 | return r;
48 | })
49 | });
50 |
51 | case ActionTypes.ADD_WAY_POINT:
52 | case ActionTypes.REMOVE_WAY_POINT:
53 | return Object.assign({}, state, {
54 | wayPoints: wayPoints(state.wayPoints, action)
55 | });
56 | case ActionTypes.OPEN_WAY_POINT_DETAILS:
57 | case ActionTypes.CLOSE_WAY_POINT_DETAILS:
58 | case ActionTypes.CHANGE_WAY_POINT_NAME:
59 | return Object.assign({}, state, {
60 | wayPoints: state.wayPoints.map((wp) => {
61 | if (wp.id === action.wayPointID) {
62 | return wayPoint(wp, action);
63 | }
64 | return wp;
65 | })
66 | });
67 | case ActionTypes.SET_EDITING_WAY_POINT:
68 | return Object.assign({}, state, {
69 | editingWayPoint: action.wayPointID
70 | });
71 | case ActionTypes.UNSET_EDITING_WAY_POINT:
72 | return Object.assign({}, state, {
73 | editingWayPoint: undefined
74 | });
75 | case ActionTypes.REFRESH_MAP:
76 | return Object.assign({}, state, {
77 | mapData: action.data
78 | });
79 | default:
80 | return state;
81 | }
82 | }
83 |
84 | module.exports = app;
--------------------------------------------------------------------------------
/src/js/components/way-points.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var WayPoint = require('./way-point');
3 | var WayPointInfo = require('./way-point-info');
4 | var Link = require('./core/link');
5 | var classnames = require('classnames');
6 | var MAXIMUM_WAY_POINTS = 10;
7 |
8 | class WayPoints extends React.Component {
9 |
10 | handleChangeWayPointName(wayPointID, newName) {
11 | this.props.onChangeWayPointName(wayPointID, newName);
12 | }
13 |
14 | renderWayPointInfo(wpIdx) {
15 | let directions = this.props.route.directions;
16 | if (directions) {
17 | let direction = directions[0];
18 | let legs = direction.legs;
19 | let leg = legs[wpIdx];
20 | if (leg) {
21 | return (
22 |
23 | );
24 | }
25 | }
26 | }
27 |
28 | renderWayPoints() {
29 | let cx = classnames({
30 | 'way-points': true,
31 | 'max-way-points': this.props.wayPoints.length >= MAXIMUM_WAY_POINTS
32 | });
33 | return (
34 |
35 | {
36 | this.props.wayPoints.map((wp, idx) => {
37 | let icon = (idx === 0 || idx === (this.props.wayPoints.length - 1)) ? 'icon-flag' : 'icon-disc';
38 | return (
39 | -
40 |
41 |
51 | {this.renderWayPointInfo(idx)}
52 |
53 | )
54 | })
55 | }
56 | - Add
57 | a place
58 |
59 | )
60 | }
61 |
62 | renderEmptyWayPoints() {
63 | return (
64 |
67 | )
68 | }
69 |
70 |
71 | render() {
72 | let elm;
73 | if (this.props.wayPoints.length > 0) {
74 | elm = this.renderWayPoints();
75 | } else {
76 | elm = this.renderEmptyWayPoints();
77 | }
78 | return (
79 |
80 | {elm}
81 |
82 | )
83 | }
84 | }
85 |
86 | module.exports = WayPoints;
--------------------------------------------------------------------------------
/src/js/actions/index.js:
--------------------------------------------------------------------------------
1 | var ActionTypes = require('./types');
2 |
3 | const saveState = true;
4 |
5 | var Actions = {
6 |
7 | addRoute: () => {
8 | return {
9 | type: ActionTypes.ADD_ROUTE,
10 | saveState
11 | }
12 | },
13 |
14 | addRouteRequested: () => {
15 | return {
16 | type: ActionTypes.ADD_ROUTE_REQUESTED,
17 | }
18 | },
19 |
20 | removeRoute: (id) => {
21 | return {
22 | type: ActionTypes.REMOVE_ROUTE,
23 | id,
24 | saveState
25 | }
26 | },
27 |
28 | removeRouteRequested: (id) => {
29 | return {
30 | type: ActionTypes.REMOVE_ROUTE_REQUESTED,
31 | id
32 | }
33 | },
34 |
35 | changeActiveRoute: (id) => {
36 | return {
37 | type: ActionTypes.CHANGE_ACTIVE_ROUTE,
38 | id
39 | }
40 | },
41 |
42 | changeRouteName: (routeID, newName) => {
43 | return {
44 | type: ActionTypes.CHANGE_ROUTE_NAME,
45 | routeID,
46 | newName
47 | }
48 | },
49 |
50 | addWayPointToRoute: (tgtWayPoint, newWayPoint) => {
51 | return {
52 | type: ActionTypes.ADD_WAY_POINT_TO_ROUTE,
53 | tgtWayPoint,
54 | newWayPoint
55 | }
56 | },
57 |
58 | removeWayPointFromRoute: (wayPointID) => {
59 | return {
60 | type: ActionTypes.REMOVE_WAY_POINT_FROM_ROUTE,
61 | wayPointID
62 | }
63 | },
64 |
65 | addWayPoint: (id) => {
66 | return {
67 | type: ActionTypes.ADD_WAY_POINT,
68 | id
69 | }
70 | },
71 |
72 | addWayPointRequested: (id) => {
73 | return {
74 | type: ActionTypes.ADD_WAY_POINT_REQUESTED,
75 | id
76 | }
77 | },
78 |
79 | removeWayPoint: (id) => {
80 | return {
81 | type: ActionTypes.REMOVE_WAY_POINT,
82 | id,
83 | saveState
84 | }
85 | },
86 |
87 | removeWayPointRequested: (id) => {
88 | return {
89 | type: ActionTypes.REMOVE_WAY_POINT_REQUESTED,
90 | id
91 | }
92 | },
93 |
94 | changeWayPointName: (wayPointID, newName) => {
95 | return {
96 | type: ActionTypes.CHANGE_WAY_POINT_NAME,
97 | wayPointID,
98 | newName,
99 | saveState
100 | }
101 | },
102 |
103 | changeWayPointNameRequested: (wayPointID, newName, mapService) => {
104 | return {
105 | type: ActionTypes.CHANGE_WAY_POINT_NAME_REQUESTED,
106 | wayPointID,
107 | newName,
108 | mapService
109 | }
110 | },
111 |
112 | setEditingWayPoint: (wayPointID) => {
113 | return {
114 | type: ActionTypes.SET_EDITING_WAY_POINT,
115 | wayPointID
116 | }
117 | },
118 |
119 | unsetEditingWayPoint: () => {
120 | return {
121 | type: ActionTypes.UNSET_EDITING_WAY_POINT
122 | }
123 | },
124 |
125 | openWayPointDetails: (wayPointID) => {
126 | return {
127 | type: ActionTypes.OPEN_WAY_POINT_DETAILS,
128 | wayPointID
129 | }
130 | },
131 |
132 | closeWayPointDetails: (wayPointID) => {
133 | return {
134 | type: ActionTypes.CLOSE_WAY_POINT_DETAILS,
135 | wayPointID
136 | }
137 | },
138 |
139 | refreshMap: (data) => {
140 | return {
141 | type: ActionTypes.REFRESH_MAP,
142 | data
143 | }
144 | },
145 |
146 | loadMap: () => {
147 | return {
148 | type: ActionTypes.LOAD_MAP
149 | }
150 | },
151 |
152 | API: {
153 | fetchRoutes: () => {
154 | return {
155 | type: ActionTypes.API_FETCH_ROUTES
156 | }
157 | },
158 |
159 | fetchRoutesSucceeded: (routeID, routes) => {
160 | return {
161 | type: ActionTypes.API_FETCH_ROUTES_SUCCEEDED,
162 | routeID,
163 | routes,
164 | saveState
165 | }
166 | }
167 |
168 | }
169 |
170 | };
171 |
172 | module.exports = Actions;
--------------------------------------------------------------------------------
/src/scss/fonts/icomoon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/npm-shrinkwrap.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "routeplanner",
3 | "version": "2.0.0",
4 | "dependencies": {
5 | "asap": {
6 | "version": "2.0.5",
7 | "from": "asap@>=2.0.3 <2.1.0",
8 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz"
9 | },
10 | "classnames": {
11 | "version": "2.2.5",
12 | "from": "classnames@latest",
13 | "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz"
14 | },
15 | "domready": {
16 | "version": "1.0.8",
17 | "from": "domready@latest",
18 | "resolved": "https://registry.npmjs.org/domready/-/domready-1.0.8.tgz"
19 | },
20 | "encoding": {
21 | "version": "0.1.12",
22 | "from": "encoding@>=0.1.11 <0.2.0",
23 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz"
24 | },
25 | "fbjs": {
26 | "version": "0.8.5",
27 | "from": "fbjs@>=0.8.4 <0.9.0",
28 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.5.tgz",
29 | "dependencies": {
30 | "core-js": {
31 | "version": "1.2.7",
32 | "from": "core-js@>=1.0.0 <2.0.0",
33 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz"
34 | }
35 | }
36 | },
37 | "google-maps": {
38 | "version": "3.2.1",
39 | "from": "google-maps@latest",
40 | "resolved": "https://registry.npmjs.org/google-maps/-/google-maps-3.2.1.tgz"
41 | },
42 | "hoist-non-react-statics": {
43 | "version": "1.2.0",
44 | "from": "hoist-non-react-statics@>=1.0.3 <2.0.0",
45 | "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz"
46 | },
47 | "iconv-lite": {
48 | "version": "0.4.13",
49 | "from": "iconv-lite@>=0.4.13 <0.5.0",
50 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
51 | },
52 | "immutable": {
53 | "version": "3.8.1",
54 | "from": "https://registry.npmjs.org/immutable/-/immutable-3.8.1.tgz",
55 | "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.1.tgz"
56 | },
57 | "invariant": {
58 | "version": "2.2.1",
59 | "from": "invariant@>=2.2.0 <3.0.0",
60 | "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.1.tgz"
61 | },
62 | "is-stream": {
63 | "version": "1.1.0",
64 | "from": "is-stream@>=1.0.1 <2.0.0",
65 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz"
66 | },
67 | "isomorphic-fetch": {
68 | "version": "2.2.1",
69 | "from": "isomorphic-fetch@>=2.1.1 <3.0.0",
70 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz"
71 | },
72 | "lodash": {
73 | "version": "4.16.2",
74 | "from": "https://registry.npmjs.org/lodash/-/lodash-4.16.2.tgz",
75 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.2.tgz"
76 | },
77 | "lodash-es": {
78 | "version": "4.16.2",
79 | "from": "lodash-es@>=4.2.1 <5.0.0",
80 | "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.16.2.tgz"
81 | },
82 | "loose-envify": {
83 | "version": "1.2.0",
84 | "from": "loose-envify@>=1.0.0 <2.0.0",
85 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.2.0.tgz",
86 | "dependencies": {
87 | "js-tokens": {
88 | "version": "1.0.3",
89 | "from": "js-tokens@>=1.0.1 <2.0.0",
90 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.3.tgz"
91 | }
92 | }
93 | },
94 | "node-fetch": {
95 | "version": "1.6.3",
96 | "from": "node-fetch@>=1.0.1 <2.0.0",
97 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz"
98 | },
99 | "object-assign": {
100 | "version": "4.1.0",
101 | "from": "object-assign@>=4.0.1 <5.0.0",
102 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz"
103 | },
104 | "promise": {
105 | "version": "7.1.1",
106 | "from": "promise@>=7.1.1 <8.0.0",
107 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz"
108 | },
109 | "react": {
110 | "version": "15.3.2",
111 | "from": "https://registry.npmjs.org/react/-/react-15.3.2.tgz",
112 | "resolved": "https://registry.npmjs.org/react/-/react-15.3.2.tgz"
113 | },
114 | "react-dom": {
115 | "version": "15.3.2",
116 | "from": "react-dom@latest",
117 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.3.2.tgz"
118 | },
119 | "react-redux": {
120 | "version": "4.4.5",
121 | "from": "react-redux@latest",
122 | "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-4.4.5.tgz"
123 | },
124 | "redux": {
125 | "version": "3.6.0",
126 | "from": "redux@latest",
127 | "resolved": "https://registry.npmjs.org/redux/-/redux-3.6.0.tgz"
128 | },
129 | "redux-saga": {
130 | "version": "0.12.0",
131 | "from": "redux-saga@latest",
132 | "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-0.12.0.tgz"
133 | },
134 | "symbol-observable": {
135 | "version": "1.0.2",
136 | "from": "symbol-observable@>=1.0.2 <2.0.0",
137 | "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.2.tgz"
138 | },
139 | "ua-parser-js": {
140 | "version": "0.7.10",
141 | "from": "ua-parser-js@>=0.7.9 <0.8.0",
142 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.10.tgz"
143 | },
144 | "whatwg-fetch": {
145 | "version": "1.0.0",
146 | "from": "whatwg-fetch@>=0.10.0",
147 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-1.0.0.tgz"
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/js/components/route-info.js:
--------------------------------------------------------------------------------
1 | let React = require('react');
2 | import RouteVisual from './route-visual';
3 |
4 | let MINUTE = 60;
5 | let HOUR = 3600;
6 | let DAY = 86400;
7 |
8 | let toMiles = (meters) => meters * 0.000621371;
9 | let ALT_COLORS = ['#E01931', '#FEC606'];
10 |
11 | class RouteInfo extends React.Component {
12 |
13 | constructor(props) {
14 | super(props);
15 | }
16 |
17 | getCanvas() {
18 |
19 | let canvasElm = this.canvasElm;
20 | let ctx = this.ctx;
21 |
22 | let cW = canvasElm.width;
23 | let cH = canvasElm.height;
24 | let midW = cW / 2;
25 | let midH = cH / 2;
26 | let pad = 10;
27 | let env = {
28 | cW,
29 | cH,
30 | midW,
31 | midH,
32 | pad
33 | };
34 |
35 | return {
36 | ctx,
37 | env
38 | };
39 | }
40 |
41 | drawDistanceSegments(ctx, env, totalDistance = 0, segments = []) {
42 |
43 | let startX = 0;
44 | let barH = 10;
45 | let startY = env.cH - barH;
46 | let barW = env.cW;
47 |
48 | ctx.save();
49 | if (segments.length <= 1) {
50 | ctx.fillStyle = ALT_COLORS[0];
51 | ctx.fillRect(startX, startY, barW, barH);
52 | } else {
53 | let i;
54 | ctx.clearRect(0, startY, barW, barH);
55 |
56 | for (i = 0; i < segments.length - 1; i++) {
57 | let segX = startX;
58 | let segY = startY;
59 | let segW = Math.floor((barW * segments[i]) / totalDistance);
60 | ctx.fillStyle = ALT_COLORS[i % 2 === 0 ? 0 : 1];
61 | ctx.fillRect(segX, segY, segW, barH);
62 | startX += segW;
63 | }
64 | ctx.fillStyle = ALT_COLORS[i % 2 === 0 ? 0 : 1];
65 | ctx.fillRect(startX, startY, (barW - startX), barH);
66 |
67 | }
68 | }
69 |
70 | renderDistance(distance = 0) {
71 | return (
72 |
73 |
Distance
74 |
{distance > 0 ? (toMiles(distance).toFixed(2)) : 0}
75 |
{distance <= 1 ? 'mile' : 'miles'}
76 |
77 | )
78 | }
79 |
80 | renderDuration(duration = 0) {
81 | let time = {
82 | days: 0,
83 | hours: 0,
84 | minutes: 0
85 | };
86 | let elm;
87 | let dayElm;
88 | let hourElm;
89 | let minuteElm;
90 |
91 | if (duration === 0) {
92 | elm = (
93 | {0}
94 | hour
95 | );
96 | } else {
97 | // map duration to days / hours / minutes
98 | if (duration >= DAY) {
99 | time.days = Math.floor(duration / DAY);
100 | duration = duration % DAY;
101 | dayElm = (
102 | {time.days}
103 | {time.days <= 1 ? 'day' : 'days'}
104 | );
105 | }
106 |
107 | if (duration >= HOUR) {
108 | time.hours = Math.floor(duration / HOUR);
109 | duration = duration % HOUR;
110 | hourElm = (
111 | {time.hours}
112 | {time.hours <= 1 ? 'hour' : 'hours'}
113 | );
114 | }
115 |
116 | if (duration >= MINUTE) {
117 | time.minutes = Math.floor(duration / MINUTE);
118 | duration = duration % MINUTE;
119 | minuteElm = (
120 | {time.minutes}
121 | {time.minutes <= 0 ? 'minute' : 'minutes'}
122 | );
123 | }
124 |
125 | }
126 |
127 |
128 | return (
129 |
130 |
Duration
131 |
132 | {elm}
133 | {dayElm}
134 | {hourElm}
135 | {minuteElm}
136 |
137 |
138 | )
139 | }
140 |
141 | extractDisplayData(props) {
142 | let source = props || this.props;
143 | if (source.route) {
144 | let directions = source.route.directions;
145 | let distances = [];
146 | let totalDistance = 0, totalDuration = 0, direction, legs, leg;
147 | if (directions) {
148 | direction = directions[0];
149 | legs = direction.legs;
150 | for (let l = 0; l < legs.length; l++) {
151 | let leg = legs[l];
152 | distances.push(leg.distance.value);
153 | totalDistance += leg.distance.value;
154 | totalDuration += leg.duration.value;
155 | }
156 | }
157 | return {
158 | distances,
159 | totalDistance,
160 | totalDuration
161 | };
162 | }
163 | return {};
164 | }
165 |
166 | render() {
167 | let data = this.extractDisplayData();
168 | return (
169 |
170 |
171 | {this.renderDistance(data.totalDistance)}
172 | {this.renderDuration(data.totalDuration)}
173 |
174 |
175 |
176 | )
177 | }
178 | }
179 |
180 | module.exports = RouteInfo;
--------------------------------------------------------------------------------
/src/js/components/route-visual.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | const COLORS = [
4 | '#4A148C',
5 | '#F57F17',
6 | '#880E4F',
7 | '#827717',
8 | '#B71C1C',
9 | '#33691E',
10 | '#1A237E',
11 | '#E65100',
12 | '#3E2723',
13 | '#01579B',
14 | '#212121',
15 | '#006064'
16 | ];
17 |
18 | class RouteVisual extends Component {
19 |
20 | renderRoute(total, segments) {
21 | let width = 350,
22 | height = 40;
23 | let mappedSegments = segments.map((seg) => (width * seg) / total);
24 | let legX = 0;
25 | let markX = ( (width / 2) - width);
26 | return (
27 |
99 | )
100 | }
101 |
102 | render() {
103 | let {data} = this.props;
104 | let {totalDistance, distances} = data;
105 |
106 | // totalDistance = 5000;
107 | // distances = [2500, 2500];
108 |
109 | return !totalDistance ? null : (
110 |
111 | {this.renderRoute(totalDistance, distances)}
112 |
113 | )
114 | }
115 | }
116 |
117 | export default RouteVisual;
--------------------------------------------------------------------------------
/src/js/components/map.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var ReactDOM = require('react-dom');
3 |
4 | class Map extends React.Component {
5 |
6 | componentDidMount() {
7 | this.initMap(ReactDOM.findDOMNode(this));
8 | }
9 |
10 | componentWillReceiveProps(nextProps) {
11 | if (this.directionsDisplay !== undefined && nextProps.mapData !== undefined) {
12 | this.directionsDisplay.setDirections(nextProps.mapData);
13 | }
14 | }
15 |
16 | initMap(elm) {
17 | if (!!elm && !this.map) {
18 | let google = this.props.mapService;
19 | this.directionsDisplay = new google.maps.DirectionsRenderer;
20 | this.map = new google.maps.Map(elm, {
21 | center: {lat: -34.397, lng: 150.644},
22 | zoom: 8,
23 | styles: [
24 | {
25 | "elementType": "geometry",
26 | "stylers": [
27 | {
28 | "color": "#ebe3cd"
29 | }
30 | ]
31 | },
32 | {
33 | "elementType": "labels.text.fill",
34 | "stylers": [
35 | {
36 | "color": "#523735"
37 | }
38 | ]
39 | },
40 | {
41 | "elementType": "labels.text.stroke",
42 | "stylers": [
43 | {
44 | "color": "#f5f1e6"
45 | }
46 | ]
47 | },
48 | {
49 | "featureType": "administrative",
50 | "elementType": "geometry.stroke",
51 | "stylers": [
52 | {
53 | "color": "#c9b2a6"
54 | }
55 | ]
56 | },
57 | {
58 | "featureType": "administrative.land_parcel",
59 | "elementType": "geometry.stroke",
60 | "stylers": [
61 | {
62 | "color": "#dcd2be"
63 | }
64 | ]
65 | },
66 | {
67 | "featureType": "administrative.land_parcel",
68 | "elementType": "labels.text.fill",
69 | "stylers": [
70 | {
71 | "color": "#ae9e90"
72 | }
73 | ]
74 | },
75 | {
76 | "featureType": "landscape.natural",
77 | "elementType": "geometry",
78 | "stylers": [
79 | {
80 | "color": "#dfd2ae"
81 | }
82 | ]
83 | },
84 | {
85 | "featureType": "poi",
86 | "elementType": "geometry",
87 | "stylers": [
88 | {
89 | "color": "#dfd2ae"
90 | }
91 | ]
92 | },
93 | {
94 | "featureType": "poi",
95 | "elementType": "labels.text.fill",
96 | "stylers": [
97 | {
98 | "color": "#93817c"
99 | }
100 | ]
101 | },
102 | {
103 | "featureType": "poi.park",
104 | "elementType": "geometry.fill",
105 | "stylers": [
106 | {
107 | "color": "#a5b076"
108 | }
109 | ]
110 | },
111 | {
112 | "featureType": "poi.park",
113 | "elementType": "labels.text.fill",
114 | "stylers": [
115 | {
116 | "color": "#447530"
117 | }
118 | ]
119 | },
120 | {
121 | "featureType": "road",
122 | "elementType": "geometry",
123 | "stylers": [
124 | {
125 | "color": "#f5f1e6"
126 | }
127 | ]
128 | },
129 | {
130 | "featureType": "road.arterial",
131 | "elementType": "geometry",
132 | "stylers": [
133 | {
134 | "color": "#fdfcf8"
135 | }
136 | ]
137 | },
138 | {
139 | "featureType": "road.highway",
140 | "elementType": "geometry",
141 | "stylers": [
142 | {
143 | "color": "#f8c967"
144 | }
145 | ]
146 | },
147 | {
148 | "featureType": "road.highway",
149 | "elementType": "geometry.stroke",
150 | "stylers": [
151 | {
152 | "color": "#e9bc62"
153 | }
154 | ]
155 | },
156 | {
157 | "featureType": "road.highway.controlled_access",
158 | "elementType": "geometry",
159 | "stylers": [
160 | {
161 | "color": "#e98d58"
162 | }
163 | ]
164 | },
165 | {
166 | "featureType": "road.highway.controlled_access",
167 | "elementType": "geometry.stroke",
168 | "stylers": [
169 | {
170 | "color": "#db8555"
171 | }
172 | ]
173 | },
174 | {
175 | "featureType": "road.local",
176 | "elementType": "labels.text.fill",
177 | "stylers": [
178 | {
179 | "color": "#806b63"
180 | }
181 | ]
182 | },
183 | {
184 | "featureType": "transit.line",
185 | "elementType": "geometry",
186 | "stylers": [
187 | {
188 | "color": "#dfd2ae"
189 | }
190 | ]
191 | },
192 | {
193 | "featureType": "transit.line",
194 | "elementType": "labels.text.fill",
195 | "stylers": [
196 | {
197 | "color": "#8f7d77"
198 | }
199 | ]
200 | },
201 | {
202 | "featureType": "transit.line",
203 | "elementType": "labels.text.stroke",
204 | "stylers": [
205 | {
206 | "color": "#ebe3cd"
207 | }
208 | ]
209 | },
210 | {
211 | "featureType": "transit.station",
212 | "elementType": "geometry",
213 | "stylers": [
214 | {
215 | "color": "#dfd2ae"
216 | }
217 | ]
218 | },
219 | {
220 | "featureType": "water",
221 | "elementType": "geometry.fill",
222 | "stylers": [
223 | {
224 | "color": "#b9d3c2"
225 | }
226 | ]
227 | },
228 | {
229 | "featureType": "water",
230 | "elementType": "labels.text.fill",
231 | "stylers": [
232 | {
233 | "color": "#92998d"
234 | }
235 | ]
236 | }
237 | ]
238 | });
239 | this.directionsDisplay.setMap(this.map);
240 | }
241 | }
242 |
243 | render() {
244 | return (
245 |
246 |
247 | )
248 | }
249 | }
250 |
251 | module.exports = Map;
--------------------------------------------------------------------------------