├── 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 |
8 |

Route Planner

9 |
10 |

Built by Veera | Source

12 |
13 |
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 | [![Route Planner](http://veerasundar.com/route-planner/route-planner-screenshot-new.PNG)](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 |
33 | 34 | 35 | 36 |
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 | 55 |
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 |
    65 |
  • Add a place
  • 66 |
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 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | 32 | 33 | 34 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | { 59 | mappedSegments.map((leg, idx) => { 60 | let markerElm = idx === 0 ? null : ; 68 | 69 | markX = markX + leg; 70 | 71 | return markerElm; 72 | }) 73 | } 74 | 75 | 82 | 83 | 84 | { 85 | mappedSegments.map((leg, idx) => { 86 | let legElm = ; 94 | legX = legX + leg; 95 | return legElm; 96 | }) 97 | } 98 | 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; --------------------------------------------------------------------------------