HTML element and optionally an object literal with Map options [*(more)*](http://leafletjs.com/reference-1.3.0.html)|
45 |
46 |
47 | ### Resources
48 | - [Leaflet quick start](http://leafletjs.com/examples/quick-start/)
49 | - [Leaflet on Mobile](http://leafletjs.com/examples/mobile/)
50 | - [Markers with custom icons](http://leafletjs.com/examples/custom-icons/)
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # PROJECT SPECIFIC
2 |
3 | # Distribution / Build
4 | build/
5 |
6 | # GENERAL IGNORES
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories / package management
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Typescript v1 declaration files
46 | typings/
47 |
48 | # Optional npm cache directory
49 | .npm
50 |
51 | # Optional eslint cache
52 | .eslintcache
53 |
54 | # Optional REPL history
55 | .node_repl_history
56 |
57 | # Output of 'npm pack'
58 | *.tgz
59 |
60 | # Yarn Integrity file
61 | .yarn-integrity
62 |
63 | # dotenv environment variables file
64 | .env
65 |
66 | # MAC OS X SPECIFIC IGNORES
67 | # General
68 | .DS_Store
69 | .AppleDouble
70 | .LSOverride
71 |
72 | # Icon must end with two \r
73 | Icon
74 |
75 | # Thumbnails
76 | ._*
77 |
78 | # Files that might appear in the root of a volume
79 | .DocumentRevisions-V100
80 | .fseventsd
81 | .Spotlight-V100
82 | .TemporaryItems
83 | .Trashes
84 | .VolumeIcon.icns
85 | .com.apple.timemachine.donotpresent
86 |
87 | # Directories potentially created on remote AFP share
88 | .AppleDB
89 | .AppleDesktop
90 | Network Trash Folder
91 | Temporary Items
92 | .apdisk
93 |
94 | # VSCODE IGNORES
95 | .vscode/*
96 | !.vscode/settings.json
97 | !.vscode/tasks.json
98 | !.vscode/launch.json
99 | !.vscode/extensions.json
100 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "navi-client",
4 | "version": "0.0.0",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "if-env NODE_ENV=production && npm run -s serve || npm run -s dev",
8 | "build": "preact build --no-prerender",
9 | "serve": "preact build --no-prerender && preact serve",
10 | "dev": "preact watch",
11 | "lint": "eslint src",
12 | "test": "jest"
13 | },
14 | "eslintConfig": {
15 | "extends": "eslint-config-airbnb"
16 | },
17 | "eslintIgnore": [
18 | "build/*"
19 | ],
20 | "babel": {
21 | "presets": [
22 | "env"
23 | ],
24 | "plugins": [
25 | [
26 | "transform-react-jsx",
27 | {
28 | "pragma": "h"
29 | }
30 | ],
31 | "transform-decorators-legacy"
32 | ]
33 | },
34 | "devDependencies": {
35 | "eslint": "^4.9.0",
36 | "eslint-config-airbnb": "^16.1.0",
37 | "eslint-plugin-import": "^2.7.0",
38 | "eslint-plugin-jsx-a11y": "^6.0.2",
39 | "eslint-plugin-react": "^7.4.0",
40 | "eslint-plugin-react-compat": "0.0.3",
41 | "if-env": "^1.0.0",
42 | "ignore-styles": "^5.0.1",
43 | "jest": "^22.4.2",
44 | "jest-css-modules": "^1.1.0",
45 | "preact-cli": "^2.0.1",
46 | "preact-compat-enzyme": "^0.2.5",
47 | "preact-render-spy": "^1.2.2",
48 | "preact-test-utils": "^0.1.3",
49 | "regenerator-runtime": "^0.11.0"
50 | },
51 | "dependencies": {
52 | "axios": "^0.18.0",
53 | "leaflet": "^1.3.1",
54 | "leaflet-routing-machine": "^3.2.8",
55 | "linkstate": "^1.1.1",
56 | "pouchdb-browser": "^6.4.3",
57 | "preact": "^8.2.6",
58 | "preact-compat": "^3.17.0",
59 | "preact-render-to-string": "^3.7.0",
60 | "preact-router": "^2.5.7",
61 | "prop-types": "^15.6.0",
62 | "universal-cookie": "^2.1.2"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # GENERAL IGNORES
2 | # Logs
3 | logs
4 | *.log
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 | *.pid.lock
14 |
15 | # Directory for instrumented libs generated by jscoverage/JSCover
16 | lib-cov
17 |
18 | # Coverage directory used by tools like istanbul
19 | coverage
20 |
21 | # nyc test coverage
22 | .nyc_output
23 |
24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
25 | .grunt
26 |
27 | # Bower dependency directory (https://bower.io/)
28 | bower_components
29 |
30 | # node-waf configuration
31 | .lock-wscript
32 |
33 | # Compiled binary addons (https://nodejs.org/api/addons.html)
34 | build/Release
35 |
36 | # Dependency directories / package management
37 | node_modules/
38 | jspm_packages/
39 |
40 | # Typescript v1 declaration files
41 | typings/
42 |
43 | # Optional npm cache directory
44 | .npm
45 |
46 | # Optional eslint cache
47 | .eslintcache
48 |
49 | # Optional REPL history
50 | .node_repl_history
51 |
52 | # Output of 'npm pack'
53 | *.tgz
54 |
55 | # Yarn Integrity file
56 | .yarn-integrity
57 |
58 | # dotenv environment variables file
59 | .env
60 |
61 | # MAC OS X SPECIFIC IGNORES
62 | # General
63 | .DS_Store
64 | .AppleDouble
65 | .LSOverride
66 |
67 | # Icon must end with two \r
68 | Icon
69 |
70 | # Thumbnails
71 | ._*
72 |
73 | # Files that might appear in the root of a volume
74 | .DocumentRevisions-V100
75 | .fseventsd
76 | .Spotlight-V100
77 | .TemporaryItems
78 | .Trashes
79 | .VolumeIcon.icns
80 | .com.apple.timemachine.donotpresent
81 |
82 | # Directories potentially created on remote AFP share
83 | .AppleDB
84 | .AppleDesktop
85 | Network Trash Folder
86 | Temporary Items
87 | .apdisk
88 |
89 | # VSCODE IGNORES
90 | .vscode/
91 | .vscode/*
92 | !.vscode/settings.json
93 | !.vscode/tasks.json
94 | !.vscode/launch.json
95 | !.vscode/extensions.json
96 |
97 | # MongoDB DB path
98 | data/*
99 |
100 | # secrets.json ***SHOULD NEVER BE PUSHED INTO GITHUB***
101 | secrets.json
102 |
--------------------------------------------------------------------------------
/client/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "navi",
3 | "name": "Navi - a navigator app for the Udacity Grow with Google project",
4 | "start_url": "/?utm_source=homescreen",
5 | "display": "standalone",
6 | "orientation": "portrait",
7 | "background_color": "#2196F3",
8 | "theme_color": "#2196F3",
9 | "permissions": ["geolocation"],
10 | "icons": [{
11 | "src": "client/src/assets/icons/android-chrome-192x192.png",
12 | "sizes": "128x128",
13 | "type": "image/png"
14 | }, {
15 | "src": "client/src/assets/icons/android-chrome-512x512.png",
16 | "sizes": "144x144",
17 | "type": "image/png"
18 | }, {
19 | "src": "client/src/assets/icons/apple-touch-icon.png",
20 | "sizes": "152x152",
21 | "type": "image/png"
22 | }, {
23 | "src": "client/src/assets/icons/favicon-16x16.png",
24 | "sizes": "128X128",
25 | "type": "image/png"
26 | }, {
27 | "src": "client/src/assets/icons/favicon-16x16.png",
28 | "sizes": "144X144",
29 | "type": "image/png"
30 | }, {
31 | "src": "client/src/assets/icons/favicon-16x16.png",
32 | "sizes": "16X16",
33 | "type": "image/png"
34 | }, {
35 | "src": "client/src/assets/icons/favicon-32x32.png",
36 | "sizes": "256x256",
37 | "type": "image/png"
38 | },{
39 | "src": "client/src/assets/icons/mstile-150x150.png",
40 | "type": "image/png",
41 | "sizes": "32X32"
42 | },
43 | {
44 | "src": "client/src/assets/icons/favicon-16x16.png",
45 | "sizes": "384X384",
46 | "type": "image/png"
47 | },
48 | {
49 | "src": "client/src/assets/icons/fmstile-150x150.png",
50 | "sizes": "48X48",
51 | "type": "image/png"
52 | }, {
53 | "src": "client/src/assets/icons/favicon-150x150.png",
54 | "sizes": "512X512",
55 | "type": "image/png"
56 | }, {
57 | "src": "client/src/assets/icons/favicon-150x150.png",
58 | "sizes": "96X96",
59 | "type": "image/png"
60 | }, {
61 | "src": "client/src/assets/icons/mstile-150x150.png",
62 | "sizes": "150X150",
63 | "type": "image/png"
64 | }
65 | ]
66 | }
67 |
--------------------------------------------------------------------------------
/tests/seed/seed.js:
--------------------------------------------------------------------------------
1 | const { ObjectID } = require('mongodb');
2 | const jwt = require('jsonwebtoken');
3 | const { JWT_KEY } = require('../../config');
4 | const server = require('../../server');
5 |
6 | const User = require('../../models/users');
7 | const SavedPins = require('../../models/saved-pins');
8 | const SearchHistory = require('../../models/search-history');
9 |
10 | const userOneId = new ObjectID();
11 | const userTwoId = new ObjectID();
12 | const users = [{
13 | _id: userOneId,
14 | name: 'mallek',
15 | email: 'mallek@example.com',
16 | password: 'userOnePass1!',
17 | tokens: [{
18 | access: 'auth',
19 | token: jwt.sign({ id: userOneId }, JWT_KEY).toString(),
20 | }],
21 | }, {
22 | _id: userTwoId,
23 | name: 'user',
24 | email: 'user@example.com',
25 | password: 'userTwoPass2!',
26 | }, {
27 | // /This user is for creating new test users only
28 | name: 'Taco Test',
29 | email: 'test@testing.com',
30 | password: 'passcode!1',
31 | },
32 | ];
33 |
34 | const pins = [{
35 | _id: new ObjectID(),
36 | lat: 1,
37 | lng: 2,
38 | place_id: 'testing pin1',
39 | user: userOneId,
40 | }, {
41 | _id: new ObjectID(),
42 | lat: 20,
43 | lng: 30,
44 | place_id: 'testing again',
45 | user: userTwoId,
46 | }];
47 |
48 | const populatePins = (done) => {
49 | SavedPins.remove({}).then(() => SavedPins.insertMany(pins)).then(() => done());
50 | };
51 |
52 | const populateUsers = function (done) {
53 | User.remove({}).then(() => {
54 | const userOne = new User(users[0]).save();
55 | const userTwo = new User(users[1]).save();
56 |
57 | return Promise.all([userOne, userTwo]);
58 | }).then(() => done());
59 | };
60 |
61 | const deleteTestUser = function (done) {
62 | User.remove({
63 | email: 'test@testing.com',
64 | }).then(() => done());
65 | };
66 |
67 | const stopServer = (done) => {
68 | server.close();
69 | done();
70 | };
71 |
72 | const userObjectWithToken = user => {
73 | const _id = new ObjectID();
74 | const tokens = [{
75 | access: 'auth',
76 | token: jwt.sign({ id: _id }, JWT_KEY).toString(),
77 | }];
78 | return Object.assign(user, { tokens: tokens, _id: _id });
79 | };
80 |
81 | module.exports = {
82 | pins,
83 | populatePins,
84 | users,
85 | populateUsers,
86 | deleteTestUser,
87 | stopServer,
88 | userObjectWithToken
89 | };
90 |
--------------------------------------------------------------------------------
/routes/search.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 |
3 | const router = express.Router();
4 |
5 | // require controller modules
6 | const savedDirectionsController = require('../controllers/saved-directions-controller');
7 | const savedPinsController = require('../controllers/saved-pins-controller');
8 | const searchHistoryController = require('../controllers/search-history-controller');
9 | const { verifyToken } = require('../controllers/utils-controller');
10 | const { autocomplete, placeDetails, textSearch } = require('../controllers/google-api-controller');
11 |
12 | /**
13 | * @description Handle requests to search main end point
14 | *
15 | * @api {GET} /search
16 | * @return {success: false, error: err} Not a valid end point
17 | */
18 | router.get('/', (req, res) => {
19 | res.status(404).send({
20 | success: false,
21 | error: 'Not a valid end point!',
22 | });
23 | });
24 |
25 | /**
26 | * Saved pins end points
27 | */
28 | router.get('/savedpins', verifyToken, savedPinsController.getSavedPins);
29 | router.get('/savedpins/:id', verifyToken, savedPinsController.getSavedPinsById);
30 | router.post('/savedpins', verifyToken, savedPinsController.postSavedPins);
31 | router.delete('/savedpins', verifyToken, savedPinsController.deleteSavedPins);
32 | router.delete('/savedpins/:id', verifyToken, savedPinsController.deleteSavedPinsById);
33 |
34 | /**
35 | * Search and Places query endpoints
36 | */
37 | router.get('/places/:id', placeDetails);
38 | router.post('/autocomplete', autocomplete);
39 | router.get('/textsearch', textSearch);
40 | router.post('/textsearch', textSearch);
41 |
42 | /**
43 | * Saved search history end points
44 | */
45 | router.get('/history', verifyToken, searchHistoryController.getSearchHistory);
46 | router.get('/history/recent/:num', verifyToken, searchHistoryController.getRecent);
47 | router.post('/history/:query', verifyToken, searchHistoryController.saveQuery);
48 | router.delete('/history', verifyToken, searchHistoryController.deleteSearchHistory);
49 |
50 | /**
51 | * Saved directions end points
52 | */
53 | router.get('/directions', verifyToken, savedDirectionsController.getSavedDirections);
54 | router.get('/directions/recent/:num', verifyToken, savedDirectionsController.getRecentDirections);
55 | router.post('/directions', verifyToken, savedDirectionsController.saveDirections);
56 | router.delete('/directions', verifyToken, savedDirectionsController.deleteSavedDirections);
57 |
58 | module.exports = router;
59 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | // Package Dependencies
2 | const bodyParser = require('body-parser');
3 | const express = require('express');
4 | const helmet = require('helmet');
5 | const mongoose = require('mongoose');
6 | const morgan = require('morgan');
7 |
8 | // Local Dependencies
9 | const { DB_URL, NODE_ENV: ENV } = require('./config');
10 |
11 | // Instantiate Express Server
12 | const app = express();
13 |
14 | /**
15 | * MONGODB CONNECTION
16 | */
17 |
18 | // Setup MongoDB connection using the global promise library and then get connection
19 | mongoose.connect(DB_URL, { promiseLibrary: global.Promise }, (error) => {
20 | if (error) {
21 | console.log(`MongoDB connection error: ${error}`);
22 |
23 | // should consider alternative to exiting the app due to db conn issue
24 | process.exit(1);
25 | }
26 | });
27 |
28 | const db = mongoose.connection;
29 |
30 | /**
31 | * MIDDLEWARE
32 | */
33 |
34 | // Set security-related HTTP headers (https://expressjs.com/en/advanced/best-practice-security.html#use-helmet)
35 | app.use(helmet());
36 |
37 | // Allow access on headers and avoid CORS issues
38 | app.use((req, res, next) => {
39 | res.header('Access-Control-Allow-Origin', '*');
40 | res.header(
41 | 'Access-Control-Allow-Headers',
42 | 'Origin, X-Requested-With, Content-Type, Access, Authorization, x-access-token',
43 | );
44 |
45 | if (req.method === 'OPTIONS') {
46 | res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, PATCH, DELETE');
47 |
48 | res.header(
49 | 'Access-Control-Allow-Headers',
50 | 'Origin, X-Requested-With, Content-Type, Access, Authorization, x-access-token',
51 | );
52 |
53 | return res.status(200).json({});
54 | }
55 |
56 | next();
57 | });
58 |
59 | // Parse incoming requests
60 | app.use(bodyParser.json());
61 | app.use(bodyParser.urlencoded({ extended: false }));
62 |
63 | // Log every request to the console
64 | app.use(morgan('dev'));
65 |
66 | /**
67 | * ROUTES
68 | */
69 |
70 | // API Routes
71 | // app.use('/', require('./routes/index'));
72 | app.use('/map', require('./routes/map'));
73 | app.use('/search', require('./routes/search'));
74 | app.use('/users', require('./routes/users'));
75 |
76 | // TODO: Create additional routes as necessary
77 |
78 | // Serve static assets and index.html in production
79 | if (ENV === 'production') {
80 | // Serve static assets
81 | app.use(express.static('client/build'));
82 |
83 | // Serve index.html file if no other routes were matched
84 | const { resolve } = require('path');
85 |
86 | app.get('**', (req, res) => {
87 | res.sendFile(resolve(__dirname, 'client', 'build', 'index.html'));
88 | });
89 | }
90 |
91 | module.exports = app;
92 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "navi",
3 | "version": "0.0.1",
4 | "description": "Open source project for Grow with Google Udacity Scholarship Challenge - Navigation app using offline first strategy and google maps api",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "dev": "concurrently \"npm run server\" \"npm run client\"",
9 | "client": "npm run start --prefix client",
10 | "server": "nodemon server.js",
11 | "server:inspect": "nodemon --inspect server.js",
12 | "test": "concurrently \"npm run test-server\" \"npm run test-client\"",
13 | "test-server": "cross-env NODE_ENV=test mocha tests --timeout 10000 -r chai/register-expect",
14 | "test-watch": "nodemon --exec 'npm test'",
15 | "test-client": "npm test --prefix client",
16 | "heroku-postbuild": "cd client/ && npm install && npm install --only=dev --no-shrinkwrap && npm run build"
17 | },
18 | "engines": {
19 | "node": ">=8.9.0"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/TheDevPath/Navi.git"
24 | },
25 | "contributors": [
26 | "Bryan Sharpley (https://github.com/motosharpley)",
27 | "Christopher Gates (https://github.com/tophergates)",
28 | "Raegan Millhollin (https://github.com/desdemonhu)",
29 | "Peter Matthews (https://github.com/pjdmatts)",
30 | "John Kwening (https://github.com/jkwening)",
31 | "Vasanth (https://github.com/Vasanthkesavan)",
32 | "Travis Haley (https://github.com/Mallek",
33 | "Eddie Ford (https://github.com/seckboy)",
34 | "Chris Young (https://github.com/someyoungideas)"
35 | ],
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/TheDevPath/googleMaps-offline-navigator/issues"
39 | },
40 | "devDependencies": {
41 | "babel-eslint": "^8.2.1",
42 | "chai": "^4.1.2",
43 | "chai-http": "^3.0.0",
44 | "concurrently": "^3.5.1",
45 | "cross-env": "^5.1.3",
46 | "eslint": "^4.16.0",
47 | "eslint-config-airbnb-base": "^12.1.0",
48 | "eslint-plugin-import": "^2.8.0",
49 | "mocha": "^5.0.0",
50 | "nodemon": "^1.14.11",
51 | "superagent": "^3.8.2",
52 | "supertest": "^3.0.0"
53 | },
54 | "dependencies": {
55 | "@google/maps": "^0.4.5",
56 | "bcryptjs": "^2.4.3",
57 | "body-parser": "^1.18.2",
58 | "express": "^4.16.2",
59 | "helmet": "^3.9.0",
60 | "jsonwebtoken": "^8.1.1",
61 | "mongodb": "^3.0.2",
62 | "mongoose": "^5.0.1",
63 | "morgan": "^1.9.0"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client/src/routes/home/index.js:
--------------------------------------------------------------------------------
1 | import { h, Component } from 'preact';
2 | import style from './style';
3 | import { route } from 'preact-router';
4 | import Search from '../../components/Search';
5 | import SearchResults from '../../components/SearchResults';
6 |
7 | export default class Home extends Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.state = {
12 | busyMessage: 'Loading your location...',
13 | userPosition: {
14 | lat: '',
15 | lng: ''
16 | },
17 | }
18 |
19 | this.routeToMap = this.routeToMap.bind(this);
20 | this.resetSelectedPin = this.resetSelectedPin.bind(this);
21 | }
22 |
23 | componentDidMount() {
24 | if (this.state.userPosition.lat && this.state.userPosition.lng) {
25 | this._setBusyMessage('');
26 | return;
27 | }
28 |
29 | if ('geolocation' in navigator === false) {
30 | this._setBusyMessage('Could not find your location');
31 | return;
32 | }
33 |
34 | const self = this;
35 | navigator.geolocation.getCurrentPosition(
36 | (position) => {
37 | this.setState({
38 | busyMessage: '',
39 | userPosition: {
40 | lat: position.coords.latitude,
41 | lng: position.coords.longitude
42 | }
43 | });
44 |
45 | console.log('\tuser position found: ', this.state.userPosition);
46 | },
47 | (err) = {
48 | if (err.code && err.code === 1)
49 | self._setBusyMessage('Cound not find your location');
50 | else
51 | self._setBusyMessage('Error occurred while getting your location');
52 | },
53 | {
54 | enableHighAccuracy: false,
55 | timeout: 60000,
56 | maximumAge: Infinity
57 | });
58 | }
59 |
60 | routeToMap() {
61 | this.resetSelectedPin();
62 | route('/maps', true);
63 | }
64 |
65 | resetSelectedPin() {
66 | this.props.selectedPin(null);
67 | }
68 |
69 | _setBusyMessage(message) {
70 | this.setState({ busyMessage: message });
71 | }
72 |
73 | render() {
74 | return (
75 |
76 |
77 |
WELCOME TO
78 |
79 |
80 |
81 |
Where can we take you today?
82 |
84 |
85 |
86 |
{this.state.busyMessage || ''}
87 |
88 |
93 |
94 | );
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/client/src/js/server-requests-utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Library for handling frontend requests to server api endpoints
3 | */
4 |
5 | // node_modules imports
6 | import axios from 'axios';
7 | import Cookies from "universal-cookie";
8 |
9 | // app module imports
10 | import { API_SERVER } from '../../config';
11 |
12 | // module constants
13 | const TOKEN_COOKIE = 'token';
14 | export const BASE_ENDPOINTS = { // Key-value pairs for existing base server endpoints
15 | savedPins: '/search/savedpins',
16 | savedDirections: '/search/directions',
17 | savedHistory: '/search/history',
18 | autocomplete: '/search/autocomplete',
19 | places: '/search/places',
20 | user: '/users/user',
21 | userLogin: '/users/login',
22 | userLogout: '/users/logout',
23 | userRegister: '/users/register',
24 | textsearch: '/search/textsearch',
25 | userReset: '/users/reset-password',
26 | userUpdate: '/users/update',
27 | geocode: '/map/geocode',
28 | }
29 |
30 | export const token = {
31 | setCookie: val => {
32 | const cookies = new Cookies();
33 | cookies.set(TOKEN_COOKIE, val, {path: '/'});
34 | },
35 |
36 | deleteCookie: () => {
37 | const cookies = new Cookies();
38 | cookies.remove(TOKEN_COOKIE);
39 | },
40 |
41 | getCookie: () => {
42 | const cookies = new Cookies();
43 | return cookies.get(TOKEN_COOKIE);
44 | }
45 |
46 | }
47 |
48 | /* makeRequest(...)
49 |
50 | __include__ import {makeRequest} from "../../js/server-requests-utils"
51 | __exmample__ makeRequest('GET','savedPins').then(res => {return res.data})
52 |
53 | __output__ Promise that returns an object with the following keys:
54 | * config
55 | * data: response body
56 | * headers
57 | * request
58 | * status
59 | * statusText
60 |
61 | * See: https://www.npmjs.com/package/axios#response-schema for more on the output
62 | *
63 | * Configuration of Axios for making server requests
64 | * See 'Creating an instance' and 'Request Config' sections for more information:
65 | * https://www.npmjs.com/package/axios
66 | */
67 |
68 | export const makeRequest = (method='GET', baseEndPoint, endPointAddon='', bodyData={}, params={}, headers={}) => {
69 |
70 | const validMethod = ['GET', 'POST', 'DELETE', 'PATCH','PUT']; //determined by axios
71 |
72 | //if it's not a valid method, return rejected promise
73 | if (!validMethod.includes(method)) {
74 | return new Promise(function (res,rej) {
75 | rej(TypeError(`Invalid request method: ${method}`));
76 | })
77 | }
78 |
79 | const url = ((BASE_ENDPOINTS[baseEndPoint] || baseEndPoint) + endPointAddon).trim();
80 | headers['x-access-token'] = token.getCookie();
81 | const config = {method,
82 | url,
83 | params,
84 | headers,
85 | baseURL: API_SERVER,
86 | data: bodyData
87 | }
88 |
89 | console.log('makeRequest().config: ', config);
90 |
91 | return axios.request(config);
92 | }
93 |
--------------------------------------------------------------------------------
/client/src/components/app.js:
--------------------------------------------------------------------------------
1 | import { h, Component } from 'preact';
2 | import { Router } from 'preact-router';
3 | import Match from 'preact-router/match';
4 |
5 | // import components
6 | import Nav from './Nav';
7 | import Logo from './Logo';
8 |
9 | // import routes
10 | import Home from '../routes/home';
11 | import Profile from '../routes/profile';
12 | import Directions from '../routes/directions';
13 | import Maps from '../routes/maps';
14 | import Account from '../routes/account';
15 | import SignOut from '../routes/signout';
16 | import Settings from '../routes/settings';
17 |
18 | // Available screen real state after factoring space for navbar
19 | const AVAIL_PANE_HEIGHT = screen.availHeight * 0.93;
20 |
21 | export default class App extends Component {
22 | constructor() {
23 | super();
24 | this.state = {
25 | navbarHeight: screen.availHeight - AVAIL_PANE_HEIGHT,
26 | userPosition: null,
27 | searchResult: null,
28 | selectedPin: null,
29 | };
30 |
31 | this.updateSearchResult = this.updateSearchResult.bind(this);
32 | this.setUserPosition = this.setUserPosition.bind(this);
33 | this.setSelectedPin = this.setSelectedPin.bind(this);
34 | }
35 |
36 | /** Gets fired when the route changes.
37 | * @param {Object} event "change" event from [preact-router](http://git.io/preact-router)
38 | * @param {string} event.url The newly routed URL
39 | */
40 | handleRoute = e => {
41 | this.currentUrl = e.url;
42 | };
43 |
44 | updateSearchResult(placeDetail) {
45 | this.setState({ searchResult: placeDetail });
46 | }
47 |
48 | setUserPosition(userPosition) {
49 | this.setState({ userPosition, });
50 | }
51 |
52 | setSelectedPin(selectedPin) {
53 | this.setState({ selectedPin });
54 | }
55 |
56 | render() {
57 | return (
58 |
59 |
60 |
61 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
75 |
76 |
77 | );
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/controllers/utils-controller.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Utility controller module.
3 | * @module controller/utils-controller
4 | */
5 |
6 | const jwt = require('jsonwebtoken');
7 | const { JWT_KEY } = require('../config');
8 |
9 | /**
10 | * @description Authorization middleware for verifying user access rights
11 | *
12 | * @apiError 401 {request error} Unauthorized - no token provided.
13 | * @apiError 403 {request error} Unable to authenticate user.
14 | *
15 | * @param {string} req.headers['x-access-token'] - request header key used for
16 | * tracking token
17 | */
18 | exports.verifyToken = (req, res, next) => {
19 | const token = req.headers['x-access-token'];
20 |
21 | if (!token) {
22 | return res.status(401).send({
23 | auth: false,
24 | message: 'No token provided.',
25 | });
26 | }
27 |
28 | jwt.verify(token, JWT_KEY, (err, decoded) => {
29 | if (err) {
30 | return res.status(403).send({
31 | auth: false,
32 | message: 'Failed to authenticate token.',
33 | });
34 | }
35 |
36 | // if good, save to request for next route
37 | req.userId = decoded.id;
38 | req.token = token;
39 | next();
40 | });
41 | };
42 |
43 | /**
44 | * @description Utility function for generating query string from an object with
45 | * key-value pairs for params needed for http request using native node https.
46 | *
47 | * @param {Object} paramsObject: key-value pairs for params needed for request
48 | * to be parsed as query string
49 | */
50 | exports.convertToQueryString = (paramsObject) => {
51 | const str = [];
52 | const keys = Object.keys(paramsObject);
53 | keys.forEach((element) => {
54 | str.push(`${encodeURIComponent(element)}=${encodeURIComponent(paramsObject[element])}`);
55 | });
56 | return str.join('&');
57 | };
58 |
59 | /**
60 | * @description Utility function for processing google places autocomplete
61 | * results.
62 | *
63 | * @param {Object} queryResult: JSON response containing two root elememts:
64 | * - status: contains metadata on the request along with status codes
65 | * - predictions: an array of query predictions
66 | *
67 | * @returns {[Ojbect]} An array of suggestions as object with two root elements:
68 | * - prediction: the predicted query
69 | * - placeID: if prediction is a place else ''
70 | */
71 | exports.processAutocomplete = (queryResult) => {
72 | const descriptions = [];
73 | const placeIds = [];
74 | const descSubfields = [];
75 |
76 | queryResult.predictions.forEach((result) => {
77 | const { description } = result;
78 | const placeId = result.place_id || '';
79 | const subfields = {
80 | mainText: result.structured_formatting.main_text,
81 | secondaryText: result.structured_formatting.secondary_text,
82 | };
83 |
84 | descriptions.push(description);
85 | placeIds.push(placeId);
86 | descSubfields.push(subfields);
87 | });
88 | return { descriptions, placeIds, descSubfields };
89 | };
90 |
--------------------------------------------------------------------------------
/client/src/sw.js:
--------------------------------------------------------------------------------
1 | const CACHE_VERSION = 1;
2 |
3 | const CACHE_NAME = `mapE_v${CACHE_VERSION}`;
4 | const urlsToCache = [
5 | '/',
6 | '/bundle.js',
7 | '/index.js',
8 | '/sw.js',
9 | '/style/index.css',
10 | '/assets/icons/leaflet/marker-icon.png',
11 | '/assets/icons/leaflet/marker-shadow.png',
12 | ];
13 |
14 | self.addEventListener('install', function(event) {
15 | // Perform install steps
16 | event.waitUntil(
17 | caches.open(CACHE_NAME)
18 | .then(function(cache) {
19 | console.log('Opened cache');
20 | return cache.addAll(urlsToCache);
21 | }).catch(function(err) {
22 | console.log('Cache install failed: ', err);
23 | })
24 | );
25 | });
26 |
27 | self.addEventListener('activate', event => {
28 | // delete any caches that aren't CACHE_NAME
29 | // which will get rid of previous caches
30 | event.waitUntil(
31 | caches.keys().then(keys => Promise.all(
32 | keys.map(key => {
33 | if (key !== CACHE_NAME) {
34 | return caches.delete(key);
35 | }
36 | })
37 | )).then(() => {
38 | console.log(`${CACHE_NAME} is now ready to handle fetches!`);
39 | })
40 | );
41 | });
42 |
43 | self.addEventListener('fetch', function(event) {
44 | const re = new RegExp('signin|register');
45 | if (event.request.url.match(re)) {
46 | console.log("DONOTCACHE:" + event.request.url);
47 | return event.respondWith(function () {
48 | return fetch(event.request);
49 | }());
50 | }
51 | event.respondWith(
52 | caches.match(event.request)
53 | .then(function(response) {
54 | // Cache hit - return response
55 | if (response) {
56 | return response;
57 | }
58 |
59 | // IMPORTANT: Clone the request. A request is a stream and
60 | // can only be consumed once. Since we are consuming this
61 | // once by cache and once by the browser for fetch, we need
62 | // to clone the response.
63 | const fetchRequest = event.request.clone();
64 |
65 | return fetch(fetchRequest).then(
66 | function(response) {
67 | // Check if we received a valid response
68 | if(!response || response.status !== 200 || response.type !== 'basic') {
69 | return response;
70 | }
71 |
72 | // IMPORTANT: Clone the response. A response is a stream
73 | // and because we want the browser to consume the response
74 | // as well as the cache consuming the response, we need
75 | // to clone it so we have two streams.
76 | const responseToCache = response.clone();
77 |
78 | caches.open(CACHE_NAME)
79 | .then(function(cache) {
80 | cache.put(event.request, responseToCache);
81 | });
82 |
83 | return response;
84 | }
85 | );
86 | })
87 | );
88 | });
89 |
--------------------------------------------------------------------------------
/client/src/components/AccountForm/style.css:
--------------------------------------------------------------------------------
1 | .inherit {
2 | display: inherit;
3 | height: inherit;
4 | width: inherit;
5 | }
6 |
7 | .display {
8 |
9 | margin: 10vh auto 0;
10 | }
11 |
12 | .logo {
13 | display: inherit;
14 | margin: auto;
15 | padding: 2vh;
16 | width: 50vw;
17 | }
18 |
19 | .form {
20 | display: grid;
21 | padding: 0 5vh;
22 | align-content: flex-start;
23 | }
24 |
25 | .form > p {
26 | margin: 1vh;
27 | font-size: 1.2em;
28 | }
29 |
30 |
31 |
32 | .formChildReset {
33 | display: block;
34 | margin: 0.7vh auto;
35 | width: 100%;
36 | padding: 3vw;
37 | border-color: lightslategray;
38 | border-radius: 8px;
39 | font-size: 1.2em;
40 | }
41 |
42 | .strike {
43 | display: block;
44 | text-align: center;
45 | overflow: hidden;
46 | white-space: nowrap;
47 | }
48 |
49 | .strike > span {
50 | position: relative;
51 | display: inline-block;
52 | color: #087E8B;
53 | font-weight:bold;
54 | text-transform:lowercase;
55 | font-family: 'Open Sans Bold';
56 | }
57 |
58 | .strike > span:before,
59 | .strike > span:after {
60 | content: "";
61 | position: absolute;
62 | top: 50%;
63 | width: 9999px;
64 | height: 1px;
65 | background: black;
66 | }
67 |
68 | .strike > span:before {
69 | right: 100%;
70 | margin-right: 15px;
71 | }
72 |
73 | .strike > span:after {
74 | left: 100%;
75 | margin-left: 15px;
76 | }
77 |
78 | .link2 {
79 | display: block;
80 | position: fixed;
81 | right: 5vw;
82 | bottom: 1vh;
83 | font-size: 1.1em;
84 | color: blue;
85 | }
86 | .link2 a, a{
87 | color: #087E8B;
88 | text-decoration: none;
89 | font-size: 15px;
90 | font-family: 'Open Sans', arial, sans-serif;
91 | font-weight:bold;
92 | }
93 | button {
94 | background-color: #28C0D1;
95 | color: #ffffff;
96 | text-transform:uppercase;
97 | font-weight:bold;
98 | border-radius: 10px;
99 | font-size: 16px;
100 | line-height: 20px;
101 | padding: 13px;
102 | width: 100%;
103 | border:0;
104 | margin:20px 0 40px;
105 | }
106 | button:hover{
107 | background: #28c0d1;
108 | background: linear-gradient(135deg, #28c0d1 0%,#10416c 100%);
109 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#28c0d1', endColorstr='#10416c',GradientType=1 );
110 | }
111 |
112 | .regBtn {
113 | background-color: #10416C;
114 | }
115 | input {
116 | border-radius: 10px;
117 | border: 1px solid #95989A;
118 | background-color: #ffffff;
119 | font-size: 16px;
120 | line-height: 20px;
121 | padding: 13px;
122 | margin: 10px 0;
123 | }
124 | @media only screen and (min-width: 800px) {
125 |
126 | .logo{
127 | width: 40vw;
128 | }
129 | .display {
130 | width: 50vw;
131 |
132 | }
133 | }
134 |
135 | @media only screen and (min-width: 1200px) {
136 |
137 | .logo{
138 | width: 20vw;
139 | }
140 | .display {
141 | width: 35vw;
142 |
143 | }
144 | }
--------------------------------------------------------------------------------
/client/src/components/Search/index.js:
--------------------------------------------------------------------------------
1 | import { h , Component, cloneElement } from 'preact';
2 | import style from './style';
3 | import { route } from 'preact-router';
4 | import { makeRequest, BASE_ENDPOINTS } from '../../js/server-requests-utils';
5 | const OK_STATUS = 'OK';
6 |
7 | export default class Search extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | value: '',
12 | predictions: [],
13 | placeIDs: [],
14 | descSubfields: [],
15 | marker: null
16 | };
17 |
18 | this.handleChange = this.handleChange.bind(this);
19 | this.handleSubmit = this.handleSubmit.bind(this);
20 | this.handleSelectedPlace = this.handleSelectedPlace.bind(this);
21 | }
22 |
23 | handleChange(event) {
24 | this.setState({value: event.target.value});
25 | // process autocomplete request and update list
26 | makeRequest('POST', BASE_ENDPOINTS.autocomplete, '', {
27 | input: this.state.value,
28 | lat: this.props.position.lat,
29 | lng: this.props.position.lng,
30 | }).then((response) => {
31 | const status = response.data.status;
32 | const predictions = response.data.predictions;
33 | const placeIds = response.data.placeIds;
34 | const descSubfields = response.data.descSubfields;
35 |
36 | this.setState({
37 | predictions,
38 | placeIds,
39 | descSubfields,
40 | });
41 | });
42 | }
43 |
44 | handleSubmit(event) {
45 | event.preventDefault();
46 | makeRequest('POST', BASE_ENDPOINTS.textsearch, '', {
47 | input: this.state.value,
48 | lat: this.props.position.lat,
49 | lng: this.props.position.lng,
50 | }).then((response) => {
51 | if(response.data.status == OK_STATUS)
52 | {
53 | const [searchResult] = response.data.results;
54 | this.handleSelectedPlace(searchResult);
55 | }
56 | else
57 | alert(response.data.status);
58 | });
59 | }
60 |
61 | /**
62 | * On search input query completion, add a marker to the map at the search result.
63 | *
64 | * @param {*} placeDetail
65 | */
66 | handleSelectedPlace(placeDetail) {
67 | if (this.props.routeUrl === '/maps') {
68 | this.props.addMarker(placeDetail.geometry.location, placeDetail.place_id);
69 |
70 | //TO DO: Customize the marker popup
71 | // this.state.marker.bindPopup(`
${placeDetail.name || ''} ${placeDetail.formatted_address}`)
72 | } else {
73 | this.props.updateSearchResult(placeDetail);
74 | route('/maps', true);
75 | }
76 | }
77 |
78 | render() {
79 | // pass props to children components
80 | const childWithProps = this.props.children.map((child) => {
81 | return cloneElement(child, {
82 | predictions: this.state.predictions,
83 | descSubfields: this.state.descSubfields,
84 | onClicked: this.handleSelectedPlace
85 | });
86 | });
87 |
88 | return (
89 |
94 | );
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at motosharpley@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/controllers/search-history-controller.js:
--------------------------------------------------------------------------------
1 | const SearchHistory = require('../models/search-history');
2 |
3 | /**
4 | * @description Handles request for user search history
5 | *
6 | * @api {GET} /search/history
7 | * @apiSuccess 200 {SearchHistory} The collection of saved search queries.
8 | * @apiError 500 {server error} Problem finding all saved search queries.
9 | */
10 | exports.getSearchHistory = (appReq, appRes) => {
11 | SearchHistory.find({ user: appReq.userId }).then((searchHistory) => {
12 | appRes.status(200).send({ searchHistory });
13 | }, (e) => {
14 | appRes.status(500).send(e);
15 | });
16 | };
17 |
18 | /**
19 | * @description Handle request for getting a specified number of recently
20 | * saved queries
21 | *
22 | * @api {GET} /search/history/recent/:num
23 | * @apiSuccess 200 {searchHistory} The requested recent num search queries.
24 | * @apiError 500 {server error} Server error
25 | * @apiError 404 {request error} Invalid req.params.num
26 | *
27 | * @param {String} appReq.params.num - Number of most recent saved queries to return.
28 | */
29 | exports.getRecent = (appReq, appRes) => {
30 | const num = parseInt(appReq.params.num);
31 | if (isNaN(num)) {
32 | return appRes.status(404).send({
33 | error: 'Invalid params passed.',
34 | param: appReq.params.num,
35 | parsedInt: num
36 | });
37 | }
38 |
39 | SearchHistory.find({ user: appReq.userId })
40 | .sort({save_date: 'desc'})
41 | .then((result) => {
42 | const searchHistory = result.slice(0, num);
43 | appRes.send({ searchHistory });
44 | }, (err) => {
45 | appRes.status(500).send(err);
46 | });
47 | };
48 |
49 | /**
50 | * @description Handles request to save a search query if not already in db
51 | *
52 | * @api {POST} /search/history/:query
53 | * @apiSuccess 200 {searchHistory} The document representing the saved query.
54 | * @apiError 500 {server error} Problem saving the requested pin.
55 | *
56 | * @param {string} appReq.params.query - the string query to save
57 | */
58 | exports.saveQuery = (appReq, appRes) => {
59 | const saveQuery = {
60 | query: appReq.params.query,
61 | user: appReq.userId
62 | }
63 |
64 | const update = {
65 | lastModified: true,
66 | $currentDate : {
67 | save_date: {$type: 'date'},
68 | },
69 | }
70 |
71 | const options = {
72 | new: true, // return modified doc instead of original
73 | upsert: true, // create if doesn't exist
74 |
75 | }
76 |
77 | SearchHistory.findOneAndUpdate(saveQuery, update, options,
78 | (err, searchHistory) => {
79 | if (err) {
80 | return appRes.status(500).send(err);
81 | }
82 |
83 | return appRes.status(200).send({searchHistory});
84 | });
85 | };
86 |
87 | /**
88 | * @description Handles request to delete all saved search history
89 | *
90 | * @api {DELETE} /search/history
91 | * @apiSuccess 200 {success} Sucessfully deleted all search history data
92 | * @apiError 500 {server error}
93 | */
94 | exports.deleteSearchHistory = (appReq, appRes) => {
95 | SearchHistory.remove({ user: appReq.userId }).then(() => {
96 | appRes.status(200).send({ success: true });
97 | }).catch((err) => {
98 | appRes.status(500).send({
99 | error: err,
100 | success: false,
101 | });
102 | });
103 | };
104 |
--------------------------------------------------------------------------------
/client/src/routes/directions/index.js:
--------------------------------------------------------------------------------
1 | import { h, Component } from "preact";
2 | import style from "./style";
3 | import MapPane from '../../components/LeafletOsmMap/MapPane';
4 |
5 | /**
6 | * Leaflet related imports: leaflet, pouchdb module, and routing machine module
7 | */
8 | import '../../../node_modules/leaflet/dist/leaflet.css';
9 | import '../../../node_modules/leaflet-routing-machine/dist/leaflet-routing-machine.css';
10 | import L from '../../js/leaflet-tileLayer-pouchdb-cached';
11 | import Routing from '../../../node_modules/leaflet-routing-machine/src/index.js';
12 |
13 | /**
14 | * TIle layer configuration and attribution constants
15 | */
16 | const OSM_URL = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
17 | const OSM_ATTRIB = '©
OpenStreetMap contributors';
18 | const OSM_TILE_LAYER = new L.TileLayer(OSM_URL, {
19 | attribution: OSM_ATTRIB,
20 | useCache: true,
21 | crossOrigin: true,
22 | });
23 |
24 | // redirect marker icon path to assets directory
25 | L.Icon.Default.imagePath = '../../assets/icons/leaflet/';
26 |
27 | export default class Directions extends Component {
28 | constructor(props) {
29 | super(props);
30 | this.state = {
31 | // map: null,
32 | // lat: null,
33 | // lng: null,
34 | // watchID: null,
35 | airport: {
36 | lat: 38.8512462,
37 | lng: -77.0424202,
38 | address: 'Ronald Reagan Washington National Airport, Arlington, VA 22202',
39 | },
40 | whiteHouse: {
41 | address: '1600 Pennsylvania Ave NW, Washington, DC 20500',
42 | lat: 38.8976094,
43 | lng: -77.0389236,
44 | }
45 | }
46 | // this.initMap = this.initMap.bind(this);
47 | }
48 |
49 | componentDidMount() {
50 | const map = L.map('map');
51 | map.addLayer(OSM_TILE_LAYER);
52 |
53 | const control = Routing.control({
54 | waypoints: [
55 | L.latLng(this.state.whiteHouse.lat,
56 | this.state.whiteHouse.lng),
57 | L.latLng(this.state.airport.lat,
58 | this.state.airport.lng),
59 | ],
60 | routeWhileDragging: true,
61 | }).addTo(map);
62 |
63 | map.on('click', function(event) {
64 | const container = L.DomUtil.create('div');
65 | const startBtn = createButton('Start Here', container);
66 | const destBtn = createButton('End Here', container);
67 |
68 | L.popup()
69 | .setContent(container)
70 | .setLatLng(event.latlng)
71 | .openOn(map);
72 |
73 | L.DomEvent.on(startBtn, 'click', function() {
74 | control.spliceWaypoints(0, 1, event.latlng);
75 | map.closePopup();
76 | });
77 |
78 | L.DomEvent.on(destBtn, 'click', function() {
79 | control.spliceWaypoints(control.getWaypoints().length - 1, 1, event.latlng);
80 | map.closePopup();
81 | });
82 | });
83 | }
84 |
85 | render() {
86 | return (
87 |
88 |
89 |
90 | );
91 | }
92 | }
93 |
94 | function createButton(label, container) {
95 | const btn = L.DomUtil.create('button', '', container);
96 | btn.setAttribute('type', 'button');
97 | btn.innerHTML = label;
98 | return btn;
99 | }
100 |
--------------------------------------------------------------------------------
/client/src/assets/icons/leaflet/SVG/whiteLogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/client/src/assets/icons/leaflet/SVG/darkLogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/docs/learning.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Navi: An open source project built for Grow with Google
4 |
5 |
6 |
7 |
8 | ## Technologies used
9 | ### Preact
10 | Preact is a JavaScript library that offers a fast 3kb alternative to React with the same ES6 API. Currently, Preact has over 10,000 stars on GitHub. Preact gives the developer an edge to build super fast JavaScript web applications without the constant headache of performance improvement because of its lightweight footprint
11 |
12 | - [Preact authentication tutorial](https://auth0.com/blog/preact-authentication-tutorial/)
13 | - [Switching to Preact](https://preactjs.com/guide/switching-to-preact)
14 | - [Preact and progressive web apps](https://preactjs.com/guide/progressive-web-apps)
15 | - [Sample code via repls in Preact](https://preactjs.com/repl)
16 | - [Preact on Github](https://github.com/developit/preact)
17 |
18 | ### Node
19 |
20 | Node.js is an open-source, cross-platform JavaScript run-time environment for executing JavaScript code server-side
21 |
22 | - [Node learning resources](https://github.com/vioan/nodejs-learning-resources)
23 | - [Node resources - Medium article](https://medium.com/@cabot_solutions/15-top-resources-for-node-js-developers-2029ab30cfa4)
24 |
25 | #### Express
26 | - [Serving static files in Express](https://expressjs.com/en/starter/static-files.html)
27 | - [How to deploy a Node app in express using Cloud9](https://medium.com/the-n00b-code-chronicles/how-to-deploy-a-node-js-web-app-using-express-in-cloud9-91e73910293f)
28 | - [Getting started - Express](https://expressjs.com/en/starter/installing.html)
29 |
30 | ### Leaflet
31 | - [Leaflet](http://leafletjs.com/) is an open-source JavaScript library for mobile-friendly interactive maps. It is light at just 38kb
32 | - Layers Out of the Box
33 | - Interaction Features
34 | - Visual Features
35 | - Zoom and pan animation
36 | - [more](leaflet.html)
37 |
38 | ## Concepts
39 | ### Service workers
40 | - [Web fundamentals code lab](https://developers.google.com/web/fundamentals/codelabs/) Progressive Web Apps, Service Workers, Simple Offline Web App and Others
41 | - [PWA case studies](https://developers.google.com/web/showcase/tags/progressive-web-apps)
42 | - [A Beginner's Guide To Progressive Web Apps](https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/) - with sample code Smashing Magazine
43 | - [Fetch API](https://developers.google.com/web/updates/2015/03/introduction-to-fetch)
44 | - [Fetch API - MDN](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent)
45 |
46 |
47 | ### ES6
48 | - [Template literals (from Google Web Fundamentals)](https://developers.google.com/web/updates/2015/01/ES6-Template-Strings)
49 | - [Understanding ES6](https://leanpub.com/understandinges6/read) (an online book by Nicholas Zakas. Covers block bindings, destructuring, Symbols, Sets and Maps, Iterators and Generators, JavaScript Classes,Improved Array Capabilities, Promises, Proxies and the Reflection API, and Encapsulating Code with Modules)
50 | - [Three great free books](http://exploringjs.com/) (one is O'Reilly animal Safari book series) on Javascript, and ES6 and ES7
51 | - [https://javascript.info](https://javascript.info)
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/docs/CONTRIBUTORS.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Google Offline Navigator: An open source project built for Grow with Google
4 |
5 |
6 | # Contributors
7 |
8 | ### Special thanks for all the people who had helped this [project](index.html) so far :
9 |
10 | .
| N
| A
| V
| I
| .
11 | ---------------- | ---------------- | ----------------- |---------------- | ----------------- | ----------------
12 |  [Bryan](https://github.com/motosharpley)
|  [John Kwening](https://github.com/jkwening)
|  [Travis](https://github.com/mallek)
|  [tanyagupta](https://github.com/tanyagupta)
|  [Sameer Khan](https://github.com/sameerkhan116)
|  [Christopher Gates](https://github.com/tophergates)
13 |  [desdemonhu](https://github.com/desdemonhu)
| [Vignesh](https://github.com/vettyvignesh)
|  [Stephen Chavez](https://github.com/redragonx)
|  [Khusbu Chandra](https://github.com/khusbuchandra)
| [Eddie](https://github.com/seckboy)
|  [Ricky](https://github.com/ricky-rose)
14 |  [Alice Palazzolo](https://github.com/alicepalazzolo)
|  [Emily Sperry](https://github.com/sperrye)
|  [Peter Matthews](https://github.com/pjdmatts)
| > [Owen](https://github.com/othomas1984)
|  [Scott Westover](https://github.com/scottwestover)
|  [Ben Reckas](https://github.com/benreckas)
15 |  [Evan](https://github.com/CodeDraken)
|  [Mebin Robin](https://github.com/mebin)
|  [Minh](https://github.com/Minh-B-Dang)
|  [Sonali Shukla](https://github.com/sonalikatara)
| |
16 |
17 | ### If you would like to join this list.
18 |
19 | We're currently looking for contributions for the following:
20 |
21 | - [ ] Bug fixes
22 | - [ ] Translations
23 | - [ ] etc...
24 |
25 | For more information, please refer to our [CONTRIBUTING](CONTRIBUTING.md) guide.
26 |
27 | 
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/controllers/saved-directions-controller.js:
--------------------------------------------------------------------------------
1 | const SavedDirections = require('../models/saved-directions');
2 |
3 | /**
4 | * @description Handles request for user saved directions
5 | *
6 | * @api {GET} /search/directions
7 | * @apiSuccess 200 {SavedDirections} The collection of all saved directions.
8 | * @apiError 500 {server error} Problem with server.
9 | */
10 | exports.getSavedDirections = (appReq, appRes) => {
11 | SavedDirections.find({ user: appReq.userId }).then((directions) => {
12 | appRes.status(200).send({ directions });
13 | }, (err) => {
14 | appRes.status(500).send(err);
15 | });
16 | };
17 |
18 | /**
19 | * @description Handle request for getting a specified number of recently
20 | * saved directions
21 | *
22 | * @api {GET} /search/directions/recent/:num
23 | * @apiSuccess 200 {directions} The requested recent num of saved directions.
24 | * @apiError 500 {server error} Server error
25 | * @apiError 404 {request error} Invalid req.params.num
26 | *
27 | * @param {String} appReq.params.num - Number of most recent saved directions to return.
28 | */
29 | exports.getRecentDirections = (appReq, appRes) => {
30 | const num = parseInt(appReq.params.num);
31 | if (isNaN(num)) {
32 | return appRes.status(404).send({
33 | error: 'Invalid params passed.',
34 | param: appReq.params.num,
35 | parsedInt: num
36 | });
37 | }
38 |
39 | SavedDirections.find({ user: appReq.userId })
40 | .sort({save_date: 'desc'})
41 | .then((result) => {
42 | const directions = result.slice(0, num);
43 | appRes.send({ directions });
44 | }, (err) => {
45 | appRes.status(500).send(err);
46 | });
47 | };
48 |
49 | /**
50 | * @description Handles request to save a search query if not already in db
51 | *
52 | * @api {POST} /search/directions/
53 | * @apiSuccess 200 {directions} The document representing the saved query.
54 | * @apiError 500 {server error} Problem saving the requested pin.
55 | *
56 | * @param {lat, lng} appReq.body.origin - starting location
57 | * @param {lat, lng} appReq.body.destination - ending location
58 | * @param {[{lat, lng}]} appReq.body.waypoints - array of geocoded waypoints
59 | * from origin to destination
60 | * @param {[String]} appReq.body.directions - array of string representing
61 | * turn-by-turn directions
62 | * @param {string} appReq.body.label - (optional) user provided label description
63 | */
64 | exports.saveDirections = (appReq, appRes) => {
65 | const saveQuery = {
66 | origin: appReq.body.origin,
67 | destination: appReq.body.destination,
68 | waypoints: appReq.body.waypoints,
69 | directions: appReq.body.directions,
70 | label: appReq.body.label || `${appReq.body.origin} - ${appReq.body.destination}`,
71 | user: appReq.userId,
72 | }
73 |
74 | const update = {
75 | lastModified: true,
76 | $currentDate : {
77 | save_date: {$type: 'date'},
78 | },
79 | $set: {
80 | waypoints: saveQuery.waypoints,
81 | directions: saveQuery.directions,
82 | label: saveQuery.label,
83 | }
84 | }
85 |
86 | const options = {
87 | new: true, // return modified doc instead of original
88 | upsert: true, // create if doesn't exist
89 | }
90 |
91 | SavedDirections.findOneAndUpdate({
92 | origin: saveQuery.origin,
93 | destination: saveQuery.destination,
94 | user: saveQuery.user,
95 | },
96 | update,
97 | options,
98 | (err, directions) => {
99 | if (err) {
100 | return appRes.status(500).send(err);
101 | }
102 |
103 | return appRes.status(200).send({directions});
104 | });
105 | };
106 |
107 | /**
108 | * @description Handles request to delete all saved directions
109 | *
110 | * @api {DELETE} /search/directions
111 | * @apiSuccess 200 {success} Sucessfully deleted all directions data
112 | * @apiError 500 {server error}
113 | */
114 | exports.deleteSavedDirections = (appReq, appRes) => {
115 | SavedDirections.remove({ user: appReq.userId }).then(() => {
116 | appRes.status(200).send({ success: true });
117 | }).catch((err) => {
118 | appRes.status(500).send({
119 | error: err,
120 | success: false,
121 | });
122 | });
123 | };
124 |
125 |
--------------------------------------------------------------------------------
/controllers/saved-pins-controller.js:
--------------------------------------------------------------------------------
1 | /* eslint no-underscore-dangle: 0 */
2 | const { ObjectID } = require('mongodb');
3 |
4 | const SavedPins = require('../models/saved-pins');
5 |
6 | /**
7 | * @description Handles request for user saved pins
8 | *
9 | * @api {GET} /search/savedpins
10 | * @apiSuccess 200 {SavedPins} The collection of saved pins.
11 | * @apiError 500 {server error} Problem finding all saved pins.
12 | */
13 | exports.getSavedPins = (appReq, appRes) => {
14 | SavedPins.find({ user: appReq.userId }).then((savedPins) => {
15 | appRes.send({ savedPins });
16 | }, (e) => {
17 | appRes.status(500).send(e);
18 | });
19 | };
20 |
21 | /**
22 | * @description Handle request for getting saved pin with a given id
23 | *
24 | * @api {GET} /search/savedpins/:id
25 | * @apiSuccess 200 {Pin} The requested pin object.
26 | * @apiError 500 {server error}
27 | * @apiError 404 {request error} Invalid pin id.
28 | *
29 | * @param {String} appReq.params.id - Unique id for the requested pin
30 | */
31 | exports.getSavedPinsById = (appReq, appRes) => {
32 | const params = { id: appReq.params.id };
33 | if (!ObjectID.isValid(params.id)) {
34 | return appRes.status(404).send();
35 | }
36 |
37 | SavedPins.findOne({
38 | _id: params.id,
39 | user: appReq.userId,
40 | }).then((pin) => {
41 | // expect db id to be unique but just in case verifiy user._id
42 | if (!pin || appReq.userId !== pin.user.toString()) {
43 | return appRes.status(404).send();
44 | }
45 | return appRes.send({ pin });
46 | }).catch((e) => {
47 | appRes.status(500).send(e);
48 | });
49 | };
50 |
51 | /**
52 | * @description Handles request to save a pin
53 | *
54 | * @api {POST} /search/savedpins
55 | * @apiSuccess 200 {SavedPins} The document representing the saved pin.
56 | * @apiError 500 {server error} Problem saving the requested pin.
57 | *
58 | * @param {number} appReq.body.lat - lattitude coordinate for pin location
59 | * @param {number} appReq.body.lng - longitude coordinate for pin location
60 | * @param {string} appReq.body.place_id - name of pin location
61 | */
62 | exports.postSavedPins = (appReq, appRes) => {
63 |
64 | //ensure not duplicate first
65 | SavedPins.find({ user: appReq.userId })
66 | .then(savedPins => {
67 | //check to see if the pin already exists
68 | return savedPins.filter(pin => {
69 | return (pin.lat == appReq.body.lat) && (pin.lng == appReq.body.lng)
70 | });
71 |
72 | })
73 | .then(duplicatePins => {
74 |
75 | if (duplicatePins.length > 0) {
76 | appRes.status(400).send({ duplicatePin: duplicatePins, message: 'Duplicate found. Pin not saved.' });
77 | return
78 | }
79 |
80 | const newPin = new SavedPins({
81 | lat: appReq.body.lat,
82 | lng: appReq.body.lng,
83 | place_id: appReq.body.place_id,
84 | desc: appReq.body.desc,
85 | user: appReq.userId, // authenticated user's id
86 | });
87 |
88 | newPin.save().then((pin) => {
89 | appRes.send({ pin });
90 | }, (e) => {
91 | appRes.status(500).send(e);
92 | });
93 |
94 |
95 | })
96 | };
97 |
98 | /**
99 | * @description Handles request to delete all saved pins
100 | *
101 | * @api {DELETE} /search/savedpins
102 | * @apiSuccess 200
103 | * @apiError 500 {server error}
104 | */
105 | exports.deleteSavedPins = (appReq, appRes) => {
106 | SavedPins.remove({ user: appReq.userId }).then(() => {
107 | appRes.status(200).send();
108 | }).catch((e) => {
109 | appRes.status(500).send(e);
110 | });
111 | };
112 |
113 | /**
114 | * @description Handles request to delete a saved pin with given id
115 | *
116 | * @api {DELETE} /search/savedpins/:id
117 | * @apiSuccess 200 {Pin} The deleted pin object.
118 | * @apiError 500 {server error}
119 | * @apiError 404 {request error} Invalid pin id.
120 | *
121 | * @param {String} appReq.params.id - Unique id for the requested pin
122 | */
123 | exports.deleteSavedPinsById = (appReq, appRes) => {
124 | const params = { id: appReq.params.id };
125 | if (!ObjectID.isValid(params.id)) {
126 | return appRes.status(404).send();
127 | }
128 |
129 | SavedPins.findByIdAndRemove(params.id).then((pin) => {
130 | if (!pin) {
131 | return appRes.status(404).send();
132 | }
133 |
134 | appRes.send({ pin });
135 | }).catch((e) => {
136 | appRes.status(500).send(e);
137 | });
138 | };
139 |
--------------------------------------------------------------------------------
/client/src/js/saved-places.js:
--------------------------------------------------------------------------------
1 | import {h, Component} from "preact";
2 | import {makeRequest} from "./server-requests-utils";
3 | import {createButton, createLabel} from "./utilities";
4 |
5 | /* Set all settings for saved place icon
6 | Waiting for new icon for favorites
7 | Full list of options: http://leafletjs.com/reference-1.3.0.html#icon
8 | */
9 |
10 | const FAV_MARKER_OPTIONS = {
11 | iconUrl: '../../assets/icons/leaflet/marker-icon-fav-2x.png',
12 | className: 'favorites',
13 | iconSize: [25, 41], //native aspect ratio: [28, 41]
14 | };
15 |
16 |
17 | /**
18 | * Exported Functions
19 | */
20 |
21 | //Get and drop pins from any user on any map (all arguments are optional)
22 | const fetchAndDropUserPins = (user_id, mapObj, L) => {
23 |
24 | getSavedPins(user_id)
25 | .then(savedPins => {
26 |
27 | if (!savedPins) return;
28 |
29 | const pinMarkers = makePinMarkers(savedPins, L);
30 | if (mapObj != null) dropPin(pinMarkers, mapObj);
31 |
32 | })
33 | }
34 |
35 | const getSavedPins = (user_id) => {
36 | //GET 'search/savedpins/:user_id'
37 |
38 | return makeRequest('GET', 'savedPins', '', user_id) //pinsPromised
39 | .then(res => res.data.savedPins)
40 | .catch(err => {
41 | // console.log(`Couldn't get the user pins: ${err}`);
42 | });
43 |
44 | }
45 |
46 | //This function generates markers and drops them on a map (if specified)
47 | const makePinMarkers = (pinArray = [], L, markerOptions=FAV_MARKER_OPTIONS) => {
48 |
49 | const icon = L ? L.icon(FAV_MARKER_OPTIONS) : undefined;
50 |
51 | let pinMarkers = [];
52 |
53 | for (const pin of pinArray) {
54 | //create marker for the pin and bind it ot the map
55 |
56 | let thisMarker = []; //the marker icon cannot be changed after the marker is created?
57 |
58 | if (icon) { //if (icon) thisMarker.icon = icon; doesn't work
59 | thisMarker = L.marker([pin.lat, pin.lng], {
60 | icon,
61 | draggable: false,
62 | autopan: true,
63 | riseOnHover: true,
64 | title: pin.place_id,
65 | desc: pin.desc
66 | });
67 |
68 | } else {
69 | thisMarker = L.marker([pin.lat, pin.lng], {
70 | draggable: false,
71 | autopan: true,
72 | riseOnHover: true,
73 | title: pin.place_id,
74 | desc: pin.desc
75 | });
76 | }
77 |
78 | const container = L.DomUtil.create('div');
79 |
80 | thisMarker.data = pin; //save the db data to the marker
81 | pinMarkers.push(thisMarker);
82 | }
83 |
84 | // console.log('User Pins added: ',pinMarkers)
85 | return pinMarkers;
86 |
87 | }
88 |
89 | //This funciton drops pins on a map replacing pins if they already exist
90 | const dropPin = (pinMarkers=[], mapObj=undefined) => {
91 |
92 | //if no map is defined, or there are no pins return
93 | if ((mapObj == null) || (pinMarkers.length == 0)) return;
94 |
95 | /*TO DO?:
96 | Do not allow duplication of pins,
97 | Remove those that exist already on the old map
98 | Hint: use sets
99 | */
100 |
101 |
102 | for (let marker of pinMarkers){
103 | marker.addTo(mapObj);
104 | setPinPopup(marker);
105 | }
106 | }
107 |
108 | /* TO DO
109 | Create a custom container for each pin on click */
110 | const setPinPopup = (marker) => {
111 | console.log(marker);
112 | const container = L.DomUtil.create('div');
113 | console.log(marker);
114 | const markerDescription = createLabel(marker.options.desc, 'center', container);
115 | const deleteBtn = createButton('Delete', 'center',container, marker.data._id);
116 | marker.bindPopup(container);
117 |
118 |
119 | L.DomEvent.on(deleteBtn, 'click', function() {
120 | makeRequest('DELETE', `/search/savedPins/${event.target.id}`, '', {
121 | //place_id: event.target.id //should be from state not dom
122 | }).then((response) => {
123 |
124 | //remove icon if removal from database was success
125 | marker.remove();
126 |
127 | }).catch((err) => {
128 |
129 | switch (err.response.status){
130 | case 400: //duplicate pin
131 | const origPin = err.response.data;
132 | alert('We had an issue deleting this pin.');
133 | /*TO DO:
134 | Convert popup alert to toast message
135 | */
136 | break;
137 | default:
138 | console.log(err); //error saving pin
139 |
140 | }
141 |
142 | })
143 | });
144 | }
145 |
146 | //Export Statements
147 | export {fetchAndDropUserPins, makePinMarkers, dropPin};
148 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 | See the Guide on how to contribute [here](https://github.com/TheDevPath/Navi/blob/development/CONTRIBUTING.md#how-to-contribute) for instructions on how to fork and set up your repository.
3 |
4 | # Installing Dependencies
5 | In the root directory of your newly cloned project `npm install`
6 |
7 | In the client directory of your project `npm install`
8 |
9 | Skip this next part if you know what you are doing
10 |
11 | ---
12 |
13 | Noob tip
14 |
15 | *If you can, "clone with `SSH` instead of clone with `HTTPS`. This means that, when you type in git remote add origin, you should use a link that looks like this: `git@github.com:*YOUR_USER_NAME/YOUR_REPO_NAME.git.*` Observe how that differs from* `https://github.com/YOUR_USER_NAME/YOUR_REPO_NAME.git`*
16 | While the first creates a remote that uses `ssh` authentication, the latter uses `https`, so it'll always prompt you to enter your username and password to authenticate the connection. For more see this [link](https://gist.github.com/juemura/899241d73cf719de7f540fc68071bd7d)*
17 |
18 | ---
19 |
20 | # Get Google Maps API key
21 |
22 | - In the config subdirectory you will find secrets-*example.json. *Copy it's contents to a new file called secrets.json in the same directory*.
23 |
24 | - Next get a [Google Maps API key](https://developers.google.com/maps/documentation/javascript/get-api-key)
25 |
26 | - Click on the button
27 |
28 | - This will take you through the process
29 |
30 | - Note: If you have an existing API key, you may use that key.
31 | [Detailed instructions](https://developers.google.com/maps/documentation/javascript/get-api-key)
32 |
33 | - Open `secrets.json` and under googlemaps, paste your API key and save
34 |
35 | # Install mongodb
36 |
37 | You also need to install and have running mongoDB - Directions can be found [here](https://docs.mongodb.com/manual/installation/)
38 |
39 | # Update and run
40 |
41 | When update has completed go to where you installed the project and run `npm install` again to install dependencies in the root and client directories. This will update the project with any new packages added to the file package.json in your project.
42 |
43 | When finished, in the project's root directory type `npm run dev`. This will start the dev servers on `localhost:8080` & `localhost:8081` respectively
44 |
45 |
46 | # About Navi
47 |
48 | This is an open source project for Grow with Google Udacity Scholarship Challenge - Navigation app using offline first strategy and Open Street Maps and google api
49 |
50 | The idea for this project is to build a progressive web app utilizing the technologies learned in the Grow with Google Udacity Scholarship challenge.
51 |
52 | The project idea - build a navigation app that will store a local copy of pre selected directions and maps so that navigation continues to work properly in poor to no signal scenarios.
53 |
54 | The stack - this will be a node app utilizing Preact for the front end.
55 |
56 | Pull requests are welcome!
57 |
58 | ## Table of Contents
59 |
60 | - [Main Goal](#main-goal)
61 | - [Features](#features)
62 | - [About the application](#about-the-application)
63 | - [Where to get the files](#where-to-get-the-files)
64 | - [Key files included](#key-files-included)
65 | - [Requirements](#requirements)
66 | - [ToDo](#todo)
67 |
68 |
69 | ## Main Goal
70 |
71 | The main goal of the app is to provide the user with a map interface that they can use on their mobile device and that will continue to be useful in poor to no signal environments.
72 |
73 | ## Features
74 |
75 | * The interface will **display a map** of a designated area
76 |
77 |
78 | * **Users** will be able to:
79 | * Search for a location
80 | * By typing into a search field
81 | * Drop a pin at a location
82 | * By placing a pin on the map
83 | * By clicking 'drop pin' next to search results
84 | * Get directions to a selected location from:
85 | * Their current location
86 | * Another dropped pin
87 | * Save a dropped pin
88 | * Save a set of directions
89 | * View list of dropped pins ('saved places')
90 | * View list of directions ('saved directions')
91 |
92 |
93 | * **The app** will:
94 | * Use the Google Maps API to:
95 | * Display the map
96 | * Provide a search interface
97 | * Provide a current location
98 | * Provide directions
99 | * Interface with a database to save a users:
100 | * Saved places
101 | * Saved directions
102 | * Maintain last state on loss of signal including:
103 | * Current map View
104 | * Saved places
105 | * Saved directions
106 |
107 |
108 | ### *About the application*
109 | * Node backend
110 | * Preact Frontend
111 | * Open Street Maps
112 | * Google api
113 | * Service Workers
114 | * [MIT License](../blob/master/LICENSE)
115 |
116 | ### *Where to get the files*
117 | * [This repository](https://github.com/TheDevPath/Navi)
118 |
119 | ### *Key files included*
120 | * Files
121 |
122 | ## *Requirements*
123 | * Requirements
124 |
125 | ## *ToDo*
126 | * Improvements
127 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 | See the Guide on how to contribute [here](https://github.com/TheDevPath/Navi/blob/development/CONTRIBUTING.md#how-to-contribute) for instructions on how to fork and set up your repository.
3 |
4 | # Installing Dependencies
5 | In the root directory of your newly cloned project `npm install`
6 |
7 | In the client directory of your project `npm install`
8 |
9 | Skip this next part if you know what you are doing
10 |
11 | ---
12 |
13 | Noob tip
14 |
15 | *If you can, "clone with `SSH` instead of clone with `HTTPS`. This means that, when you type in git remote add origin, you should use a link that looks like this: `git@github.com:*YOUR_USER_NAME/YOUR_REPO_NAME.git.*` Observe how that differs from* `https://github.com/YOUR_USER_NAME/YOUR_REPO_NAME.git`*
16 | While the first creates a remote that uses `ssh` authentication, the latter uses `https`, so it'll always prompt you to enter your username and password to authenticate the connection. For more see this [link](https://gist.github.com/juemura/899241d73cf719de7f540fc68071bd7d)*
17 |
18 | ---
19 |
20 | # Get Google Maps API key
21 |
22 | - In the config subdirectory you will find secrets-*example.json. *Copy it's contents to a new file called secrets.json in the same directory*.
23 |
24 | - Next get a [Google Maps API key](https://developers.google.com/maps/documentation/javascript/get-api-key)
25 |
26 | - Click on the button
27 |
28 | - This will take you through the process
29 |
30 | - Note: If you have an existing API key, you may use that key.
31 | [Detailed instructions](https://developers.google.com/maps/documentation/javascript/get-api-key)
32 |
33 | - Open `secrets.json` and under googlemaps, paste your API key and save
34 |
35 | # Install mongodb
36 |
37 | You also need to install and have running mongoDB - Directions can be found [here](https://docs.mongodb.com/manual/installation/)
38 |
39 | # Update and run
40 |
41 | When update has completed go to where you installed the project and run `npm install` again to install dependencies in the root and client directories. This will update the project with any new packages added to the file package.json in your project.
42 |
43 | When finished, in the project's root directory type `npm run dev`. This will start the dev servers on `localhost:8080` & `localhost:8081` respectively
44 |
45 |
46 | # About Navi
47 |
48 | This is an open source project for Grow with Google Udacity Scholarship Challenge - Navigation app using offline first strategy and Open Street Maps and google api
49 |
50 | The idea for this project is to build a progressive web app utilizing the technologies learned in the Grow with Google Udacity Scholarship challenge.
51 |
52 | The project idea - build a navigation app that will store a local copy of pre selected directions and maps so that navigation continues to work properly in poor to no signal scenarios.
53 |
54 | The stack - this will be a node app utilizing Preact for the front end.
55 |
56 | Pull requests are welcome!
57 |
58 | ## Table of Contents
59 |
60 | - [Main Goal](#main-goal)
61 | - [Features](#features)
62 | - [About the application](#about-the-application)
63 | - [Where to get the files](#where-to-get-the-files)
64 | - [Key files included](#key-files-included)
65 | - [Requirements](#requirements)
66 | - [ToDo](#todo)
67 |
68 |
69 | ## Main Goal
70 |
71 | The main goal of the app is to provide the user with a map interface that they can use on their mobile device and that will continue to be useful in poor to no signal environments.
72 |
73 | ## Features
74 |
75 | * The interface will **display a map** of a designated area
76 |
77 |
78 | * **Users** will be able to:
79 | * Search for a location
80 | * By typing into a search field
81 | * Drop a pin at a location
82 | * By placing a pin on the map
83 | * By clicking 'drop pin' next to search results
84 | * Get directions to a selected location from:
85 | * Their current location
86 | * Another dropped pin
87 | * Save a dropped pin
88 | * Save a set of directions
89 | * View list of dropped pins ('saved places')
90 | * View list of directions ('saved directions')
91 |
92 |
93 | * **The app** will:
94 | * Use the Google Maps API to:
95 | * Display the map
96 | * Provide a search interface
97 | * Provide a current location
98 | * Provide directions
99 | * Interface with a database to save a users:
100 | * Saved places
101 | * Saved directions
102 | * Maintain last state on loss of signal including:
103 | * Current map View
104 | * Saved places
105 | * Saved directions
106 |
107 |
108 | ### *About the application*
109 | * Node backend
110 | * Preact Frontend
111 | * Open Street Maps
112 | * Google api
113 | * Service Workers
114 | * [MIT License](../blob/master/LICENSE)
115 |
116 | ### *Where to get the files*
117 | * [This repository](https://github.com/TheDevPath/Navi)
118 |
119 | ### *Key files included*
120 | * Files
121 |
122 | ## *Requirements*
123 | * Requirements
124 |
125 | ## *ToDo*
126 | * Improvements
127 |
--------------------------------------------------------------------------------
/client/src/js/validate-account-form.js:
--------------------------------------------------------------------------------
1 | import {route} from 'preact-router';
2 | import { makeRequest, token, BASE_ENDPOINTS } from './server-requests-utils';
3 |
4 | const ALERT_MESSAGES = {
5 | passwordFormat: "Password should have at least 6 characters, should have at least one letter, number, and special character",
6 | mismatchedPasswords: "Try again, passwords don't match!",
7 | }
8 |
9 |
10 | const validEmail = (email) => {
11 | const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
12 | return re.test(String(email).toLowerCase());
13 | };
14 |
15 | const validPassword = (password) => {
16 | const re = /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,}$/;
17 | return re.test(String(password));
18 | };
19 |
20 | const passwordsMatch = (password1, password2) => {
21 | return password1 === password2;
22 | };
23 |
24 | const validateReset = (formData) => {
25 | const {name, email, password, new_password, confirm_password} = formData;
26 |
27 | // passwords form fields are required so if any is null then user info update
28 | if (password && new_password && confirm_password) {
29 | // validate passwords
30 | if (!validPassword(password) || !validPassword(new_password) ||
31 | !validPassword(confirm_password)) {
32 | return {
33 | status: false,
34 | message: ALERT_MESSAGES.passwordFormat,
35 | body: null,
36 | };
37 | } else if (!passwordsMatch(new_password, confirm_password)) {
38 | return {
39 | status: false,
40 | message: ALERT_MESSAGES.mismatchedPasswords,
41 | body: null,
42 | };
43 | } else {
44 | return {
45 | status: true,
46 | message: 'Success - valid request!',
47 | body: {password, new_password, confirm_password},
48 | };
49 | }
50 | }
51 |
52 | // validate user info has at least one field completed
53 | if (!name && !email) {
54 | return {
55 | status: false,
56 | message: 'Please enter a new name and/or email address!',
57 | body: null,
58 | };
59 | }
60 |
61 | return {
62 | status: true,
63 | message: 'Success - valid request!',
64 | body: {name, email},
65 | };
66 | };
67 |
68 | const validateLogin = (formData) => {
69 | const {email, password} = formData;
70 |
71 | if (!validPassword(password)) {
72 | return {
73 | status: false,
74 | message: ALERT_MESSAGES.passwordFormat,
75 | body: null,
76 | };
77 | }
78 |
79 | return {
80 | status: true,
81 | message: 'Success - valid request!',
82 | body: {email, password},
83 | };
84 | };
85 |
86 | const validateRegister = (formData) => {
87 | const {name, email, password, confirm_password} = formData;
88 |
89 | if (!validPassword(password) || !validPassword(confirm_password)) {
90 | return {
91 | status: false,
92 | message: ALERT_MESSAGES.passwordFormat,
93 | body: null,
94 | };
95 | } else if (!passwordsMatch(password, confirm_password)) {
96 | return {
97 | status: false,
98 | message: ALERT_MESSAGES.mismatchedPasswords,
99 | body: null,
100 | };
101 | } else {
102 | return {
103 | status: true,
104 | message: 'Success - valid request!',
105 | body: {name, email, password, confirm_password},
106 | };
107 | }
108 | };
109 |
110 | // Exports below
111 | export const validateAccountForm = (args) => {
112 |
113 | let {path, formData} = args;
114 |
115 | let result = null;
116 |
117 | if (path === BASE_ENDPOINTS.userReset) {
118 | result = validateReset(formData);
119 |
120 | if (result.body.email || result.body.name) {
121 | path = BASE_ENDPOINTS.userUpdate;
122 | }
123 | }
124 |
125 | if (path === BASE_ENDPOINTS.userRegister) {
126 | result = validateRegister(formData);
127 | }
128 |
129 | if (path == BASE_ENDPOINTS.userLogin) {
130 | result = validateLogin(formData);
131 | }
132 |
133 | const {status, message, body} = result;
134 |
135 | // return if validation fails
136 | if (!status) {
137 | return new Promise((resolve, reject) => {
138 | resolve({status, message});
139 | });
140 | }
141 |
142 | // otherwise process server request
143 | return new Promise((resolve, reject) => {
144 | makeRequest('POST', path, '', body)
145 | .then(function (response) {
146 | // TODO - need to better handle token assignment and update
147 | // because depending on the type of response this is accidentally
148 | // removing the token. This temp change should prevent that
149 | // from happening for now.
150 | if (response.data.token) {
151 | token.setCookie(response.data.token);
152 | }
153 | resolve({status, message});
154 | })
155 | .catch(function (error) {
156 | reject(error);
157 | });
158 | });
159 | };
160 |
161 | export const clearForms = () => {
162 | for (let form of document.getElementsByTagName("form")) {
163 | form.reset();
164 | }
165 | };
166 |
167 | export const logout = () => {
168 | token.deleteCookie();
169 | };
170 |
171 | export const setStateUserOrRedirectToSignIn = (component) => {
172 | return makeRequest('GET','user')
173 | .then((response) => {
174 | component.setState({
175 | user: response.data,
176 | isSignedIn: true,
177 | });
178 | }
179 | ).catch(() => {
180 | route('/signin', true);
181 | });
182 | }
183 |
--------------------------------------------------------------------------------
/tests/saved-pins-test.js:
--------------------------------------------------------------------------------
1 | /* eslint no-underscore-dangle: 0 */
2 | // api routes tests
3 | const request = require('supertest');
4 | const { ObjectID } = require('mongodb');
5 |
6 | const app = require('../app');
7 | const SavedPins = require('../models/saved-pins');
8 | const {
9 | pins, populatePins, users, populateUsers
10 | } = require('./seed/seed');
11 |
12 | beforeEach(populateUsers);
13 | beforeEach(populatePins);
14 |
15 | describe('POST /search/savedpins', () => {
16 | it('should create new saved pin', (done) => {
17 | const new_pin = {
18 | lat: 10,
19 | lng: 20,
20 | place_id: 'testing pin1',
21 | user: users[0]._id
22 | };
23 | request(app)
24 | .post('/search/savedpins')
25 | .set('x-access-token', users[0].tokens[0].token)
26 | .send(new_pin)
27 | .expect(200)
28 | .expect((res) => {
29 | expect(res.body.pin.lat).to.equal(new_pin.lat);
30 | expect(res.body.pin.lng).to.equal(new_pin.lng);
31 | })
32 | .end((err, res) => {
33 | if (err) {
34 | return done(err);
35 | }
36 |
37 | SavedPins.findOne({lat: 10, lng: 20}).then(receivedPin => {
38 | expect(receivedPin).to.deep.include(new_pin);
39 | done();
40 | }).catch(e => done(e));
41 | });
42 | });
43 |
44 | it('should not save a duplicate pin', done => {
45 | request(app)
46 | .post('/search/savedpins')
47 | .set('x-access-token', users[0].tokens[0].token)
48 | .send(pins[0])
49 | .expect(400)
50 | .end((err, res) => {
51 | if (err) return done(err);
52 | expect(res.body.message).to.equal("Duplicate found. Pin not saved.");
53 | done();
54 | });
55 | });
56 |
57 | it('should not create savedPin with invalid body data', (done) => {
58 | request(app)
59 | .post('/search/savedpins')
60 | .set('x-access-token', users[0].tokens[0].token)
61 | .send({})
62 | .expect(500)
63 | .end((err, res) => {
64 | if (err) {
65 | return done(err);
66 | }
67 |
68 | SavedPins.find().then((savedPins) => {
69 | expect(savedPins.length).to.equal(pins.length);
70 | done();
71 | }).catch(e => done(e));
72 | });
73 | });
74 | });
75 |
76 | describe('GET /search/savedpins', () => {
77 | it('should return savedpins doc', (done) => {
78 | request(app)
79 | .get('/search/savedpins/')
80 | .set('x-access-token', users[0].tokens[0].token)
81 | .expect(200)
82 | .expect((res) => {
83 | expect(res.body.savedPins[0].lat).to.equal(pins[0].lat);
84 | expect(res.body.savedPins[0].lng).to.equal(pins[0].lng);
85 | expect(res.body.savedPins.length).to.equal(1);
86 | })
87 | .end(done);
88 | });
89 | });
90 |
91 | describe('GET /search/savedpins/:id', () => {
92 | it('should return savedpins doc', (done) => {
93 | request(app)
94 | .get(`/search/savedpins/${pins[0]._id.toHexString()}`)
95 | .set('x-access-token', users[0].tokens[0].token)
96 | .expect(200)
97 | .expect((res) => {
98 | expect(res.body.pin.lat).to.equal(pins[0].lat);
99 | expect(res.body.pin.lng).to.equal(pins[0].lng);
100 | })
101 | .end(done);
102 | });
103 |
104 | it('should return 404 if savedpins not found', (done) => {
105 | const hexId = new ObjectID().toHexString();
106 |
107 | request(app)
108 | .get(`/search/savedpins/${hexId}`)
109 | .set('x-access-token', users[0].tokens[0].token)
110 | .expect(404)
111 | .end(done);
112 | });
113 |
114 | it('should return 404 for non-object ids', (done) => {
115 | request(app)
116 | .get('/search/savedpins/123abc')
117 | .set('x-access-token', users[0].tokens[0].token)
118 | .expect(404)
119 | .end(done);
120 | });
121 | });
122 |
123 | describe('DELETE /search/savedpins', () => {
124 | it('should remove all searchpins', (done) => {
125 | request(app)
126 | .delete('/search/savedpins')
127 | .set('x-access-token', users[0].tokens[0].token)
128 | .expect(200)
129 | .end((err, res) => {
130 | if (err) {
131 | return done(err);
132 | }
133 |
134 | SavedPins.find().then((allPins) => {
135 | expect(allPins.length).to.equal(pins.length - 1);
136 | done();
137 | });
138 | });
139 | });
140 | });
141 |
142 | describe('DELETE /search/savedpins/:id', () => {
143 | it('should remove a single searchpins', (done) => {
144 | const hexId = pins[0]._id.toHexString();
145 |
146 | request(app)
147 | .delete(`/search/savedpins/${hexId}`)
148 | .set('x-access-token', users[0].tokens[0].token)
149 | .expect(200)
150 | .expect((res) => {
151 | expect(res.body.pin._id).to.equal(hexId);
152 | })
153 | .end((err, res) => {
154 | if (err) {
155 | return done(err);
156 | }
157 |
158 | SavedPins.findById(hexId).then((resPin) => {
159 | expect(resPin).to.equal(null);
160 | done();
161 | }).catch(e => done(e));
162 | });
163 | });
164 |
165 | it('should return 404 if pin not found', (done) => {
166 | const hexId = new ObjectID().toHexString();
167 |
168 | request(app)
169 | .delete(`/search/savedpins/${hexId}`)
170 | .set('x-access-token', users[0].tokens[0].token)
171 | .expect(404)
172 | .end(done);
173 | });
174 |
175 | it('should return 404 if object id is invalid', (done) => {
176 | request(app)
177 | .delete('/search/savedpins/123abc')
178 | .set('x-access-token', users[0].tokens[0].token)
179 | .expect(404)
180 | .end(done);
181 | });
182 | });
183 |
184 |
--------------------------------------------------------------------------------
/client/tests/AccountForm.test.js:
--------------------------------------------------------------------------------
1 | import {h} from 'preact';
2 | import {shallow} from 'preact-render-spy';
3 | import AccountForm from '../src/components/AccountForm';
4 | import {REGISTER_PATH, LOGIN_PATH, RESET_PATH} from '../config';
5 | jest.mock('preact-router');
6 | const myRouter = require('preact-router');
7 | import * as validate_constants from '../src/js/validate-account-form';
8 | import {BASE_ENDPOINTS} from '../src/js/server-requests-utils';
9 |
10 | const VALIDATION_MESSAGE = "VALIDATION MESSAGE";
11 |
12 | const setup = ({testPropsOverrides, wrapperStateOverrides, wrapperPropsOverrides}) => {
13 |
14 | /**
15 | * If validateAccountForm resolves, it does not necessarily mean the form is validated.
16 | * validateAccountForm will resolve with status: false if the form is not validated.
17 | * validateAccountForm only rejects if there is an error from the server after submitting.
18 | * To test error messages from the server, the test should explicitly set resolve_validation to false.
19 | */
20 | const testProps = Object.assign({
21 | resolve_validation: true
22 | }, testPropsOverrides);
23 |
24 | // Mock the alert function
25 | window.alert = jest.fn().mockImplementation( message => testProps.alert_message = message);
26 |
27 | // Mock the route function
28 | myRouter.route = jest.fn().mockImplementation((url, replace) => {
29 | testProps.route_url = url;
30 | testProps.route_replace = replace;
31 | });
32 |
33 | // Mock the validateAccountForm function
34 | validate_constants.validateAccountForm = jest.fn().mockImplementation( () => {
35 | return new Promise((resolve, reject) => {
36 | if (testProps.resolve_validation) {
37 | resolve({status: testProps.validation_status, message: testProps.validation_message});
38 | } else {
39 | reject({response: {data: testProps.validation_message}})
40 | }
41 | });
42 | });
43 |
44 | return testProps;
45 | }
46 |
47 | describe('
', () => {
48 |
49 | const wrapper = shallow();
50 |
51 | describe('page loads', () => {
52 |
53 | it('has 4 inputs', async () => {
54 | const testProps = setup({
55 | testPropsOverrides: {
56 | wrapper,
57 | }
58 | });
59 |
60 | const inputs = testProps.wrapper.find('input');
61 | expect(inputs.length).toBe(4);
62 | });
63 | });
64 |
65 | describe('validateAccountForm returns true status and message', () => {
66 |
67 | it('routes to /profile', async () => {
68 | const testProps = setup({
69 | testPropsOverrides: {
70 | validation_status: true,
71 | wrapper,
72 | }
73 | });
74 |
75 | await Promise.all([testProps.wrapper.find('form').at(0).simulate('submit',{ preventDefault () {} })]);
76 | expect(testProps.route_url).toBe('/profile');
77 | expect(testProps.route_replace).toBe(true);
78 | });
79 |
80 | });
81 |
82 | describe('validateAccountForm returns false status and message', () => {
83 |
84 | it('warns message', async () => {
85 | const testProps = setup({
86 | testPropsOverrides: {
87 | validation_status: false,
88 | validation_message: VALIDATION_MESSAGE,
89 | wrapper,
90 | }
91 | });
92 |
93 | await Promise.all([testProps.wrapper.find('form').at(0).simulate('submit',{ preventDefault () {} })]);
94 | expect(testProps.alert_message).toBe(VALIDATION_MESSAGE);
95 | });
96 |
97 | });
98 |
99 | describe('validateAccountForm rejects instead of resolves', () => {
100 |
101 | it('warns message', async () => {
102 | const testProps = setup({
103 | testPropsOverrides: {
104 | resolve_validation: false,
105 | validation_message: VALIDATION_MESSAGE,
106 | wrapper,
107 | }
108 | });
109 |
110 | await Promise.all([testProps.wrapper.find('form').at(0).simulate('submit',{ preventDefault () {} })]);
111 | expect(testProps.alert_message).toBe(VALIDATION_MESSAGE);
112 | });
113 |
114 | });
115 |
116 | });
117 |
118 |
119 | describe('', () => {
120 |
121 | const wrapper = shallow();
122 |
123 | describe('page loads', () => {
124 |
125 | it('has 2 inputs', async () => {
126 | const testProps = setup({
127 | testPropsOverrides: {
128 | wrapper,
129 | }
130 | });
131 |
132 | const inputs = testProps.wrapper.find('input');
133 | expect(inputs.length).toBe(2);
134 | });
135 | });
136 |
137 | });
138 |
139 | describe('', () => {
140 |
141 | const wrapper = shallow();
142 |
143 | describe('page loads', () => {
144 |
145 | it('has 5 inputs', async () => {
146 | const testProps = setup({
147 | testPropsOverrides: {
148 | wrapper,
149 | }
150 | });
151 |
152 | const inputs = testProps.wrapper.find('input');
153 | expect(inputs.length).toBe(5);
154 | });
155 | });
156 |
157 | describe('validateAccountForm returns true status and message', () => {
158 |
159 | it('warns message', async () => {
160 | const testProps = setup({
161 | testPropsOverrides: {
162 | validation_status: true,
163 | validation_message: VALIDATION_MESSAGE,
164 | wrapper,
165 | },
166 | });
167 |
168 | await Promise.all([testProps.wrapper.find('form').at(0).simulate('submit',{ preventDefault () {} })]);
169 | expect(testProps.alert_message).toBe(VALIDATION_MESSAGE);
170 | });
171 |
172 | });
173 |
174 | });
175 |
--------------------------------------------------------------------------------
/client/src/components/AccountForm/index.js:
--------------------------------------------------------------------------------
1 | import { h, Component, render } from 'preact';
2 | import style from './style.css';
3 | import { validateAccountForm, clearForms } from "../../js/validate-account-form";
4 | import { BASE_ENDPOINTS } from "../../js/server-requests-utils";
5 | import linkState from "linkstate";
6 | import { route } from 'preact-router';
7 |
8 | export default class AccountForm extends Component {
9 | constructor() {
10 | super();
11 |
12 | this.routeToRegister = this.routeToRegister.bind(this);
13 | this.handleSubmit = this.handleSubmit.bind(this);
14 | }
15 |
16 | handleSubmit = (event) => {
17 | event.preventDefault();
18 |
19 | const formData = {
20 | name: this.state.name,
21 | email: this.state.email,
22 | password: this.state.password,
23 | new_password: this.state.new_password,
24 | confirm_password: this.state.confirm_password,
25 | }
26 |
27 | const args = {
28 | path: this.props.path,
29 | formData,
30 | }
31 |
32 | validateAccountForm(args).then((response) => {
33 | console.log('validateAccountForm(): ', response);
34 | if (response.status) {
35 | if (this.props.path === BASE_ENDPOINTS.userReset) {
36 | alert(response.message);
37 | } else { // redirect to /profile with success for login/registration
38 | route(`/profile`, true);
39 | }
40 | } else {
41 | // TODO - alert failure to process
42 | alert(response.message);
43 | }
44 | }).catch(function (error) {
45 | // TODO - standardize API error output so we can handle cleanly
46 | // on frontend
47 | if (error.response) {
48 | alert(error.response.data);
49 | } else {
50 | alert(error);
51 | }
52 | });
53 | };
54 |
55 | componentWillUnmount = () => {
56 | clearForms();
57 | }
58 |
59 | routeToRegister() {
60 | route("/register", true);
61 | }
62 |
63 | render({ path },{ name, email, password, new_password, confirm_password }) {
64 |
65 | //DEFAULT TO LOGIN PATH
66 | let display =
67 | ;
86 |
87 | if(path === BASE_ENDPOINTS.userRegister){
88 | display =
89 |
90 |
101 |
;
102 | }
103 |
104 | if(path === BASE_ENDPOINTS.userReset){
105 | display =
106 | ;
126 | }
127 | return (
128 |
129 |
130 | {display}
131 |
132 | );
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributions Welcome
2 |
3 | Thanks for your interest in contributing to **Navi**! Contributing to open source projects like this one can be a rewarding way to learn, teach, and build experience. Not only that, contributing is a great way to get involved with _social coding_. We are excited to see what amazing contributions you will make, as well as how your contributions will benefit others.
4 |
5 | If you are new to contributing to open source projects, the process can be intimidating. Not to worry! To help ensure both you and the community get the most out of your contributions, we've put together the following guidelines.
6 |
7 | ## Table of Contents
8 |
9 | 1. [Types of Contributions](#types-of-contributions)
10 | 1. [Ground Rules & Expectations](#ground-rules--expectations)
11 | 1. [How to Contribute](#how-to-contribute)
12 |
13 | ---
14 |
15 | ## Types of Contributions
16 |
17 | The common misconception about contributing to an open source project is that you need to contribute code. In fact, there are numerous ways you can directly contribute. To give you some ideas of how you can contribute, here are some examples of the types of contributions we are looking for:
18 |
19 | ### Developers can:
20 |
21 | * Take a look at the [open issues][issues] and find one you can tackle.
22 |
23 | * Locate and fix bugs.
24 |
25 | * Implement innovative and awesome new features.
26 |
27 | * Help to improve tooling and testing.
28 |
29 | ### Organizers and Planners can:
30 |
31 | * Link to duplicate issues, and suggest new issue labels, to help keep things organized.
32 |
33 | * Go through the [open issues][issues] and suggest closing old ones.
34 |
35 | * Ask clarifying questions on recently opened issues to move the discussion forward.
36 |
37 | * Help to organize meetups about the project.
38 |
39 | ### Writers can:
40 |
41 | * Help to fix or improve the project's documentation.
42 |
43 | * Contribute to the project's [Wiki][wiki].
44 |
45 | ### Designers can:
46 |
47 | * Design wire frames, mock-ups, graphical assets, and logos.
48 |
49 | * Put together a style guide to help the project have a consistent visual design.
50 |
51 | ### Supporters can:
52 |
53 | * Answer questions for people on open issues, or about the project in general.
54 |
55 | * Help to moderate discussion boards or conversation channels.
56 |
57 | ## Ground Rules & Expectations
58 |
59 | Since the project is constantly being updated with contributions of all sorts, it is important to establish ground rules and as well as expectations. This helps to ensure the best possible experience for users of the Offline Google Maps Navigator application, as well as encourage a positive, helpful, and lively community of active contributors just like you!
60 |
61 | Please make sure you read our [code of conduct][code-of-conduct] prior to contributing.
62 |
63 | ## How to Contribute
64 |
65 | If you'd like to contribute, a good place to start is by searching through the [issues][issues] and [pull requests][pull-requests] to see if someone else had a similar idea or question.
66 |
67 | If you don't see your idea listed, and you think it fits into the goals of the project, you should:
68 |
69 | * **Minor Contribution _(e.g., typo fix)_:** Open a pull request
70 | * **Major Contribution _(e.g., new feature)_:** Start by opening an issue first. That way, other people can weigh in on the discussion and planning before you do any work.
71 |
72 | To start making a contribution:
73 |
74 | 1. `fork` the project repository by clicking the **fork** button on GitHub.
75 |
76 | 1. `clone` your forked repository (_noob tip: the actual command you type in is everything after the $_):
77 |
78 | ```shell
79 | $ git clone https://github.com//Navi
80 | ```
81 |
82 | 1. Add a new remote that points to the original project so you can sync project changes with your local copy:
83 |
84 | ```shell
85 | $ git remote add upstream https://github.com/TheDevPath/Navi
86 | ```
87 |
88 | 1. Pull upstream changes into your local repositories `development` branch:
89 |
90 | ```shell
91 | $ git checkout development
92 | $ git pull upstream development && git push origin development
93 | ```
94 |
95 | 1. Create a new branch from the `development` branch:
96 | 
97 |
98 | **IMPORTANT:** Make sure you are on the `development` branch first.
99 |
100 | ```shell
101 | $ git checkout -b
102 | ```
103 |
104 | 1. Make your contribution to the project code.
105 |
106 | 1. Write or adapt tests as needed.
107 |
108 | 1. Add or change documentation as needed.
109 |
110 | 1. After commiting changes, push your branch to your fork on Github, the remote `origin`:
111 |
112 | **IMPORTANT:** Your commit message should be in present tense and should describe what the commit, when applied, does to the code - not what you did to the code.
113 |
114 | ```shell
115 | $ git push -u origin
116 | ```
117 |
118 | 1. From your forked GitHub repository, open a pull request in the branch containing your contributions. Target the project's `development` branch for the pull request.
119 |
120 | 1. At this point, your contribution has been submitted for review. Please be patient while your contribution is being reviewed as this can take some time. Meanwhile, if there are questions or comments on your contribution, please respond and/or update with future commits.
121 |
122 | 1. Once the pull request is approved and merged, you can pull the changes from `upstream` to your local repository and delete your extra branch(es).
123 |
124 | 1. Don't forget to check out more [about] this project
125 |
126 | Happy contributing!
127 |
128 | [issues]: https://github.com/TheDevPath/Navi/issues
129 | [pull-requests]: https://github.com/TheDevPath/Navi/pulls
130 | [wiki]: https://thedevpath.github.io/Navi/
131 | [code-of-conduct]: ./CODE_OF_CONDUCT.md
132 | [about]: https://github.com/TheDevPath/Navi/blob/static-docs/README.md
133 |
--------------------------------------------------------------------------------
/docs/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributions Welcome
2 |
3 | Thanks for your interest in contributing to **Navi**! Contributing to open source projects like this one can be a rewarding way to learn, teach, and build experience. Not only that, contributing is a great way to get involved with _social coding_. We are excited to see what amazing contributions you will make, as well as how your contributions will benefit others.
4 |
5 | If you are new to contributing to open source projects, the process can be intimidating. Not to worry! To help ensure both you and the community get the most out of your contributions, we've put together the following guidelines.
6 |
7 | ## Table of Contents
8 |
9 | 1. [Types of Contributions](#types-of-contributions)
10 | 1. [Ground Rules & Expectations](#ground-rules--expectations)
11 | 1. [How to Contribute](#how-to-contribute)
12 |
13 | ---
14 |
15 | ## Types of Contributions
16 |
17 | The common misconception about contributing to an open source project is that you need to contribute code. In fact, there are numerous ways you can directly contribute. To give you some ideas of how you can contribute, here are some examples of the types of contributions we are looking for:
18 |
19 | ### Developers can:
20 |
21 | * Take a look at the [open issues][issues] and find one you can tackle.
22 |
23 | * Locate and fix bugs.
24 |
25 | * Implement innovative and awesome new features.
26 |
27 | * Help to improve tooling and testing.
28 |
29 | ### Organizers and Planners can:
30 |
31 | * Link to duplicate issues, and suggest new issue labels, to help keep things organized.
32 |
33 | * Go through the [open issues][issues] and suggest closing old ones.
34 |
35 | * Ask clarifying questions on recently opened issues to move the discussion forward.
36 |
37 | * Help to organize meetups about the project.
38 |
39 | ### Writers can:
40 |
41 | * Help to fix or improve the project's documentation.
42 |
43 | * Contribute to the project's [Wiki][wiki].
44 |
45 | ### Designers can:
46 |
47 | * Design wire frames, mock-ups, graphical assets, and logos.
48 |
49 | * Put together a style guide to help the project have a consistent visual design.
50 |
51 | ### Supporters can:
52 |
53 | * Answer questions for people on open issues, or about the project in general.
54 |
55 | * Help to moderate discussion boards or conversation channels.
56 |
57 | ## Ground Rules & Expectations
58 |
59 | Since the project is constantly being updated with contributions of all sorts, it is important to establish ground rules and as well as expectations. This helps to ensure the best possible experience for users of the Offline Google Maps Navigator application, as well as encourage a positive, helpful, and lively community of active contributors just like you!
60 |
61 | Please make sure you read our [code of conduct][code-of-conduct] prior to contributing.
62 |
63 | ## How to Contribute
64 |
65 | If you'd like to contribute, a good place to start is by searching through the [issues][issues] and [pull requests][pull-requests] to see if someone else had a similar idea or question.
66 |
67 | If you don't see your idea listed, and you think it fits into the goals of the project, you should:
68 |
69 | * **Minor Contribution _(e.g., typo fix)_:** Open a pull request
70 | * **Major Contribution _(e.g., new feature)_:** Start by opening an issue first. That way, other people can weigh in on the discussion and planning before you do any work.
71 |
72 | To start making a contribution:
73 |
74 | 1. `fork` the project repository by clicking the **fork** button on GitHub.
75 |
76 | 1. `clone` your forked repository (_noob tip: the actual command you type in is everything after the $_):
77 |
78 | ```shell
79 | $ git clone https://github.com//Navi
80 | ```
81 |
82 | 1. Add a new remote that points to the original project so you can sync project changes with your local copy:
83 |
84 | ```shell
85 | $ git remote add upstream https://github.com/TheDevPath/Navi
86 | ```
87 |
88 | 1. Pull upstream changes into your local repositories `development` branch:
89 |
90 | ```shell
91 | $ git checkout development
92 | $ git pull upstream development && git push origin development
93 | ```
94 |
95 | 1. Create a new branch from the `development` branch:
96 | 
97 |
98 | **IMPORTANT:** Make sure you are on the `development` branch first.
99 |
100 | ```shell
101 | $ git checkout -b
102 | ```
103 |
104 | 1. Make your contribution to the project code.
105 |
106 | 1. Write or adapt tests as needed.
107 |
108 | 1. Add or change documentation as needed.
109 |
110 | 1. After commiting changes, push your branch to your fork on Github, the remote `origin`:
111 |
112 | **IMPORTANT:** Your commit message should be in present tense and should describe what the commit, when applied, does to the code - not what you did to the code.
113 |
114 | ```shell
115 | $ git push -u origin
116 | ```
117 |
118 | 1. From your forked GitHub repository, open a pull request in the branch containing your contributions. Target the project's `development` branch for the pull request.
119 |
120 | 1. At this point, your contribution has been submitted for review. Please be patient while your contribution is being reviewed as this can take some time. Meanwhile, if there are questions or comments on your contribution, please respond and/or update with future commits.
121 |
122 | 1. Once the pull request is approved and merged, you can pull the changes from `upstream` to your local repository and delete your extra branch(es).
123 |
124 | 1. Don't forget to check out more [about] this project
125 |
126 | Happy contributing!
127 |
128 | [issues]: https://github.com/TheDevPath/Navi/issues
129 | [pull-requests]: https://github.com/TheDevPath/Navi/pulls
130 | [wiki]: https://thedevpath.github.io/Navi/
131 | [code-of-conduct]: ./CODE_OF_CONDUCT.md
132 | [about]: https://github.com/TheDevPath/Navi/blob/static-docs/README.md
133 |
--------------------------------------------------------------------------------
/tests/users-test.js:
--------------------------------------------------------------------------------
1 | // api routes tests
2 | const request = require('supertest');
3 | const app = require('../app');
4 |
5 | const {
6 | users, populateUsers, deleteTestUser, stopServer,
7 | } = require('./seed/seed');
8 |
9 |
10 | beforeEach(populateUsers);
11 | afterEach(deleteTestUser);
12 | afterEach(stopServer);
13 |
14 | describe('/Users API Routes', () => {
15 | describe('GET /users/user', () => {
16 | it('does not send a user if not logged in', (done) => {
17 | request(app)
18 | .get('/users/user')
19 | .expect(401)
20 | .then(() => {
21 | done();
22 | })
23 | .catch((err) => {
24 | done(err);
25 | },
26 | );
27 | });
28 | it('Sends the requested user when they exist', (done) => {
29 | request(app)
30 | .get('/users/user')
31 | .set('x-access-token', users[0].tokens[0].token)
32 | .expect(200)
33 | .then((res) => {
34 | expect(res.body._id).to.equal(`${users[0]._id}`);
35 | done();
36 | })
37 | .catch((err) => {
38 | done(err);
39 | });
40 | });
41 | });
42 | // /register- POST
43 | // valid email, password, and doesn't exist already
44 |
45 | describe('POST users/register', () => {
46 | it('succesfully creates a new user', (done) => {
47 | request(app)
48 | .post('/users/register')
49 | .send({
50 | name: users[2].name,
51 | email: users[2].email,
52 | password: users[2].password,
53 | })
54 | .expect(200)
55 | .then((res) => {
56 | expect(res.body.auth).to.equal(true);
57 | done();
58 | })
59 | .catch((err) => {
60 | done(err);
61 | });
62 | });
63 |
64 | // valid email and password, but exists already
65 | it('Wont create a new user if email address already exists', (done) => {
66 | request(app)
67 | .post('/users/register')
68 | .send({
69 | name: users[0].name,
70 | email: users[0].email,
71 | password: users[0].password,
72 | })
73 | .expect(409)
74 | .then((res) => {
75 | expect(res.text).to.equal('Email already in use.');
76 | done();
77 | })
78 | .catch((err) => {
79 | done(err);
80 | });
81 | });
82 | // invalid email and try to create
83 | it('Wont create user with invalid email', (done) => {
84 | request(app)
85 | .post('/users/register')
86 | .send({
87 | name: users[2].name,
88 | email: 'badtestemail',
89 | password: 'passcode',
90 | })
91 | .expect(400)
92 | .then((res) => {
93 | expect(res.text).to.equal('Email is not of the valid format');
94 | done();
95 | })
96 | .catch((err) => {
97 | done(err);
98 | });
99 | });
100 | // invalid password and try to create
101 | it('Wont create user with invalid password', (done) => {
102 | request(app)
103 | .post('/users/register')
104 | .send({
105 | name: users[2].name,
106 | email: 'testing@test.com',
107 | password: '0',
108 | })
109 | .expect(400)
110 | .then((res) => {
111 | expect(res.text).to.equal('Password should have minimum length of 6 & it should have atleast one letter, one number, and one special character');
112 | done();
113 | })
114 | .catch((err) => {
115 | done(err);
116 | });
117 | });
118 | });
119 |
120 |
121 | // /login - POST
122 | // apiSuccess 200 { auth: true, token: token } jsonwebtoken.
123 | describe('POST /users/login', () => {
124 | it('Successfully logs the user in', (done) => {
125 | request(app)
126 | .post('/users/register')
127 | .send({
128 | name: users[2].name,
129 | email: users[2].email,
130 | password: users[2].password,
131 | }).then((res) => {
132 | request(app)
133 | .post('/users/login')
134 | .set('x-access-token', res.body.token)
135 | .send({
136 | name: users[2].name,
137 | email: users[2].email,
138 | password: users[2].password,
139 | })
140 | .expect(200)
141 | .then((finalRes) => {
142 | expect(finalRes.body.auth).to.equal(true);
143 | expect(finalRes.body.token).to.equal(res.body.token);
144 | done();
145 | })
146 | .catch((err) => {
147 | done(err);
148 | });
149 | });
150 | });
151 | // apiError 400 { request error } User not found.
152 | it('Should send 404 if user is not found', (done) => {
153 | request(app)
154 | .post('/users/login')
155 | .set('x-access-token', users[0].tokens[0].token)
156 | .send({
157 | email: 'gibberish@gmail.com',
158 | password: users[0].password,
159 | })
160 | .expect(404)
161 | .then((res) => {
162 | expect(res.text).to.equal('No user found.');
163 | done();
164 | })
165 | .catch((err) => {
166 | done(err);
167 | });
168 | });
169 | // apiError 401 { auth: false, token: null } Invalid password.
170 | it('Should send 401 if user has invalid password', (done) => {
171 | request(app)
172 | .post('/users/login')
173 | .set('x-access-token', users[0].tokens[0].token)
174 | .send({
175 | email: users[0].email,
176 | password: 'badpassword',
177 | })
178 | .expect(401)
179 | .then((res) => {
180 | expect(res.body.auth).to.equal(false);
181 | expect(res.body.token).to.equal(null);
182 | done();
183 | })
184 | .catch((err) => {
185 | done(err);
186 | });
187 | });
188 | });
189 |
190 | // /logout - GET
191 | describe('GET /users/logout', () => {
192 | // apiSuccess 200 { auth: false, token: null }
193 | it('Should send 200 if logout is successful', (done) => {
194 | request(app)
195 | .get('/users/logout')
196 | .set('x-access-token', users[0].tokens[0].token)
197 | .expect(200)
198 | .then((res) => {
199 | expect(res.body.auth).to.equal(false);
200 | expect(res.body.token).to.equal(null);
201 | done();
202 | })
203 | .catch((err) => {
204 | done(err);
205 | });
206 | });
207 | });
208 | });
209 |
--------------------------------------------------------------------------------