├── netlify.toml
├── server
├── .eslintignore
├── webpack.config.js
├── Procfile
├── .babelrc
├── .prettierrc.json
├── src
│ ├── api
│ │ ├── index.js
│ │ ├── resources
│ │ │ ├── email
│ │ │ │ ├── index.js
│ │ │ │ ├── email.restRouter.js
│ │ │ │ └── email.controller.js
│ │ │ ├── trip
│ │ │ │ ├── index.js
│ │ │ │ ├── trip.restRouter.js
│ │ │ │ └── trip.model.js
│ │ │ ├── user
│ │ │ │ ├── index.js
│ │ │ │ ├── user.restRouter.js
│ │ │ │ └── user.controller.js
│ │ │ ├── subscribe
│ │ │ │ ├── index.js
│ │ │ │ ├── subscribe.restRouter.js
│ │ │ │ └── subscribe.controller.js
│ │ │ └── waypoint
│ │ │ │ ├── index.js
│ │ │ │ ├── waypoint.restRouter.js
│ │ │ │ └── waypoint.model.js
│ │ ├── modules
│ │ │ ├── public.js
│ │ │ └── email.js
│ │ └── restRouter.js
│ ├── config
│ │ ├── prod.js
│ │ ├── testing.js
│ │ └── index.js
│ ├── server.js
│ ├── db.js
│ └── index.js
├── .gitignore
├── public
│ └── images
│ │ └── marker.png
├── tests
│ ├── modules
│ │ ├── testServer.js
│ │ ├── testPublic.js
│ │ ├── testSettings.js
│ │ ├── testSubscribe.js
│ │ └── testUser.js
│ ├── setup.js
│ └── mock.js
├── .eslintrc.json
├── README.md
├── webpack.prod.js
├── webpack.dev.js
└── package.json
├── .gitignore
├── client
├── public
│ ├── _redirects
│ ├── favicon.ico
│ ├── images
│ │ ├── AJ.png
│ │ ├── JC.png
│ │ ├── VM.png
│ │ ├── bg.jpg
│ │ ├── AB1.png
│ │ ├── UJ1.png
│ │ ├── Thuy1.png
│ │ ├── pear.jpeg
│ │ ├── bkwdslogo.png
│ │ ├── phoneimage.png
│ │ ├── features-list.png
│ │ ├── features-plan.png
│ │ ├── features-share.png
│ │ ├── features-track.png
│ │ ├── hikerscontent.png
│ │ ├── hikerscontent2.png
│ │ └── map_placeholder.gif
│ ├── fonts
│ │ ├── Wals-Light.otf
│ │ ├── Wals-Medium.otf
│ │ ├── Wals-Regular.otf
│ │ ├── Wals-Light-Oblique.otf
│ │ ├── Wals-Medium-Oblique.otf
│ │ └── Wals-Regular-Oblique.otf
│ ├── manifest.json
│ └── index.html
├── .prettierrc.json
├── src
│ ├── components
│ │ ├── Maps
│ │ │ ├── SingleTrip
│ │ │ │ ├── mapRenderUtil.js
│ │ │ │ ├── TripInfo.js
│ │ │ │ ├── Waypoint.js
│ │ │ │ └── mapUtil.js
│ │ │ ├── Autocomplete.js
│ │ │ └── MobileMapPanel.js
│ │ ├── NewTrip.js
│ │ ├── Landing.js
│ │ ├── Settings.js
│ │ ├── Register.js
│ │ ├── Login.js
│ │ ├── Breadcrumb.js
│ │ ├── Billing
│ │ │ ├── StripeProvider.js
│ │ │ ├── Pending.js
│ │ │ ├── index.js
│ │ │ ├── Invoices.js
│ │ │ ├── AccountType.js
│ │ │ └── PaymentDetails.js
│ │ ├── EditTrip.js
│ │ ├── icons
│ │ │ ├── AddSvg.js
│ │ │ ├── orange-marker.svg
│ │ │ ├── black-marker.svg
│ │ │ ├── green-marker.svg
│ │ │ ├── FlagSvg.js
│ │ │ ├── AddButton.js
│ │ │ ├── SaveSvg.js
│ │ │ ├── DeleteIcon.js
│ │ │ ├── ChevronSvg.js
│ │ │ ├── EditSvg.js
│ │ │ ├── UserSvg.js
│ │ │ ├── GitHubSvg.js
│ │ │ ├── Puff.js
│ │ │ ├── GoogleIcon.js
│ │ │ └── ChartSvg.js
│ │ ├── UnauthenticatedLinks.js
│ │ ├── AuthenticatedLinks.js
│ │ ├── AddTripButton.js
│ │ ├── LandingPage
│ │ │ ├── MobileMenu.js
│ │ │ ├── CallToAction.js
│ │ │ ├── LandingPageNav.js
│ │ │ ├── FooterContent.js
│ │ │ ├── Plans.js
│ │ │ ├── index.js
│ │ │ ├── Button.js
│ │ │ ├── Hero.js
│ │ │ ├── PlansCard.js
│ │ │ ├── Footer.js
│ │ │ └── Typewriter.js
│ │ ├── TripCardLoader.js
│ │ ├── Root.js
│ │ ├── PublicTripCard.js
│ │ ├── pages
│ │ │ ├── Pages.js
│ │ │ └── Dashboard.js
│ │ ├── Banner.js
│ │ ├── propTypes.js
│ │ ├── AppContainer.js
│ │ ├── Trips.js
│ │ ├── Breadcrumbs.js
│ │ ├── ArchivedTrips.js
│ │ ├── forms
│ │ │ ├── WaypointForm.js
│ │ │ ├── customInputs.js
│ │ │ ├── formValidations.js
│ │ │ ├── RecoverPassword.js
│ │ │ └── SettingsForm.js
│ │ ├── PublicTrips.js
│ │ ├── CopyTripLinkModal.js
│ │ ├── Trip.js
│ │ ├── Modals
│ │ │ └── Modal.js
│ │ └── Dropdown.js
│ ├── styles
│ │ ├── Trip.styles.js
│ │ ├── AuthenticatedLinks.styles.js
│ │ ├── Landing.styles.js
│ │ ├── Billing.styles.js
│ │ ├── Explore.styles.js
│ │ ├── Register.styles.js
│ │ ├── Login.styles.js
│ │ ├── Settings.styles.js
│ │ ├── CreateTrip.styles.js
│ │ ├── NewTrip.styles.js
│ │ ├── Dashboard.styles.js
│ │ ├── CheckoutForm.styles.js
│ │ ├── Banner.styles.js
│ │ ├── Sidebar.styles.js
│ │ ├── AddTripButton.styles.js
│ │ ├── Dropdown.styles.js
│ │ ├── TripCard.styles.js
│ │ ├── TripPicturesStyles.js
│ │ └── theme
│ │ │ ├── variables.js
│ │ │ └── GlobalStyles.js
│ ├── redux
│ │ ├── actions
│ │ │ ├── navigation.js
│ │ │ ├── modal.js
│ │ │ └── settings.js
│ │ └── reducers
│ │ │ ├── navigation.js
│ │ │ ├── modal.js
│ │ │ ├── settings.js
│ │ │ ├── billing.js
│ │ │ └── auth.js
│ ├── config
│ │ ├── firebase.js
│ │ └── index.js
│ ├── test
│ │ └── App.test.js
│ ├── index.js
│ ├── utils
│ │ ├── CustomRoute.js
│ │ └── index.js
│ └── store.js
├── .gitignore
├── .eslintrc.json
└── package.json
├── scripts
└── test
├── mkdocs.yml
├── .travis.yml
├── package.json
├── docs
└── Auth.md
└── pull_request_template.md
/netlify.toml:
--------------------------------------------------------------------------------
1 | base = "client"
--------------------------------------------------------------------------------
/server/.eslintignore:
--------------------------------------------------------------------------------
1 | /dist
--------------------------------------------------------------------------------
/server/webpack.config.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/Procfile:
--------------------------------------------------------------------------------
1 | web: node dist/server.js
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode
3 | .env
4 |
--------------------------------------------------------------------------------
/client/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/server/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
--------------------------------------------------------------------------------
/client/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false
3 | }
4 |
--------------------------------------------------------------------------------
/client/src/components/Maps/SingleTrip/mapRenderUtil.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false
3 | }
4 |
--------------------------------------------------------------------------------
/server/src/api/index.js:
--------------------------------------------------------------------------------
1 | export { restRouter } from "./restRouter"
2 |
--------------------------------------------------------------------------------
/server/src/api/resources/email/index.js:
--------------------------------------------------------------------------------
1 | export * from "./email.restRouter"
2 |
--------------------------------------------------------------------------------
/server/src/api/resources/trip/index.js:
--------------------------------------------------------------------------------
1 | export * from "./trip.restRouter"
2 |
--------------------------------------------------------------------------------
/server/src/api/resources/user/index.js:
--------------------------------------------------------------------------------
1 | export * from "./user.restRouter"
2 |
--------------------------------------------------------------------------------
/server/src/api/resources/subscribe/index.js:
--------------------------------------------------------------------------------
1 | export * from "./subscribe.restRouter"
2 |
--------------------------------------------------------------------------------
/server/src/api/resources/waypoint/index.js:
--------------------------------------------------------------------------------
1 | export * from "./waypoint.restRouter"
2 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.log
3 | *.error
4 | .webpack
5 |
6 | .env
7 | dist/
8 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/images/AJ.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/AJ.png
--------------------------------------------------------------------------------
/client/public/images/JC.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/JC.png
--------------------------------------------------------------------------------
/client/public/images/VM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/VM.png
--------------------------------------------------------------------------------
/client/public/images/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/bg.jpg
--------------------------------------------------------------------------------
/client/public/images/AB1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/AB1.png
--------------------------------------------------------------------------------
/client/public/images/UJ1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/UJ1.png
--------------------------------------------------------------------------------
/client/public/images/Thuy1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/Thuy1.png
--------------------------------------------------------------------------------
/client/public/images/pear.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/pear.jpeg
--------------------------------------------------------------------------------
/server/public/images/marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/server/public/images/marker.png
--------------------------------------------------------------------------------
/client/public/fonts/Wals-Light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/fonts/Wals-Light.otf
--------------------------------------------------------------------------------
/client/public/images/bkwdslogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/bkwdslogo.png
--------------------------------------------------------------------------------
/client/public/fonts/Wals-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/fonts/Wals-Medium.otf
--------------------------------------------------------------------------------
/client/public/fonts/Wals-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/fonts/Wals-Regular.otf
--------------------------------------------------------------------------------
/client/public/images/phoneimage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/phoneimage.png
--------------------------------------------------------------------------------
/client/public/images/features-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/features-list.png
--------------------------------------------------------------------------------
/client/public/images/features-plan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/features-plan.png
--------------------------------------------------------------------------------
/client/public/images/features-share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/features-share.png
--------------------------------------------------------------------------------
/client/public/images/features-track.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/features-track.png
--------------------------------------------------------------------------------
/client/public/images/hikerscontent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/hikerscontent.png
--------------------------------------------------------------------------------
/client/public/images/hikerscontent2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/hikerscontent2.png
--------------------------------------------------------------------------------
/client/public/fonts/Wals-Light-Oblique.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/fonts/Wals-Light-Oblique.otf
--------------------------------------------------------------------------------
/client/public/images/map_placeholder.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/images/map_placeholder.gif
--------------------------------------------------------------------------------
/client/public/fonts/Wals-Medium-Oblique.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/fonts/Wals-Medium-Oblique.otf
--------------------------------------------------------------------------------
/client/public/fonts/Wals-Regular-Oblique.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BloomTech-Labs/LabsPT1_bkwds/HEAD/client/public/fonts/Wals-Regular-Oblique.otf
--------------------------------------------------------------------------------
/scripts/test:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 | yarn
3 | cd client
4 | yarn
5 | yarn run lint
6 |
7 | cd ..
8 | cd server
9 | yarn
10 | yarn run lint
11 | yarn run test
12 |
--------------------------------------------------------------------------------
/client/src/styles/Trip.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const TripStyles = styled.div`
4 | a {
5 | text-decoration: underline;
6 | }
7 | `
8 |
--------------------------------------------------------------------------------
/client/src/styles/AuthenticatedLinks.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const AuthenticatedLinksStyles = styled.ul`
4 | display: flex;
5 | align-items: center;
6 | `
7 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Backwoods Tracker API
2 | nav:
3 | - Home: Auth.md
4 | - Authorization: Auth.md
5 | - User: User.md
6 | - Trip: Trip.md
7 | - Waypoint: Waypoint.md
8 | theme: readthedocs
--------------------------------------------------------------------------------
/client/src/components/NewTrip.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import CreateTrip from "../components/Maps/CreateTrip"
3 |
4 | const NewTrip = () => {
5 | return
6 | }
7 |
8 | export default NewTrip
9 |
--------------------------------------------------------------------------------
/client/src/redux/actions/navigation.js:
--------------------------------------------------------------------------------
1 | import { TOGGLE_SIDEBAR } from "./types"
2 |
3 | export const toggleSidebar = isSidebarOpen => dispatch => {
4 | dispatch({ type: TOGGLE_SIDEBAR, payload: isSidebarOpen })
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/redux/actions/modal.js:
--------------------------------------------------------------------------------
1 | import { OPEN_MODAL, CLOSE_MODAL } from "./types"
2 |
3 | export const openModal = () => ({
4 | type: OPEN_MODAL
5 | })
6 |
7 | export const closeModal = () => ({
8 | type: CLOSE_MODAL
9 | })
10 |
--------------------------------------------------------------------------------
/server/src/config/prod.js:
--------------------------------------------------------------------------------
1 | export const config = {
2 | port: process.env.PORT,
3 | db: {
4 | url: process.env.MONGO_URI
5 | },
6 | stripe: {
7 | instance: require("stripe")(process.env.STRIPE_KEY_SERVER_PROD)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/config/testing.js:
--------------------------------------------------------------------------------
1 | export const config = {
2 | port: process.env.PORT || 5000,
3 | db: {
4 | url: "mongodb://127.0.0.1/backwoods"
5 | },
6 | stripe: {
7 | instance: require("stripe")(process.env.STRIPE_KEY_SERVER_TEST)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/styles/Landing.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const LandingStyles = styled.div`
4 | height: 200vh;
5 | background: url(./images/pear.jpeg);
6 | .trash {
7 | position: fixed;
8 | bottom: 0;
9 | }
10 | `
11 |
--------------------------------------------------------------------------------
/client/src/styles/Billing.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const BillingStyles = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | margin-top: 30px;
7 | padding: 0px;
8 | align-items: center;
9 | width: 100%;
10 | `
11 |
--------------------------------------------------------------------------------
/client/src/styles/Explore.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | import { media } from "./theme/mixins"
4 |
5 | export const ExploreHeader = styled.h4`
6 | margin: 40px 0 20px 50px;
7 |
8 | ${media.phone`
9 | margin: 40px 0 20px 60px;
10 | `}
11 | `
12 |
--------------------------------------------------------------------------------
/client/src/components/Landing.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import * as s from "../styles/Landing.styles"
3 |
4 | const Landing = () => {
5 | return (
6 |
7 | LANDING PAGE
8 |
9 | )
10 | }
11 |
12 | export default Landing
13 |
--------------------------------------------------------------------------------
/client/src/config/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app"
2 | import "firebase/auth"
3 |
4 | import { FirebaseConfig } from "../config/index"
5 | firebase.initializeApp(FirebaseConfig)
6 |
7 | export const authRef = firebase.auth()
8 | export const provider = new firebase.auth.GoogleAuthProvider()
9 |
--------------------------------------------------------------------------------
/client/src/test/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import App from "../components/App"
4 |
5 | it("renders without crashing", () => {
6 | const div = document.createElement("div")
7 | ReactDOM.render( , div)
8 | ReactDOM.unmountComponentAtNode(div)
9 | })
10 |
--------------------------------------------------------------------------------
/client/src/styles/Register.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const RegisterStyles = styled.div`
4 | height: 100%;
5 | width: 100%;
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | justify-content: center;
10 | form {
11 | width: 300px;
12 | }
13 | `
14 |
--------------------------------------------------------------------------------
/server/tests/modules/testServer.js:
--------------------------------------------------------------------------------
1 | import request from "supertest"
2 | import app from "../../src/server"
3 |
4 | describe("Test server root path", () => {
5 | test("It should start and run without error", async () => {
6 | const response = await request(app).get("/")
7 | expect(response.statusCode).toBe(200)
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/client/src/components/Settings.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import SettingsForm from "./forms/SettingsForm"
4 | import * as s from "../styles/Settings.styles"
5 |
6 | const Settings = () => (
7 |
8 | Change email / password
9 |
10 |
11 | )
12 |
13 | export default Settings
14 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "bkwds.",
3 | "name": "bkwds.",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/components/Register.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import * as s from "../styles/Register.styles"
4 | import RegisterForm from "./forms/RegisterForm"
5 |
6 | const Register = () => {
7 | return (
8 |
9 | Sign up
10 |
11 |
12 | )
13 | }
14 |
15 | export default Register
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "10"
5 |
6 | before_install:
7 | - chmod +x scripts/test
8 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0
9 | - export PATH="$HOME/.yarn/bin:$PATH"
10 |
11 | cache:
12 | yarn: true
13 | directories:
14 | - node_modules
15 |
16 | services: mongodb
17 |
18 | script:
19 | - scripts/test
20 |
21 |
--------------------------------------------------------------------------------
/server/src/api/resources/email/email.restRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import * as emailController from "./email.controller"
3 |
4 | export const emailRouter = express.Router()
5 |
6 | emailRouter.route("/user/:email").post(emailController.sendPasswordResetEmail)
7 |
8 | emailRouter
9 | .route("/receive_new_password/:userId/:token")
10 | .post(emailController.receiveNewPassword)
11 |
--------------------------------------------------------------------------------
/client/src/styles/Login.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const LoginStyles = styled.div`
4 | height: 100%;
5 | width: 100%;
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | justify-content: center;
10 | form {
11 | width: 300px;
12 | }
13 | input {
14 | width: 100%;
15 | }
16 | a {
17 | margin-top: 1rem;
18 | }
19 | `
20 |
--------------------------------------------------------------------------------
/server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "commonjs": true,
4 | "es6": true
5 | },
6 | "plugins": ["prettier"],
7 | "extends": ["plugin:prettier/recommended"],
8 | "parser": "babel-eslint",
9 | "parserOptions": {
10 | "ecmaVersion": 2018,
11 | "sourceType": "module"
12 | },
13 | "rules": {
14 | "no-unused-vars": "error",
15 | "prettier/prettier": "error"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/redux/reducers/navigation.js:
--------------------------------------------------------------------------------
1 | import { TOGGLE_SIDEBAR } from "../actions/types"
2 |
3 | const defaultState = {
4 | isSidebarOpen: false
5 | }
6 |
7 | export const navigationReducer = (state = defaultState, action) => {
8 | switch (action.type) {
9 | case TOGGLE_SIDEBAR:
10 | return { ...state, isSidebarOpen: !action.payload }
11 |
12 | default:
13 | return state
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Link } from "react-router-dom"
3 |
4 | import * as s from "../styles/Login.styles"
5 | import LoginForm from "./forms/LoginForm"
6 |
7 | const Login = () => (
8 |
9 | Log in
10 |
11 | Forgot your password?
12 |
13 | )
14 |
15 | export default Login
16 |
--------------------------------------------------------------------------------
/client/src/redux/reducers/modal.js:
--------------------------------------------------------------------------------
1 | import { OPEN_MODAL, CLOSE_MODAL } from "../actions/types"
2 |
3 | const defaultState = {
4 | isOpen: false
5 | }
6 |
7 | export const modalReducer = (state = defaultState, action) => {
8 | switch (action.type) {
9 | case OPEN_MODAL:
10 | return { ...state, isOpen: true }
11 | case CLOSE_MODAL:
12 | return { ...state, isOpen: false }
13 | default:
14 | return state
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/styles/Settings.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const SettingsStyles = styled.div`
4 | h4 {
5 | text-align: center;
6 | }
7 |
8 | .container {
9 | max-width: 100%;
10 | margin: 0 auto;
11 | display: flex;
12 | justify-content: flex-start;
13 | flex-wrap: wrap;
14 | }
15 |
16 | width: 100%;
17 | max-width: 400px;
18 | padding: 30px;
19 | float: none;
20 | margin: 0 auto;
21 | `
22 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .env
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
--------------------------------------------------------------------------------
/server/src/server.js:
--------------------------------------------------------------------------------
1 | import { restRouter } from "./api"
2 | import dotenv from "dotenv"
3 | import express from "express"
4 | import cors from "cors"
5 |
6 | dotenv.config()
7 | const app = express()
8 |
9 | app.use(express.json({ limit: 4000000 }))
10 | app.use(cors())
11 | app.use(express.static("public"))
12 |
13 | app.use("/api", restRouter)
14 |
15 | app.all("*", (req, res) => {
16 | res.json({
17 | ok: true
18 | })
19 | })
20 |
21 | export default app
22 |
--------------------------------------------------------------------------------
/server/src/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 | import config from "./config"
3 |
4 | mongoose.Promise = global.Promise
5 |
6 | export const connect = () => {
7 | return mongoose
8 | .connect(
9 | config.db.url,
10 | { userNewUrlParser: true }
11 | )
12 | .then(() => {
13 | console.log("MONGO DB CONNECTED")
14 | })
15 | .catch(err => {
16 | console.log(err)
17 | console.log(`Connection failed with config ${config.db.url}`)
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/server/src/api/resources/user/user.restRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import * as userController from "./user.controller"
3 |
4 | export const userRouter = express.Router()
5 |
6 | userRouter
7 | .route("/")
8 | .get(userController.getAllUsers)
9 | .post(userController.createUser)
10 |
11 | userRouter
12 | .route("/:id")
13 | .get(userController.getOneUser)
14 | .put(userController.updateUser)
15 | .delete(userController.deleteUser)
16 |
17 | userRouter.route("/:id/trips").get(userController.getUserTrips)
18 |
--------------------------------------------------------------------------------
/server/src/index.js:
--------------------------------------------------------------------------------
1 | import { createServer } from "http"
2 | import { connect } from "./db"
3 | import config from "./config"
4 | import app from "./server"
5 |
6 | const server = createServer(app)
7 | let currentApp = app
8 |
9 | connect()
10 | server.listen(config.port, () => {
11 | console.log(`Server listening on port ${config.port}`)
12 | })
13 | if (module.hot) {
14 | module.hot.accept(["./server"], () => {
15 | server.removeListener("request", currentApp)
16 | server.on("request", app)
17 | currentApp = app
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/components/Breadcrumb.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Link } from "react-router-dom"
3 | import PropTypes from "prop-types"
4 |
5 | const Breadcrumb = ({ linkpath, name, last }) => {
6 | return (
7 |
8 |
9 | {name}
10 |
11 |
12 | )
13 | }
14 |
15 | Breadcrumb.propTypes = {
16 | linkpath: PropTypes.string.isRequired,
17 | name: PropTypes.string.isRequired,
18 | last: PropTypes.bool
19 | }
20 | export default Breadcrumb
21 |
--------------------------------------------------------------------------------
/client/src/components/Maps/SingleTrip/TripInfo.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | export const TripInfo = props => {
5 | const trip = { props }
6 | const style = {
7 | padding: ".5rem",
8 | width: "30%",
9 | height: "35%",
10 | background: "white",
11 | position: "absolute",
12 | zIndex: 10,
13 | top: "1rem",
14 | right: "1rem"
15 | }
16 | return (
17 |
18 |
{trip.name}
19 |
20 | )
21 | }
22 |
23 | TripInfo.propTypes = {
24 | trip: PropTypes.object
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/Billing/StripeProvider.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { connect } from "react-redux"
3 | import { StripeProvider } from "react-stripe-elements"
4 | import PropTypes from "prop-types"
5 |
6 | const StripeElementsContainer = ({ children, stripe }) => (
7 | {children}
8 | )
9 |
10 | StripeElementsContainer.propTypes = {
11 | stripe: PropTypes.object,
12 | children: PropTypes.element.isRequired
13 | }
14 |
15 | export default connect(({ billing }) => ({ stripe: billing.stripe }))(
16 | StripeElementsContainer
17 | )
18 |
--------------------------------------------------------------------------------
/client/src/components/EditTrip.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import * as s from "../styles/NewTrip.styles"
4 | import NewTripForm from "./forms/NewTripForm"
5 |
6 | const EditTrip = () => {
7 | return (
8 |
9 |
10 |
Edit trip
11 |
{ }
12 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | export default EditTrip
21 |
--------------------------------------------------------------------------------
/client/src/components/icons/AddSvg.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | const AddSvg = ({ width = "18px", height = "18px" }) => {
5 | return (
6 |
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | AddSvg.propTypes = {
21 | width: PropTypes.string,
22 | height: PropTypes.string
23 | }
24 |
25 | export default AddSvg
26 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | Server for Backwoods Tracker
2 |
3 | ## Deployment
4 |
5 | We deploy to Heroku: https://backwoods-tracker.herokuapp.com/
6 |
7 | _Note:_ Make sure you set the environment variables in the Heroku dashboard! `MONGO_URI` should be our production database.
8 |
9 | To deploy, cd into the server folder and run these commands:
10 |
11 | ```bash
12 | $ git init
13 | $ git remote add heroku https://git.heroku.com/backwoods-tracker.git
14 | ```
15 |
16 | Double check that your remote is working by running `git remote -v`. You should see heroku pointing to our app on Heroku.
17 |
18 | Pushing is easy, just do `git push heroku master`.
19 |
--------------------------------------------------------------------------------
/client/src/components/UnauthenticatedLinks.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Link } from "react-router-dom"
3 | import PropTypes from "prop-types"
4 |
5 | const UnauthenticatedLinks = ({ pathname }) => (
6 |
7 | {pathname === "/login" || pathname === "/" ? (
8 | Sign up
9 | ) : null}
10 | {pathname === "/register" || pathname === "/" ? (
11 | Login
12 | ) : null}
13 |
14 | )
15 |
16 | UnauthenticatedLinks.propTypes = {
17 | pathname: PropTypes.string.isRequired
18 | }
19 |
20 | export default UnauthenticatedLinks
21 |
--------------------------------------------------------------------------------
/server/src/api/resources/subscribe/subscribe.restRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import * as subscribeController from "./subscribe.controller"
3 |
4 | export const subscribeRouter = express.Router()
5 | const stripe = require("stripe")(process.env.STRIPE_KEY_SERVER_TEST)
6 |
7 | subscribeRouter
8 | .route("/invoices")
9 | .post((req, res) => subscribeController.retrieveInvoices(req, res, stripe))
10 |
11 | subscribeRouter
12 | .route("/:id")
13 | .post((req, res) => subscribeController.subscribe(req, res, stripe))
14 |
15 | subscribeRouter
16 | .route("/cancel/:id")
17 | .post((req, res) => subscribeController.cancel(req, res, stripe))
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "devDependencies": {
4 | "eslint-config-prettier": "^3.3.0",
5 | "eslint-plugin-prettier": "^3.0.0",
6 | "eslint-plugin-react": "^7.11.1",
7 | "husky": "^1.2.0",
8 | "lint-staged": "^8.1.0",
9 | "prettier": "^1.15.3"
10 | },
11 | "husky": {
12 | "hooks": {
13 | "pre-commit": "lint-staged"
14 | }
15 | },
16 | "lint-staged": {
17 | "*.{js,json,css,md}": [
18 | "prettier --write",
19 | "git add"
20 | ]
21 | },
22 | "scripts": {
23 | "lint:client": "eslint client/**/*.js",
24 | "lint:server": "eslint server/**/*.js"
25 | },
26 | "dependencies": {}
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/components/AuthenticatedLinks.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import Dropdown from "./Dropdown"
5 | import * as s from "../styles/AuthenticatedLinks.styles"
6 |
7 | class AuthenticatedLinks extends Component {
8 | handleLogout = e => {
9 | e.preventDefault()
10 | this.props.logout()
11 | }
12 |
13 | render() {
14 | return (
15 |
16 |
17 |
18 | )
19 | }
20 | }
21 |
22 | AuthenticatedLinks.propTypes = {
23 | logout: PropTypes.func.isRequired
24 | }
25 |
26 | export default AuthenticatedLinks
27 |
--------------------------------------------------------------------------------
/client/src/styles/CreateTrip.styles.js:
--------------------------------------------------------------------------------
1 | import Styled from "styled-components"
2 | import { media } from "./theme/mixins"
3 |
4 | export const MapWrapper = Styled.div`
5 | position:relative;
6 | margin-left: -50px;
7 | overflow-x: hidden;
8 | overflow-y: hidden;
9 | ${media.tablet`
10 | margin-left: 0;
11 | `}
12 |
13 | height: 100%;
14 |
15 | #plus-icon {
16 | visibility: hidden;
17 |
18 | ${media.tablet`
19 | visibility: visible;
20 | cursor: pointer;
21 | z-index: 1;
22 | right: 40px;
23 | bottom: 200px;
24 | background: white;
25 | border-radius: 50%;
26 | position: absolute;
27 | `}
28 | }
29 | `
30 |
--------------------------------------------------------------------------------
/server/src/config/index.js:
--------------------------------------------------------------------------------
1 | import merge from "lodash.merge"
2 |
3 | const env = process.env.NODE_ENV
4 |
5 | const baseConfig = {
6 | port: process.env.PORT || 5000,
7 | secrets: {
8 | JWT_SECRET: process.env.JWT_SECRET
9 | },
10 | db: {
11 | url: process.env.MONGO_URI
12 | }
13 | }
14 |
15 | let envConfig = {}
16 |
17 | switch (env) {
18 | case "development":
19 | case "dev":
20 | break
21 | case "test":
22 | case "testing":
23 | envConfig = require("./testing").config
24 | break
25 | case "prod":
26 | case "production":
27 | envConfig = require("./prod").config
28 | break
29 | default:
30 | }
31 |
32 | export default merge(baseConfig, envConfig)
33 |
--------------------------------------------------------------------------------
/client/src/components/AddTripButton.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Link } from "react-router-dom"
3 | import PropTypes from "prop-types"
4 | import * as s from "../styles/AddTripButton.styles"
5 |
6 | const AddTripButton = ({ text }) => (
7 |
8 |
9 |
10 |
11 |
{text}
12 | +
13 |
14 |
15 |
16 |
17 | )
18 |
19 | AddTripButton.propTypes = {
20 | text: PropTypes.string.isRequired
21 | }
22 |
23 | export default AddTripButton
24 |
--------------------------------------------------------------------------------
/client/src/components/icons/orange-marker.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Path
5 | Created with Sketch.
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/client/src/components/icons/black-marker.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Path Copy 3
5 | Created with Sketch.
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/client/src/components/icons/green-marker.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Path Copy 4
5 | Created with Sketch.
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/server/src/api/resources/trip/trip.restRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import * as tripController from "./trip.controller"
3 |
4 | export const tripRouter = express.Router()
5 |
6 | tripRouter
7 | .route("/")
8 | .get(tripController.getAllTrips)
9 | .post(tripController.createTrip)
10 |
11 | tripRouter.route("/repeat").post(tripController.repeatTrip)
12 |
13 | tripRouter
14 | .route("/:id")
15 | .get(tripController.getOneTrip)
16 | .put(tripController.updateTrip)
17 | .delete(tripController.deleteTrip)
18 |
19 | tripRouter.route("/:id/waypoints").get(tripController.populateWaypoints)
20 |
21 | tripRouter.route("/upload/:id").put(tripController.uploadPics)
22 | tripRouter.route("/pictures/:id").get(tripController.uploadPics)
23 |
--------------------------------------------------------------------------------
/server/src/api/resources/waypoint/waypoint.restRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import * as waypointController from "./waypoint.controller"
3 |
4 | export const waypointRouter = express.Router()
5 |
6 | waypointRouter
7 | .route("/")
8 | .get(waypointController.getAllWaypoints)
9 | .post(waypointController.createWaypoint)
10 |
11 | waypointRouter
12 | .route("/trip/:tripId")
13 | .get(waypointController.getWaypointsByTrip)
14 | .delete(waypointController.deleteWaypointsByTrip)
15 |
16 | waypointRouter.route("/batch").post(waypointController.createManyWaypoints)
17 |
18 | waypointRouter
19 | .route("/:id")
20 | .get(waypointController.getWaypoint)
21 | .put(waypointController.updateWaypoint)
22 | .delete(waypointController.deleteWaypoint)
23 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/MobileMenu.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { connect } from "react-redux"
3 | import { push as Menu } from "react-burger-menu"
4 | import PropTypes from "prop-types"
5 | import { Link } from "react-router-dom"
6 |
7 | const MobileMenu = ({ isOpen }) => (
8 |
15 | Log in
16 | Sign Up
17 |
18 | )
19 |
20 | MobileMenu.propTypes = {
21 | isOpen: PropTypes.bool.isRequired
22 | }
23 |
24 | export default connect(({ navigation }) => ({
25 | isOpen: navigation.isSidebarOpen
26 | }))(MobileMenu)
27 |
--------------------------------------------------------------------------------
/client/src/components/icons/FlagSvg.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | const FlagSvg = ({ height = "32px", width = "32px", fill }) => {
5 | return (
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | FlagSvg.propTypes = {
20 | height: PropTypes.string,
21 | width: PropTypes.string,
22 | fill: PropTypes.string
23 | }
24 |
25 | export default FlagSvg
26 |
--------------------------------------------------------------------------------
/client/src/config/index.js:
--------------------------------------------------------------------------------
1 | // GENERAL
2 | export const APP_NAME = process.env.REACT_APP_NAME
3 | export const CLIENT_URI = process.env.REACT_APP_CLIENT_URI
4 | export const SERVER_URI = process.env.REACT_APP_SERVER_URI
5 | export const MAPS_KEY = process.env.REACT_APP_MAPS_KEY
6 |
7 | // STRIPE
8 | export const STRIPE_KEY = process.env.REACT_APP_STRIPE_KEY
9 | export const STRIPE_KEY_SERVER = process.env.REACT_APP_STRIPE_KEY_SERVER
10 | export const STRIPE_PLAN_ID_TEST = process.env.REACT_APP_STRIPE_PLAN_ID_TEST
11 |
12 | // OAUTH
13 | export const FB_APP_ID = process.env.REACT_APP_FB_APP_ID
14 | export const FirebaseConfig = {
15 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
16 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
17 | databaseURL: process.env.REACT_APP_FIREBASE_DB_URL
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/redux/reducers/settings.js:
--------------------------------------------------------------------------------
1 | import {
2 | INI_UPDATE_SETTINGS,
3 | UPDATE_SETTINGS_SUCCESS,
4 | UPDATE_SETTINGS_FAILURE
5 | } from "../actions/types"
6 |
7 | const defaultState = {
8 | pending: false,
9 | error: null
10 | }
11 |
12 | export const settingsReducer = (state = defaultState, action) => {
13 | switch (action.type) {
14 | case INI_UPDATE_SETTINGS:
15 | return {
16 | ...state,
17 | pending: true
18 | }
19 | case UPDATE_SETTINGS_SUCCESS:
20 | return {
21 | ...state,
22 | pending: false
23 | }
24 |
25 | case UPDATE_SETTINGS_FAILURE:
26 | return {
27 | ...state,
28 | pending: false,
29 | error: action.payload
30 | }
31 |
32 | default:
33 | return state
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/tests/modules/testPublic.js:
--------------------------------------------------------------------------------
1 | import request from "supertest"
2 | import app from "../../src/server"
3 |
4 | let tripId
5 |
6 | describe("Test public trip endpoints", () => {
7 | test("GET all public trips", done => {
8 | request(app)
9 | .get("/api/public/trips")
10 | .then(response => {
11 | tripId = response.body[1].id
12 | expect(response.statusCode).toBe(200)
13 | expect(response.body.length).toEqual(2)
14 | return done()
15 | })
16 | })
17 | test("Get single public trip", done => {
18 | request(app)
19 | .get(`/api/public/trips/${tripId}`)
20 | .then(response => {
21 | expect(response.statusCode).toBe(200)
22 | expect(response.body.isPublic).toBe(true)
23 | return done()
24 | })
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "plugins": ["prettier", "react"],
8 | "extends": ["plugin:prettier/recommended", "plugin:react/recommended"],
9 | "parser": "babel-eslint",
10 | "parserOptions": {
11 | "ecmaFeatures": {
12 | "jsx": true
13 | },
14 | "ecmaVersion": 2018,
15 | "sourceType": "module"
16 | },
17 | "rules": {
18 | "array-callback-return": "off",
19 | "no-unused-vars": "error",
20 | "prettier/prettier": "error",
21 | "react/prop-types": 2,
22 | "react/no-unescaped-entities": 0,
23 | "react/prefer-es6-class": "error",
24 | "react/prefer-stateless-function": "error"
25 | },
26 | "settings": {
27 | "react": {
28 | "version": "16.6.3"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/components/TripCardLoader.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ContentLoader from "react-content-loader"
3 |
4 | export const TripCardLoader = () => (
5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 |
22 | export default TripCardLoader
23 |
--------------------------------------------------------------------------------
/server/tests/modules/testSettings.js:
--------------------------------------------------------------------------------
1 | import request from "supertest"
2 | import app from "../../src/server"
3 |
4 | import * as mock from "../mock"
5 |
6 | let token
7 | let email
8 |
9 | describe("Test Setting page", () => {
10 | beforeAll(async done => {
11 | const response = await request(app)
12 | .post("/api/login")
13 | .send({ email: mock.userOne.email, password: "testpass" })
14 | email = mock.userOne.email
15 | token = response.body.token
16 | return done()
17 | })
18 | test("POST change password", done => {
19 | request(app)
20 | .get("/api/changePassword")
21 | .set("Authorization", `Bearer ${token}`)
22 | .send({ email, oldPassword: "testpass", newPassword: "newTestPass" })
23 | .then(response => {
24 | expect(response.statusCode).toBe(200)
25 | done()
26 | })
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/client/src/components/Root.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Switch, Route } from "react-router-dom"
3 |
4 | import LandingPage from "./LandingPage/"
5 | import Dashboard from "./pages/Dashboard"
6 | import CustomRoute from "../utils/CustomRoute"
7 | import Pages from "./pages/Pages"
8 | import ActiveTrip from "./Maps/SingleTrip"
9 |
10 | const Root = () => (
11 |
12 |
13 | (
17 |
18 | )}
19 | />
20 |
21 |
22 | 404: Route not found
} />
23 |
24 |
25 | )
26 | export default Root
27 |
--------------------------------------------------------------------------------
/server/src/api/modules/public.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import { Trip } from "../resources/trip/trip.model"
3 |
4 | const getAllPublicTrips = (req, res) => {
5 | Trip.find({ isPublic: true })
6 | .then(trips => {
7 | res.status(200).json(trips)
8 | })
9 | .catch(err => {
10 | res.status(500).json(err)
11 | })
12 | }
13 |
14 | const getOnePublicTrip = (req, res) => {
15 | Trip.findOne({ _id: req.params.id })
16 | .populate("waypoints")
17 | .exec()
18 | .then(trip => {
19 | if (trip.isPublic === false) {
20 | return res.status(401).json("Trip is not publicly available")
21 | }
22 | res.status(200).json(trip)
23 | })
24 | .catch(err => {
25 | return res.status(500).send(err)
26 | })
27 | }
28 | export const publicRouter = express.Router()
29 |
30 | publicRouter.route("/trips").get(getAllPublicTrips)
31 | publicRouter.route("/trips/:id").get(getOnePublicTrip)
32 |
--------------------------------------------------------------------------------
/docs/Auth.md:
--------------------------------------------------------------------------------
1 | ## Register a new user
2 |
3 | Registers and creates a new user in the database
4 |
5 | **URL**: `/api/register/`
6 |
7 | **Method**: `POST`
8 |
9 | **Token required**: NO
10 |
11 | **Success Response**:
12 |
13 | - **Status Code**: `201 Created`
14 |
15 | ---
16 |
17 | ## Log in a user
18 |
19 | Returns the logged in user and authorization token
20 |
21 | **URL**: `/api/login/`
22 |
23 | **Method**: `POST`
24 |
25 | **Token required**: NO
26 |
27 | **Success Response**:
28 |
29 | - **Status Code**: `200 OK`
30 |
31 | **Example Content**
32 |
33 | ```
34 | {
35 | "user": {
36 | "id": "5c32a56f83d4a923130752b2",
37 | "username": "Diddy",
38 | "email": "test@gmail.com",
39 | "subscribed": false
40 | },
41 | "token": "e2JhbGciOgJIUzI1NiIsInR5dCI6IkpXVCJ9.eyJpZCI6IjVjMzJhNTZmODNkNGE5MjMxMzA#NTJihnIsImlhdCIhMTU0szAwNgUwNiwiZXhwIjoxNTQ3MDkxOTA2fQ.zb8M8jpVWDfxdK2Jum6iy-MCNQfyQNNaq_UpX3U5U6Q"
42 | }
43 | ```
44 |
45 | ---
46 |
--------------------------------------------------------------------------------
/client/src/styles/NewTrip.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { media, flexCenterMixin } from "./theme/mixins"
3 |
4 | export const NewTripStyles = styled.div`
5 | .create-trip {
6 | display: flex;
7 | flex-direction: column;
8 | }
9 |
10 | .new-trip-form {
11 | z-index: 10000;
12 | position: relative;
13 | form {
14 | display: flex;
15 | flex-wrap: wrap;
16 | }
17 |
18 | .new-trip-form-field {
19 | ${flexCenterMixin};
20 | }
21 |
22 | button {
23 | width: 10rem;
24 | }
25 | }
26 |
27 | ${media.tablet`
28 | input {
29 | width: 10rem;
30 | }
31 | `}
32 | ${media.phone`
33 | input {
34 | width: 100%;
35 | }
36 | `}
37 |
38 | .waypoint-form {
39 | form {
40 | display: flex;
41 | flex-wrap: wrap;
42 | }
43 |
44 | button {
45 | width: 10rem;
46 | }
47 |
48 | .waypoint-form-field {
49 | ${flexCenterMixin};
50 | }
51 | }
52 | `
53 |
--------------------------------------------------------------------------------
/server/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 |
3 | require("dotenv").config()
4 |
5 | module.exports = {
6 | entry: {
7 | app: "./src/index"
8 | },
9 | mode: "production",
10 | watch: false,
11 | target: "node",
12 | node: {
13 | __filename: true,
14 | __dirname: true
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.js?$/,
20 | use: [
21 | {
22 | loader: "babel-loader",
23 | options: {
24 | babelrc: false,
25 | presets: ["@babel/preset-env"],
26 | plugins: [
27 | "@babel/plugin-transform-regenerator",
28 | "@babel/plugin-transform-runtime",
29 | "@babel/plugin-proposal-function-bind"
30 | ]
31 | }
32 | }
33 | ],
34 | exclude: /node_modules/
35 | }
36 | ]
37 | },
38 | plugins: [],
39 | output: { path: path.join(__dirname, "dist"), filename: "server.js" }
40 | }
41 |
--------------------------------------------------------------------------------
/client/src/components/Billing/Pending.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 |
4 | import PuffIcon from "../icons/Puff"
5 |
6 | const Dimmer = styled.div`
7 | background: rgba(0, 0, 0, 0.25);
8 | top: 0;
9 | left: 0;
10 | height: 100vh;
11 | width: 100%;
12 | position: fixed;
13 | z-index: 8;
14 | `
15 |
16 | const Spinner = styled.div`
17 | position: absolute;
18 | display: flex;
19 | flex-direction: column;
20 | align-self: center;
21 | justify-self: center;
22 | align-items: center;
23 | justify-content: center;
24 | width: 300px;
25 | height: 200px;
26 | z-index: 9;
27 | border-radius: 10px;
28 | box-shadow: 0px 8px 24px rgba(13, 13, 18, 0.04);
29 | background: white;
30 |
31 | span {
32 | margin-bottom: 20px;
33 | }
34 | `
35 |
36 | const Pending = () => (
37 | <>
38 |
39 | Please wait...
40 |
41 |
42 |
43 | >
44 | )
45 |
46 | export default Pending
47 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { render } from "react-dom"
3 | import { Provider } from "react-redux"
4 | import { ConnectedRouter } from "connected-react-router"
5 | import { ThemeProvider } from "styled-components"
6 |
7 | import "bootstrap/dist/css/bootstrap.min.css"
8 | import * as serviceWorker from "./serviceWorker"
9 |
10 | import Root from "./components/Root"
11 | import { store } from "./store"
12 | import { theme } from "./styles/theme/variables"
13 | import { history } from "./store"
14 |
15 | render(
16 |
17 |
18 |
19 |
20 |
21 |
22 | ,
23 | document.getElementById("root")
24 | )
25 |
26 | // If you want your app to work offline and load faster, you can change
27 | // unregister() to register() below. Note this comes with some pitfalls.
28 | // Learn more about service workers: http://bit.ly/CRA-PWA
29 | serviceWorker.unregister()
30 |
--------------------------------------------------------------------------------
/server/src/api/modules/email.js:
--------------------------------------------------------------------------------
1 | import nodemailer from "nodemailer"
2 |
3 | export const transporter = nodemailer.createTransport({
4 | service: "gmail",
5 | auth: {
6 | user: process.env.EMAIL_LOGIN,
7 | pass: process.env.EMAIL_PASSWORD
8 | }
9 | })
10 |
11 | export const getPasswordResetURL = (user, token) =>
12 | `http://localhost:3000/password/reset/${user._id}/${token}`
13 |
14 | export const resetPasswordTemplate = (user, url) => {
15 | const from = process.env.EMAIL_LOGIN
16 | const to = user.email
17 | const subject = "🌻 Backwoods Password Reset 🌻"
18 | const html = `
19 | Hey ${user.displayName || user.email},
20 | We heard that you lost your Backwoods password. Sorry about that!
21 | But don’t worry! You can use the following link to reset your password:
22 | ${url}
23 | If you don’t use this link within 1 hour, it will expire.
24 | Do something outside today!
25 | –Your friends at Backwoods
26 | `
27 |
28 | return { from, to, subject, html }
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/api/restRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import { userRouter } from "./resources/user"
3 | import { tripRouter } from "./resources/trip"
4 | import { waypointRouter } from "./resources/waypoint"
5 | import { protect, register, login, changePassword } from "./modules/auth"
6 | import { subscribeRouter } from "./resources/subscribe"
7 | import { emailRouter } from "./resources/email"
8 | import { publicRouter } from "./modules/public"
9 |
10 | export const restRouter = express.Router()
11 |
12 | // Auth routes
13 | restRouter.route("/register").post(register)
14 | restRouter.route("/login").post(login)
15 | restRouter.route("/changePassword").post(protect, changePassword)
16 |
17 | // Resource routes
18 | restRouter.use("/users", protect, userRouter)
19 | restRouter.use("/trips", protect, tripRouter)
20 | restRouter.use("/waypoints", protect, waypointRouter)
21 |
22 | // Service routes
23 | restRouter.use("/subscribe", protect, subscribeRouter)
24 | restRouter.use("/reset_password", emailRouter)
25 |
26 | // Public route
27 | restRouter.use("/public", publicRouter)
28 |
--------------------------------------------------------------------------------
/client/src/components/Maps/SingleTrip/Waypoint.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import * as s from "./components"
5 | import DeleteIcon from "../../icons/DeleteIcon"
6 |
7 | const Waypoint = ({ i, isEditing, handleDelete, handleEdit, name }) => (
8 |
9 | handleEdit(e, i)}
16 | />
17 |
18 | handleDelete(i)}
22 | >
23 |
24 |
25 |
26 | )
27 |
28 | Waypoint.propTypes = {
29 | handleDelete: PropTypes.func.isRequired,
30 | handleEdit: PropTypes.func.isRequired,
31 | i: PropTypes.number.isRequired,
32 | isEditing: PropTypes.bool.isRequired,
33 | name: PropTypes.string.isRequired
34 | }
35 |
36 | export default Waypoint
37 |
--------------------------------------------------------------------------------
/client/src/components/icons/AddButton.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | const AddButton = ({ addWaypoint }) => (
5 |
13 |
14 |
15 |
20 |
25 |
26 |
27 |
28 | )
29 |
30 | AddButton.propTypes = {
31 | addWaypoint: PropTypes.func.isRequired
32 | }
33 |
34 | export default AddButton
35 |
--------------------------------------------------------------------------------
/client/src/utils/CustomRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { connect } from "react-redux"
3 | import { Redirect, Route } from "react-router"
4 |
5 | import { addTokenToState } from "../redux/actions/auth"
6 |
7 | const CustomRoute = props => {
8 | const { isLoggedIn, protectedPath, checkedForToken, ...rest } = props
9 |
10 | // If not logged in and haven't checked for token yet,
11 | // try to query DB for user with token:
12 | if (!checkedForToken && !isLoggedIn) {
13 | props.addTokenToState()
14 | }
15 |
16 | if (isLoggedIn || !protectedPath) {
17 | return
18 | }
19 |
20 | if (protectedPath && !isLoggedIn) {
21 | return (
22 |
28 | )
29 | }
30 | }
31 |
32 | const mapStateToProps = state => ({
33 | isLoggedIn: state.auth.isLoggedIn,
34 | checkedForToken: state.auth.checkedForToken
35 | })
36 |
37 | const mapDispatchToProps = { addTokenToState }
38 |
39 | export default connect(
40 | mapStateToProps,
41 | mapDispatchToProps
42 | )(CustomRoute)
43 |
--------------------------------------------------------------------------------
/client/src/styles/Dashboard.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { media } from "../styles/theme/mixins"
3 |
4 | export const DashboardStyles = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: center;
9 | height: 100%;
10 |
11 | /* Modal styles: */
12 |
13 | ${media.phone`
14 |
15 | form {
16 | padding: 0 0.5rem;
17 | border-radius: none;
18 | background: inherit;
19 | box-shadow: none;
20 |
21 | label, input {
22 | font-size: 0.825rem;
23 | }
24 | input {
25 | margin-bottom: 0.625rem;
26 | padding: 0.375rem 0.625rem;
27 | }
28 |
29 | button {
30 | width: 100%;
31 | }
32 | }
33 |
34 | `}
35 |
36 | /* h6 is only used to greet a first timer in the modal */
37 | h6 {
38 | color: ${({ theme }) => theme.tertiary};
39 | font-weight: 300;
40 | font-size: 1.5rem;
41 | ${media.tablet`
42 | font-size: 1.25rem;
43 | font-style: italic;
44 | `}
45 | ${media.phone`
46 | font-size: 1.125rem;
47 | text-align: center;
48 | `}
49 | }
50 | `
51 |
--------------------------------------------------------------------------------
/client/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export function isProtectedPath(pathname, pathArray) {
2 | return pathArray.reduce(
3 | (acc, curr) => (pathname === curr ? true : acc),
4 | false
5 | )
6 | }
7 |
8 | export function* makeTaglineIterator(taglinesArray) {
9 | let count = 0
10 | while (count < Infinity) {
11 | yield taglinesArray[count++ % taglinesArray.length]
12 | }
13 | }
14 |
15 | export function validateEmail(string) {
16 | return !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(string)
17 | }
18 |
19 | export const formatDate = date => date.toISOString().split("T")[0]
20 |
21 | export const getToday = () => new Date()
22 |
23 | export const getTomorrow = () =>
24 | (today => new Date(new Date().setDate(today.getDate() + 1)))(new Date())
25 |
26 | export const convertMarkerToWaypoint = marker => ({
27 | order: marker.index + 1,
28 | name: `Checkpoint ${marker.index}`,
29 | lat: marker.getPosition().lat(),
30 | lon: marker.getPosition().lng(),
31 | start: Date.now(),
32 | end: Date.now()
33 | })
34 |
35 | export const scrollTo = target =>
36 | document
37 | .getElementById(target)
38 | .scrollIntoView({ block: "start", behavior: "smooth" })
39 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/CallToAction.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 | import ButtonCTA from "./ButtonCTA"
4 |
5 | const CallToAction = styled.div`
6 | display: grid;
7 | grid-template-columns: 1fr 1fr;
8 | height: 90%;
9 | width: 100%;
10 | margin-top: 42px;
11 | margin-right: 2rem;
12 |
13 | h1 {
14 | color: white !important;
15 | text-shadow: 0.5px 0.5px 0.5px #000000;
16 | }
17 | div {
18 | display: flex;
19 | flex-direction: column;
20 | align-self: center;
21 | justify-self: center;
22 | }
23 | .accent {
24 | color: #f26a21 !important;
25 | }
26 |
27 | a {
28 | align-self: center;
29 | }
30 | `
31 |
32 | const LandingCTA = () => {
33 | return (
34 |
35 |
36 |
The companion app for
37 |
38 | +
39 | hiking
40 |
41 |
42 | +
43 | mountain climbing
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | export default LandingCTA
52 |
--------------------------------------------------------------------------------
/client/src/components/icons/SaveSvg.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | const SaveSvg = ({ width = "20px", height = "20px" }) => {
5 | return (
6 |
14 |
15 |
19 |
24 |
25 |
26 | )
27 | }
28 |
29 | SaveSvg.propTypes = {
30 | width: PropTypes.string,
31 | height: PropTypes.string
32 | }
33 |
34 | export default SaveSvg
35 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/LandingPageNav.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 | import { Link, NavLink } from "react-router-dom"
4 |
5 | const NavigationMenu = styled.div`
6 | display: grid;
7 | grid-template-columns: 1fr 500px;
8 | height: 50px;
9 | width: 100%;
10 | margin-top: 42px;
11 | `
12 |
13 | const Img = styled.img`
14 | width: 128px;
15 | margin-left: 90px;
16 | `
17 |
18 | const Menu = styled.div`
19 | display: grid;
20 | grid-template-columns: repeat(4, 110px);
21 | margin-right: 5%;
22 | text-align: center;
23 | justify-content: space-around;
24 |
25 | a {
26 | font-size: 20px;
27 | color: white !important;
28 | text-decoration: none;
29 | }
30 | `
31 |
32 | const Nav = () => {
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | Features
40 | About
41 | Log in
42 | Sign up
43 |
44 |
45 | )
46 | }
47 |
48 | export default Nav
49 |
--------------------------------------------------------------------------------
/client/src/components/icons/DeleteIcon.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | const DeleteIcon = props => {
5 | const { width, height } = props
6 | return (
7 |
15 |
16 |
17 |
21 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | DeleteIcon.propTypes = {
33 | height: PropTypes.string,
34 | width: PropTypes.string
35 | }
36 |
37 | export default DeleteIcon
38 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/FooterContent.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 |
4 | import ButtonCTA from "./Button"
5 |
6 | const ContentContainer = styled.div`
7 | display: flex;
8 | flex-direction: row;
9 | background-image: url(./images/hikerscontent2.png);
10 | background-size: cover;
11 | justify-content: center;
12 | height: 60vh;
13 | width: 100vw;
14 | `
15 |
16 | const BrandedContent = styled.div`
17 | display: flex;
18 | flex-direction: column;
19 | align-self: center;
20 | justify-content: center;
21 | align-items: center;
22 |
23 | h3 {
24 | color: white !important;
25 | text-shadow: 1px 2px 5px rgba(0, 0, 0, 0.75);
26 | }
27 |
28 | a {
29 | color: white !important;
30 | }
31 |
32 | .accent {
33 | color: #f26a21 !important;
34 | font-weight: bold;
35 | }
36 | `
37 |
38 | const FooterContent = () => (
39 |
40 |
41 |
42 | Explore without boundaries.
43 |
44 |
45 |
46 |
47 | )
48 |
49 | export default FooterContent
50 |
--------------------------------------------------------------------------------
/client/src/styles/CheckoutForm.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { media } from "./theme/mixins"
3 |
4 | export const CheckoutFormStyles = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | margin: 30px auto 0;
8 | width: 400px;
9 |
10 | input {
11 | margin-bottom: 10px;
12 | border: 1px solid #d1d5da;
13 | font-size: 0.95rem;
14 | }
15 |
16 | .stripe-card-input {
17 | height: 38px;
18 | margin-bottom: 10px;
19 | }
20 |
21 | .StripeElement {
22 | padding: 0.6rem 0.6rem;
23 | border-radius: 0.375rem;
24 | border: 1px solid #d1d5da;
25 | background-color: white;
26 | color: #f6f6f6;
27 | }
28 |
29 | .StripeElement--focus {
30 | outline: none;
31 | border-color: #1e306e;
32 | box-shadow: 0 0 0 1px #1e306e;
33 | }
34 |
35 | .form-city-state {
36 | display: grid;
37 | grid-template-columns: 2fr 1fr 1fr;
38 | grid-gap: 10px;
39 | width: 100%;
40 |
41 | input {
42 | width: inherit;
43 | }
44 | }
45 |
46 | ${media.phone`
47 | width: 300px;
48 |
49 | .input-button {
50 | width: 300px;
51 | float: none;
52 | margin: 10px auto;
53 | display: flex;
54 | justify-content: center;
55 | align-items: center;
56 | }
57 | `}
58 | `
59 |
--------------------------------------------------------------------------------
/client/src/components/icons/ChevronSvg.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 | import PropTypes from "prop-types"
4 |
5 | const ChevronSvg = ({
6 | width = "1rem",
7 | height = "1rem",
8 | fill = "currentColor",
9 | transform
10 | }) => {
11 | const Icon = styled.span`
12 | width: ${width};
13 | height: ${height};
14 | fill: ${fill};
15 | /* transform: ${transform}; */
16 |
17 | position: relative;
18 | display: inline-block;
19 | vertical-align: middle;
20 | cursor: pointer;
21 | align-self: center;
22 | margin-top: 1px;
23 |
24 | svg {
25 | position: absolute;
26 | top: 1.5px;
27 | left: 1px;
28 | width: auto;
29 | height: 100%;
30 | transform: ${transform};
31 | }
32 | `
33 |
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | ChevronSvg.propTypes = {
45 | height: PropTypes.string,
46 | width: PropTypes.string,
47 | fill: PropTypes.string,
48 | transform: PropTypes.string
49 | }
50 |
51 | export default ChevronSvg
52 |
--------------------------------------------------------------------------------
/client/src/components/icons/EditSvg.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | const EditIcon = ({ width = "20px", height = "20px" }) => {
5 | return (
6 |
14 |
15 |
16 |
17 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | EditIcon.propTypes = {
33 | width: PropTypes.string,
34 | height: PropTypes.string
35 | }
36 |
37 | export default EditIcon
38 |
--------------------------------------------------------------------------------
/client/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, combineReducers } from "redux"
2 | import { connectRouter, routerMiddleware } from "connected-react-router"
3 | import { createBrowserHistory } from "history"
4 | import { composeWithDevTools } from "redux-devtools-extension"
5 | import thunk from "redux-thunk"
6 | import logger from "redux-logger"
7 |
8 | import { authReducer } from "./redux/reducers/auth"
9 | import { tripReducer } from "./redux/reducers/trips"
10 | import { billingReducer } from "./redux/reducers/billing"
11 | import { modalReducer } from "./redux/reducers/modal"
12 | import { settingsReducer } from "./redux/reducers/settings"
13 | import { navigationReducer } from "./redux/reducers/navigation"
14 |
15 | export const history = createBrowserHistory()
16 |
17 | const composeEnhancers = composeWithDevTools({ trace: true })
18 | const middleware = [thunk, logger, routerMiddleware(history)]
19 |
20 | const createRootReducer = history =>
21 | combineReducers({
22 | auth: authReducer,
23 | trips: tripReducer,
24 | billing: billingReducer,
25 | modal: modalReducer,
26 | settings: settingsReducer,
27 | router: connectRouter(history),
28 | navigation: navigationReducer
29 | })
30 |
31 | export const store = createStore(
32 | createRootReducer(history),
33 | composeEnhancers(applyMiddleware(...middleware))
34 | )
35 |
--------------------------------------------------------------------------------
/client/src/components/icons/UserSvg.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 | import PropTypes from "prop-types"
4 | import { theme } from "../../styles/theme/variables"
5 |
6 | const UserSvg = ({ height, width }) => {
7 | const Icon = styled.span`
8 | display: inline-block;
9 | cursor: pointer;
10 | height: ${height * 2}rem;
11 | width: ${width * 2}rem;
12 | text-align: center;
13 |
14 | svg {
15 | height: ${height}rem;
16 | width: ${width}rem;
17 | fill: ${theme.secondaryDark};
18 | margin-top: 11px;
19 | }
20 | `
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | UserSvg.propTypes = {
34 | height: PropTypes.string,
35 | width: PropTypes.string
36 | }
37 |
38 | export default UserSvg
39 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/Plans.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 |
4 | import { media } from "../../styles/theme/mixins"
5 | import PlansCard from "./PlansCard"
6 |
7 | const Wrapper = styled.div`
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: center;
11 | align-items: center;
12 | height: 100vh;
13 | width: 100%;
14 | background: url(/images/hikerscontent.png);
15 | background-size: cover;
16 |
17 | h1 {
18 | color: white;
19 | text-align: left;
20 | text-shadow: 1px 2px 5px rgba(0, 0, 0, 0.75);
21 | font-size: 2.5rem;
22 | font-weight: 600;
23 | white-space: nowrap;
24 | overflow: visible;
25 | }
26 |
27 | ${media.tablet`
28 | height: 100%;
29 | padding: 10%;
30 |
31 | h1 {
32 | font-size: 1.75rem;
33 | }
34 | `}
35 | `
36 |
37 | const Cards = styled.div`
38 | display: grid;
39 | grid-template-columns: 1fr 1fr;
40 | grid-gap: 5%;
41 | margin: 5% 0;
42 |
43 | ${media.tablet`
44 | grid-template-columns: 1fr;
45 | grid-template-rows: 1fr 1fr;
46 | `}
47 | `
48 |
49 | const Plans = () => (
50 |
51 | Choose the right plan for you
52 |
53 |
54 |
55 |
56 |
57 | )
58 |
59 | export default Plans
60 |
--------------------------------------------------------------------------------
/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 | Please include a summary of the change and a link to which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
4 |
5 | Fixes # (issue)
6 |
7 | ## Type of change
8 |
9 | Please delete options that are not relevant.
10 |
11 | - [ ] Bug fix (non-breaking change which fixes an issue)
12 | - [ ] New feature (non-breaking change which adds functionality)
13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
14 | - [ ] This change requires a documentation update
15 |
16 | # How Has This Been Tested?
17 |
18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
19 |
20 | - [ ] Test A
21 | - [ ] Test B
22 |
23 | # Checklist:
24 |
25 | - [ ] My code follows the style guidelines of this project
26 | - [ ] I have performed a self-review of my own code
27 | - [ ] My code has been reviewed by at least one peer
28 | - [ ] I have commented my code, particularly in hard-to-understand areas
29 | - [ ] I have made corresponding changes to the documentation
30 | - [ ] My changes generate no new warnings
31 | - [ ] I have added tests that prove my fix is effective or that my feature works
32 | - [ ] New and existing unit tests pass locally with my changes
33 |
--------------------------------------------------------------------------------
/client/src/components/icons/GitHubSvg.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 | import PropTypes from "prop-types"
4 |
5 | const GitHubSvg = ({ width = "32px", height = "32px" }) => {
6 | const Icon = styled.a`
7 | svg {
8 | width: ${width};
9 | height: ${height};
10 | stroke: none;
11 | fill: rgba(0, 0, 0, 0.3);
12 | }
13 | `
14 |
15 | return (
16 |
21 |
22 |
30 |
31 |
32 | )
33 | }
34 |
35 | GitHubSvg.propTypes = {
36 | width: PropTypes.string,
37 | height: PropTypes.string
38 | }
39 |
40 | export default GitHubSvg
41 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import styled from "styled-components"
3 | import axios from "axios"
4 | import { SERVER_URI } from "../../config"
5 |
6 | import MobileMenu from "./MobileMenu"
7 | import Hero from "./Hero"
8 | import Features from "./Features"
9 | import Plans from "./Plans"
10 | import FooterContent from "./FooterContent"
11 | import Footer from "./Footer"
12 | import { fontDeclarations } from "../../styles/theme/mixins"
13 |
14 | const LandingPageContainer = styled.div`
15 | overflow: auto;
16 | height: 100%;
17 | ${fontDeclarations}
18 | font-family: Wals, sans-serif;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 |
22 | .bm-item-list {
23 | padding: 40px 20px;
24 | }
25 |
26 | .bm-item {
27 | padding: 10px;
28 | font-size: 2rem;
29 | font-weight: 600;
30 | color: ${({ theme }) => theme.primary};
31 | outline: none;
32 | }
33 |
34 | .bm-overlay {
35 | background: none !important;
36 | }
37 | `
38 |
39 | class LandingPage extends Component {
40 | componentDidMount() {
41 | // WAKE UP HEROKU SERVER ON INITIAL PAGE LOAD
42 | axios.get(`${SERVER_URI}`)
43 | }
44 | render() {
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | )
55 | }
56 | }
57 |
58 | export default LandingPage
59 |
--------------------------------------------------------------------------------
/server/src/api/resources/waypoint/waypoint.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 | const Schema = mongoose.Schema
3 | const ObjectId = Schema.Types.ObjectId
4 |
5 | export const schema = {
6 | tripId: {
7 | type: ObjectId,
8 | ref: "Trip",
9 | required: [true]
10 | },
11 | order: {
12 | type: Number,
13 | required: [true]
14 | },
15 | name: {
16 | type: String,
17 | required: [true, "Name for waypoint required."]
18 | },
19 | lat: {
20 | type: Number,
21 | require: [true]
22 | },
23 | lon: {
24 | type: Number,
25 | require: [true]
26 | },
27 | start: {
28 | type: Date,
29 | default: Date.now
30 | },
31 | end: {
32 | type: Date,
33 | required: [true, "End date and time required"]
34 | },
35 | complete: {
36 | type: Boolean,
37 | default: false
38 | },
39 | timeComplete: {
40 | type: Date
41 | }
42 | }
43 |
44 | const waypointSchema = new Schema(schema, { timestamps: true })
45 |
46 | waypointSchema.set("toJSON", {
47 | transform: function(doc, ret) {
48 | let retJson = {
49 | id: ret._id,
50 | tripId: ret.tripId,
51 | order: ret.order,
52 | name: ret.name,
53 | lat: ret.lat,
54 | lon: ret.lon,
55 | start: ret.start,
56 | end: ret.end,
57 | complete: ret.complete,
58 | timeComplete: ret.timeComplete
59 | }
60 | return retJson
61 | }
62 | })
63 |
64 | mongoose.set("useCreateIndex", true)
65 | mongoose.set("useFindAndModify", false)
66 | export const Waypoint = mongoose.model("Waypoint", waypointSchema, "waypoints")
67 |
--------------------------------------------------------------------------------
/client/src/styles/Banner.styles.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes, css } from "styled-components"
2 | import { media } from "./theme/mixins"
3 |
4 | // Seconds btwn animations / new tagline generation:
5 |
6 | const animation = keyframes`
7 | 0% { opacity: 0; }
8 | 20% { opacity: 1; }
9 | 70% { opacity: 1; }
10 | 100% { opacity: 0; }
11 | `
12 |
13 | const animationRule = css`
14 | ${animation} ${props => props.seconds}s infinite ease;
15 | `
16 |
17 | export const Banner = styled.div`
18 | /* Apply animations */
19 | .banner-rotating-tagline {
20 | animation: ${animationRule};
21 | animation-delay: 0s;
22 | opacity: 1;
23 | }
24 |
25 | .landing-page-banner {
26 | height: 60px;
27 | ${media.tablet`height: 70px;`}
28 |
29 | color: ${props => props.theme.white};
30 | background-color: ${props => props.theme.secondaryDark};
31 | /* background-color: #0e153f; */
32 | width: 100%;
33 | display: flex;
34 | align-items: center;
35 | justify-content: flex-start;
36 | flex: 1 100%;
37 | padding-left: 10%;
38 | ${media.phone`padding-left: 0.8rem`};
39 |
40 | span {
41 | font-weight: 400;
42 | font-size: 1.125rem;
43 | }
44 | span.banner-title {
45 | color: ${props => props.theme.white};
46 | }
47 | span.banner-app-name {
48 | color: ${props => props.theme.white};
49 | font-size: 1.125rem;
50 | ${media.phone`font-size: .8rem`};
51 | }
52 | span.banner-rotating-tagline {
53 | color: ${props => props.theme.primaryLight};
54 | font-style: italic;
55 | ${media.phone`font-size: .8rem`};
56 | }
57 | }
58 | `
59 |
--------------------------------------------------------------------------------
/client/src/components/Billing/index.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { BillingStyles } from "../../styles/Billing.styles"
3 | import { connect } from "react-redux"
4 | import PropTypes from "prop-types"
5 |
6 | import { retrieveInvoices } from "../../redux/actions/billing"
7 | import Pending from "./Pending"
8 | import Invoices from "./Invoices"
9 | import AccountType from "./AccountType"
10 | import { UserPropTypes } from "../propTypes"
11 |
12 | class Billing extends React.Component {
13 | componentDidMount() {
14 | const { customerId, subscribeId } = this.props.user
15 |
16 | if (subscribeId && customerId) {
17 | this.props.retrieveInvoices(customerId, subscribeId)
18 | }
19 | }
20 |
21 | componentDidUpdate(prevProps) {
22 | const { retrieveInvoices, user } = this.props
23 |
24 | if (!prevProps.user.subscribed && user.subscribed && user.subscribeId) {
25 | retrieveInvoices(user.customerId, user.subscribeId)
26 | }
27 | }
28 |
29 | render() {
30 | const { invoices, isPending } = this.props
31 |
32 | return (
33 |
34 | {invoices && }
35 | { }
36 | {isPending && }
37 |
38 | )
39 | }
40 | }
41 |
42 | Billing.propTypes = {
43 | invoices: PropTypes.array,
44 | isPending: PropTypes.bool.isRequired,
45 | retrieveInvoices: PropTypes.func,
46 | user: UserPropTypes
47 | }
48 |
49 | const mapStateToProps = ({ billing, auth }) => ({
50 | invoices: billing.invoices,
51 | isPending: billing.pending,
52 | user: auth.user
53 | })
54 |
55 | export default connect(
56 | mapStateToProps,
57 | { retrieveInvoices }
58 | )(Billing)
59 |
--------------------------------------------------------------------------------
/client/src/components/PublicTripCard.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { connect } from "react-redux"
3 | import { Link } from "react-router-dom"
4 | import moment from "moment"
5 | import PropTypes from "prop-types"
6 | import { TripPropTypes } from "./propTypes"
7 |
8 | import { MAPS_KEY } from "../config"
9 |
10 | import TripCardLoader from "./TripCardLoader"
11 | import { togglePublic } from "../redux/actions/trips"
12 |
13 | const Card = ({ trip }) => (
14 | <>
15 |
16 |
17 |
25 |
26 |
27 |
28 |
{trip.name}
29 |
Start: {moment(trip.start).format("LL")}
30 |
End: {moment(trip.end).format("LL")}
31 |
32 |
33 | >
34 | )
35 |
36 | const PublicTripCard = props => (
37 |
38 | {props.loading ? : }
39 |
40 | )
41 |
42 | PublicTripCard.propTypes = {
43 | loading: PropTypes.bool.isRequired,
44 | trip: TripPropTypes,
45 | userId: PropTypes.string
46 | }
47 |
48 | const mapStateToProps = ({ auth, router, trips }) => ({
49 | userId: auth.user.id,
50 | isPublicTripRoute: router.location.pathname === "/app/trips/explore",
51 |
52 | loading: trips.pending
53 | })
54 |
55 | export default connect(
56 | mapStateToProps,
57 | { togglePublic }
58 | )(PublicTripCard)
59 |
--------------------------------------------------------------------------------
/client/src/components/pages/Pages.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Switch } from "react-router-dom"
3 |
4 | import AppContainer from "../AppContainer"
5 | import Register from "../Register"
6 | import Login from "../Login"
7 |
8 | import CustomRoute from "../../utils/CustomRoute"
9 | import RecoverPassword from "../forms/RecoverPassword"
10 | import UpdatePassword from "../forms/UpdatePassword"
11 |
12 | import { MatchPropTypes } from "../propTypes"
13 |
14 | const UpdatePasswordForm = ({ match }) => (
15 |
16 | )
17 |
18 | const pagesRoutes = [
19 | {
20 | path: "/register",
21 | name: "Register",
22 | component: Register
23 | },
24 | {
25 | path: "/login",
26 | name: "Login",
27 | component: Login
28 | },
29 | {
30 | path: "/password/recover",
31 | name: "RecoverPassword",
32 | component: RecoverPassword
33 | },
34 | {
35 | path: "/password/reset/:userId/:token",
36 | name: "UpdatePassword",
37 | component: UpdatePasswordForm
38 | }
39 | ]
40 |
41 | const Pages = ({ match }) => {
42 | return (
43 |
44 |
45 | {pagesRoutes.map(({ path, ...rest }, idx) => {
46 | // Normalize match.path to remove extra / at beginning if mounting Pages on root route:
47 | const pathname = match.path === "/" ? path : match.path + path
48 | return
49 | })}
50 |
51 |
52 | )
53 | }
54 |
55 | Pages.propTypes = {
56 | match: MatchPropTypes
57 | }
58 |
59 | UpdatePasswordForm.propTypes = {
60 | match: MatchPropTypes
61 | }
62 |
63 | export default Pages
64 |
--------------------------------------------------------------------------------
/client/src/components/Banner.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import * as s from "../styles/Banner.styles"
5 | import { makeTaglineIterator } from "../utils"
6 |
7 | class Banner extends Component {
8 | state = {
9 | taglines: [
10 | "Built for adventures.",
11 | "The safest way to explore your world.",
12 | "Get outside and get out of your comfort zone."
13 | ],
14 | tagline: ""
15 | }
16 |
17 | taglineGenerator = makeTaglineIterator(this.state.taglines)
18 | interval = null
19 |
20 | componentDidMount() {
21 | const { seconds } = this.props
22 | this.setState({ tagline: this.taglineGenerator.next().value })
23 |
24 | // Then get a new tagline every n seconds:
25 | this.interval = setInterval(() => {
26 | this.setState({
27 | tagline: this.taglineGenerator.next().value
28 | })
29 | }, `${seconds}000`)
30 | }
31 |
32 | componentWillUnmount() {
33 | // Stop getting taglines!
34 | clearInterval(this.interval)
35 | }
36 |
37 | render() {
38 | const { seconds } = this.props
39 | const { tagline } = this.state
40 | return (
41 |
42 |
43 |
44 | bkwds.
45 |
46 | {" ... "}
47 | {tagline.toLowerCase()}
48 |
49 |
50 |
51 |
52 | )
53 | }
54 | }
55 |
56 | Banner.propTypes = {
57 | seconds: PropTypes.number.isRequired
58 | }
59 |
60 | Banner.defaultProps = {
61 | seconds: 9
62 | }
63 |
64 | export default Banner
65 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "bootstrap": "^4.1.3",
8 | "cloudinary": "^1.13.2",
9 | "connected-react-router": "6.0.0",
10 | "d3": "5.8.0",
11 | "firebase": "^5.7.3",
12 | "formik": "1.4.1",
13 | "history": "4.7.2",
14 | "jwt-decode": "2.2.0",
15 | "moment": "^2.23.0",
16 | "progress-tracker": "^1.4.0",
17 | "prop-types": "15.6.2",
18 | "react": "^16.6.3",
19 | "react-accessible-accordion": "^2.4.5",
20 | "react-animated-burgers": "^1.2.6",
21 | "react-burger-menu": "^2.6.3",
22 | "react-content-loader": "^4.0.0",
23 | "react-copy-to-clipboard": "^5.0.1",
24 | "react-dates": "^18.3.1",
25 | "react-dom": "^16.6.3",
26 | "react-icons": "^3.3.0",
27 | "react-image-lightbox": "^5.1.0",
28 | "react-redux": "6.0.0",
29 | "react-router-dom": "^4.3.1",
30 | "react-step-progress-bar": "^1.0.3",
31 | "react-stripe-elements": "^2.0.1",
32 | "react-toastify": "^4.5.2",
33 | "reactstrap": "^6.5.0",
34 | "redux": "4.0.1",
35 | "redux-logger": "3.0.6",
36 | "redux-thunk": "2.3.0",
37 | "scriptly": "^0.0.8",
38 | "styled-components": "4.1.3"
39 | },
40 | "devDependencies": {
41 | "react-scripts": "2.1.1",
42 | "redux-devtools-extension": "2.13.7"
43 | },
44 | "scripts": {
45 | "start": "react-scripts start",
46 | "build": "react-scripts build",
47 | "test": "react-scripts test",
48 | "eject": "react-scripts eject",
49 | "lint": "eslint \"./**/*.js\""
50 | },
51 | "eslintConfig": {
52 | "extends": "react-app"
53 | },
54 | "browserslist": [
55 | ">0.2%",
56 | "not dead",
57 | "not ie <= 11",
58 | "not op_mini all"
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/client/src/components/Billing/Invoices.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import moment from "moment"
3 | import styled from "styled-components"
4 | import PropTypes from "prop-types"
5 |
6 | import { media } from "../../styles/theme/mixins"
7 |
8 | const InvoicesContainer = styled.div`
9 | display: flex;
10 | flex-direction: column;
11 | width: 100%;
12 | max-width: 525px;
13 | margin-bottom: 30px;
14 |
15 | ${media.phone`
16 | max-width: 300px;
17 | `}
18 | `
19 |
20 | const TableWrapper = styled.div`
21 | border: 1px solid #d1d5da;
22 | border-radius: 3px;
23 | box-shadow: 0px 8px 24px rgba(13, 13, 18, 0.04);
24 |
25 | th {
26 | background-color: #fafbfc;
27 | border-bottom: 1px solid #eaecef;
28 | padding: 9px;
29 | }
30 |
31 | td {
32 | border-bottom: 1px solid #eaecef;
33 | padding: 9px;
34 | vertical-align: top;
35 | }
36 | `
37 |
38 | const Invoices = ({ invoices }) => (
39 |
40 | Payment history
41 |
42 |
43 |
44 |
45 | Service
46 | Period
47 |
48 |
49 |
50 | {invoices.map(({ id, lines }) => (
51 |
52 | {lines.data[0].description}
53 |
54 | {moment.unix(lines.data[0].period.start).format("YYYY-MM-DD")}
55 | {" to "}
56 | {moment.unix(lines.data[0].period.end).format("YYYY-MM-DD")}
57 |
58 |
59 | ))}
60 |
61 |
62 |
63 |
64 | )
65 |
66 | Invoices.propTypes = {
67 | invoices: PropTypes.array.isRequired
68 | }
69 |
70 | export default Invoices
71 |
--------------------------------------------------------------------------------
/client/src/components/Maps/Autocomplete.js:
--------------------------------------------------------------------------------
1 | import { Component } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | class Autocomplete extends Component {
5 | state = {
6 | location: {
7 | lat: null,
8 | lng: null
9 | },
10 | viewport: null,
11 | formattedAddress: null
12 | }
13 |
14 | componentDidMount() {
15 | const { google, inputRef, getFormattedAddress } = this.props
16 | this.autocomplete = new google.maps.places.Autocomplete(inputRef.current)
17 | this.autocomplete.setFields(["geometry", "formatted_address"])
18 | this.listener = this.autocomplete.addListener("place_changed", () => {
19 | const place = this.autocomplete.getPlace()
20 | if (!place.geometry) return
21 | if (!place.geometry.location) return
22 | const { lat, lng } = place.geometry.location
23 |
24 | getFormattedAddress(place.formatted_address)
25 |
26 | if (window.map && lat && lng) {
27 | window.map.setCenter({
28 | lat: lat(),
29 | lng: lng()
30 | })
31 | }
32 |
33 | this.setState({
34 | location: { lat: lat(), lng: lng() },
35 | viewport: place.geometry.viewport,
36 | formattedAddress: place.formatted_address
37 | })
38 | })
39 | }
40 |
41 | componentWillUnmount() {
42 | this.props.google.maps.event.clearInstanceListeners(this.listener)
43 | }
44 |
45 | render() {
46 | return this.props.children(this.state)
47 | }
48 | }
49 |
50 | Autocomplete.propTypes = {
51 | google: PropTypes.object.isRequired,
52 | inputRef: PropTypes.shape({
53 | current: PropTypes.instanceOf(HTMLInputElement)
54 | }).isRequired,
55 | map: PropTypes.object,
56 | children: PropTypes.func.isRequired,
57 | getFormattedAddress: PropTypes.func
58 | }
59 |
60 | export default Autocomplete
61 |
--------------------------------------------------------------------------------
/client/src/styles/Sidebar.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { media } from "../styles/theme/mixins"
3 |
4 | export const SidebarStyles = styled.div`
5 | position: absolute;
6 | z-index: 7;
7 |
8 | .sidebar-links {
9 | width: 50px;
10 | background: transparent;
11 | /* Sidebar open & close transition (Desktop) */
12 | transition: visibility, width ease-in-out 0.3s;
13 | }
14 |
15 | button {
16 | width: 100%;
17 | border-radius: 0;
18 | display: flex;
19 | justify-content: flex-start;
20 | align-items: center;
21 | padding: 0;
22 | border: none;
23 | margin-top: 2px;
24 |
25 | i {
26 | width: 26px;
27 | }
28 |
29 | a:first-child {
30 | padding: 6px 12px;
31 | min-width: 50px;
32 | }
33 |
34 | a:last-child {
35 | white-space: nowrap;
36 | opacity: 0;
37 | width: 0;
38 | padding: 6px 0;
39 | width: 100%;
40 | text-align: left;
41 | display: ${props => (props.isSidebarOpen ? "block" : "none")};
42 | }
43 | }
44 |
45 | .open {
46 | width: ${props => `${props.theme.sidebarWidth}px`};
47 | a:last-child {
48 | opacity: 1;
49 | }
50 | }
51 |
52 | /* MEDIA QUERIES */
53 | ${media.tablet`
54 | height: ${props => (props.isSidebarOpen ? "100vh" : "inherit")};
55 | min-width: ${props => (props.isSidebarOpen ? "100vw" : "inherit")};
56 | background: rgba(0, 0, 0, 0.5);
57 | z-index: ${props => (props.isSidebarOpen ? 7 : -1)}
58 |
59 | .sidebar-links {
60 | opacity: ${props => (props.isSidebarOpen ? 1 : 0)};
61 | /* Sidebar open & close transition (Tablet & Phone) */
62 | transition: opacity ease-in-out 0.3s;
63 | width: 100%;
64 | button {
65 | height: 54px;
66 | a {
67 | font-size: 1.35rem;
68 | }
69 | }
70 | }
71 | `}
72 | `
73 |
--------------------------------------------------------------------------------
/client/src/styles/AddTripButton.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const AddTripButtonStyles = styled.div`
4 | margin: 0 10px 20px;
5 |
6 | .add-trip-card-wrapper {
7 | background: ${props => props.theme.white};
8 | border-radius: 0.25rem;
9 | }
10 |
11 | .add-trip-card-container {
12 | height: 400px;
13 | width: 380px;
14 | max-height: 100%;
15 | max-width: 100%;
16 | box-shadow: rgba(0, 0, 0, 0.05) 0px 0.0625rem 0px,
17 | rgba(0, 0, 5, 0.1) 0px 0.0625rem 0.125rem,
18 | rgba(0, 0, 0, 0.05) 0px 0.3125rem 0.9375rem;
19 | cursor: pointer;
20 | border-radius: 0.25rem;
21 | max-width: 25rem;
22 | overflow: hidden;
23 | transition: transform 0.22s ease-out 0s, box-shadow;
24 |
25 | &:hover {
26 | box-shadow: rgba(0, 0, 0, 0.05) 0px 0.3125rem 0.9375rem,
27 | rgba(0, 0, 0, 0.1) 0px 0.3125rem 0.3125rem,
28 | rgba(0, 0, 0, 0.05) 0px 0.125rem 0.3125rem;
29 | transform: translate3d(0px, -0.1875rem, 0px);
30 | }
31 |
32 | @media (max-width: 55.875em) {
33 | height: 300px;
34 | width: 280px;
35 | }
36 | }
37 |
38 | .add-trip-card-link {
39 | position: relative;
40 | text-decoration: none;
41 | display: flex;
42 | flex-direction: column;
43 | align-items: center;
44 | text-align: center;
45 | justify-content: center;
46 | padding: 5rem 2.5rem;
47 | height: 100%;
48 | width: 100%;
49 | max-width: 380px;
50 | color: rgba(179, 179, 179, 0.75);
51 | &:hover {
52 | h2,
53 | span {
54 | color: ${props => props.theme.midGray};
55 | }
56 | }
57 | }
58 |
59 | h2 {
60 | font-size: 2.25rem;
61 | padding: 0;
62 | margin: 0;
63 |
64 | @media (max-width: 55.875em) {
65 | font-size: 2rem;
66 | }
67 | }
68 | span {
69 | font-size: 3.5rem;
70 | margin-top: -0.25rem;
71 | }
72 | `
73 |
--------------------------------------------------------------------------------
/client/src/components/propTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types"
2 |
3 | export const TripPropTypes = PropTypes.shape({
4 | end: PropTypes.string.isRequired,
5 | id: PropTypes.string.isRequired,
6 | inProgress: PropTypes.bool.isRequired,
7 | isArchived: PropTypes.bool.isRequired,
8 | lat: PropTypes.number,
9 | lon: PropTypes.number,
10 | name: PropTypes.string.isRequired,
11 | start: PropTypes.string.isRequired,
12 | userId: PropTypes.string.isRequired,
13 | waypoints: PropTypes.array.isRequired,
14 | tripPics: PropTypes.arrayOf(PropTypes.string).isRequired
15 | })
16 |
17 | export const getDefaultTripProps = overrides => {
18 | const defaults = {
19 | end: "",
20 | id: "",
21 | inProgress: false,
22 | isArchived: false,
23 | lat: 0,
24 | lon: 0,
25 | name: "",
26 | start: "",
27 | userId: "",
28 | waypoints: []
29 | }
30 | return Object.assign({}, defaults, overrides)
31 | }
32 |
33 | export const UserPropTypes = PropTypes.shape({
34 | coordinates: PropTypes.arrayOf(PropTypes.number),
35 | createdAt: PropTypes.string.isRequired,
36 | customerId: PropTypes.string,
37 | displayName: PropTypes.string,
38 | email: PropTypes.string.isRequired,
39 | formattedAddress: PropTypes.string,
40 | id: PropTypes.string.isRequired,
41 | lastLogin: PropTypes.string.isRequired,
42 | loginCount: PropTypes.number.isRequired,
43 | picture: PropTypes.string,
44 | subDate: PropTypes.string,
45 | subscribed: PropTypes.bool.isRequired,
46 | subscribeId: PropTypes.string,
47 | token: PropTypes.string,
48 | trips: PropTypes.arrayOf(PropTypes.any),
49 | type: PropTypes.string.isRequired,
50 | updatedAt: PropTypes.string.isRequired
51 | })
52 |
53 | export const MatchPropTypes = PropTypes.shape({
54 | isExact: PropTypes.bool.isRequired,
55 | params: PropTypes.object,
56 | path: PropTypes.string.isRequired,
57 | url: PropTypes.string.isRequired
58 | })
59 |
--------------------------------------------------------------------------------
/server/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack")
2 | const path = require("path")
3 | const nodeExternals = require("webpack-node-externals")
4 | const StartServerPlugin = require("start-server-webpack-plugin")
5 |
6 | require("dotenv").config()
7 |
8 | module.exports = {
9 | entry: ["webpack/hot/poll?1000", "./src/index"],
10 | mode: "development",
11 | watch: true,
12 | devtool: "sourcemap",
13 | target: "node",
14 | node: {
15 | __filename: true,
16 | __dirname: true
17 | },
18 | externals: [nodeExternals({ whitelist: ["webpack/hot/poll?1000"] })],
19 | module: {
20 | rules: [
21 | {
22 | test: /\.js?$/,
23 | use: [
24 | {
25 | loader: "babel-loader",
26 | options: {
27 | babelrc: false,
28 | presets: ["@babel/preset-env"],
29 | plugins: [
30 | "@babel/plugin-transform-regenerator",
31 | "@babel/plugin-transform-runtime",
32 | "@babel/plugin-proposal-function-bind"
33 | ]
34 | }
35 | }
36 | ],
37 | exclude: /node_modules/
38 | }
39 | ]
40 | },
41 | plugins: [
42 | new StartServerPlugin("server.js"),
43 | new webpack.NamedModulesPlugin(),
44 | new webpack.HotModuleReplacementPlugin(),
45 | new webpack.NoEmitOnErrorsPlugin(),
46 | // new webpack.DefinePlugin({
47 | // "process.env": JSON.stringify("server"),
48 | // "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
49 | // // 'process.env.PORT': JSON.stringify(process.env.PORT),
50 | // // "process.env.MONGO_URI": JSON.stringify(process.env.MONGO_URI)
51 | // }),
52 | new webpack.BannerPlugin({
53 | banner: 'require("source-map-support").install();',
54 | raw: true,
55 | entryOnly: false
56 | })
57 | ],
58 | output: { path: path.join(__dirname, "dist"), filename: "server.js" }
59 | }
60 |
--------------------------------------------------------------------------------
/server/src/api/resources/trip/trip.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 | const ObjectId = mongoose.Schema.Types.ObjectId
3 |
4 | export const schema = {
5 | userId: {
6 | type: ObjectId,
7 | ref: "User",
8 | required: [true]
9 | },
10 | name: {
11 | type: String,
12 | required: [true, "name is required."]
13 | },
14 | isArchived: {
15 | type: Boolean,
16 | default: false,
17 | required: [true]
18 | },
19 | start: {
20 | type: Date,
21 | required: [true]
22 | },
23 | end: {
24 | type: Date,
25 | required: [false]
26 | },
27 | lat: {
28 | type: Number,
29 | required: [true]
30 | },
31 | lon: {
32 | type: Number,
33 | required: [true]
34 | },
35 | image: {
36 | type: String
37 | },
38 | inProgress: {
39 | type: Boolean,
40 | default: false
41 | },
42 | timeLimit: {
43 | type: Number
44 | },
45 | complete: {
46 | type: Boolean,
47 | default: false
48 | },
49 | isPublic: {
50 | type: Boolean,
51 | default: false
52 | },
53 | waypoints: [{ type: ObjectId, ref: "Waypoint" }],
54 | tripPics: [{ type: String }]
55 | }
56 |
57 | const tripSchema = new mongoose.Schema(schema, { timestamps: true })
58 |
59 | tripSchema.set("toJSON", {
60 | transform: function(doc, ret) {
61 | let retJson = {
62 | id: ret._id,
63 | userId: ret.userId,
64 | name: ret.name,
65 | lat: ret.lat,
66 | lon: ret.lon,
67 | start: ret.start,
68 | end: ret.end,
69 | image: ret.image,
70 | isArchived: ret.isArchived,
71 | inProgress: ret.inProgress,
72 | complete: ret.complete,
73 | timeLimit: ret.timeLimit,
74 | isPublic: ret.isPublic,
75 | waypoints: ret.waypoints,
76 | tripPics: ret.tripPics
77 | }
78 | return retJson
79 | }
80 | })
81 |
82 | mongoose.set("useCreateIndex", true)
83 | mongoose.set("useFindAndModify", false)
84 | export const Trip = mongoose.model("Trip", tripSchema, "trips")
85 |
--------------------------------------------------------------------------------
/client/src/styles/Dropdown.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | import { boxShadowMixin, media } from "./theme/mixins"
4 |
5 | export const DropdownStyles = styled.div`
6 | /* BOOTSTRAP OVERRIDES: */
7 | margin-right: 0.75rem;
8 |
9 | .dropdown {
10 | ${media.phone`display: none !important;`}
11 |
12 | width: 240px;
13 | a:hover,
14 | button:hover {
15 | text-decoration: none;
16 | }
17 |
18 | .navbar-toggle {
19 | width: 100%;
20 | display: flex;
21 | justify-content: flex-end;
22 | height: 50px;
23 | padding: 0;
24 | }
25 |
26 | /* dropdown button AND nested dropdown items styles */
27 | button {
28 | background-color: transparent;
29 | color: ${props => props.theme.midGray};
30 | &:hover {
31 | color: ${props => props.theme.primary};
32 | }
33 | }
34 |
35 |
36 | .dropdown-menu {
37 | width: 75%;
38 | }
39 |
40 | /* DROPDOWN HEADER BUTTON STYLES */
41 | & > button {
42 | display: flex;
43 | flex: 1 100%;
44 | }
45 |
46 | & > div {
47 | left: 64px !important;
48 | margin: 0;
49 | padding: 0;
50 | ${boxShadowMixin}
51 | border: 0;
52 | border-radius: 0;
53 | }
54 |
55 | /* DROPDOWN LIST ITEM STYLES */
56 | button.dropdown-item {
57 | margin: 0;
58 | padding: 0;
59 | a {
60 | transition: padding-left 0.15s ease-in, color 0.15s ease-in;
61 | height: auto;
62 | padding: 9px 20px;
63 | font-size: 1rem;
64 | font-weight: 300;
65 |
66 | &:hover {
67 | padding-left: 1.5rem;
68 | }
69 | &:last-child {
70 | /* background-color: ${props => props.theme.primary};
71 | color: ${props => props.theme.white};
72 | font-weight: 400; */
73 | }
74 | }
75 | }
76 |
77 | .dropdown-divider {
78 | margin: 0;
79 | }
80 | }
81 | `
82 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backwood-tracker-backend",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "jest": {
7 | "setupTestFrameworkScriptFile": "/tests/setup.js",
8 | "verbose": true,
9 | "testMatch": [
10 | "/tests/modules/**"
11 | ],
12 | "testEnvironment": "node"
13 | },
14 | "scripts": {
15 | "start": "node dist/server.js",
16 | "dev": "webpack --config webpack.dev.js --colors --progress",
17 | "lint": "eslint \"./**/*.js\"",
18 | "test": "jest --runInBand --detectOpenHandles",
19 | "clean": "rm -rf dist/",
20 | "build": "yarn clean && webpack --config webpack.prod.js",
21 | "heroku-postbuild": "yarn build"
22 | },
23 | "engines": {
24 | "node": "10.x"
25 | },
26 | "dependencies": {
27 | "bcryptjs": "2.4.3",
28 | "cloudinary": "^1.13.2",
29 | "cors": "2.8.5",
30 | "dotenv": "^6.2.0",
31 | "express": "4.16.4",
32 | "jsonwebtoken": "^8.4.0",
33 | "lodash.merge": "4.6.1",
34 | "moment": "^2.24.0",
35 | "mongoose": "5.3.15",
36 | "nodemailer": "^5.1.1",
37 | "stripe": "^6.19.0",
38 | "twilio": "^3.27.1",
39 | "webpack-merge": "4.1.5"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "7.2.0",
43 | "@babel/plugin-proposal-function-bind": "7.2.0",
44 | "@babel/plugin-transform-regenerator": "7.0.0",
45 | "@babel/plugin-transform-runtime": "7.2.0",
46 | "@babel/preset-env": "7.2.0",
47 | "@babel/runtime": "7.2.0",
48 | "babel-cli": "^6.26.0",
49 | "babel-eslint": "^10.0.1",
50 | "babel-loader": "8.0.4",
51 | "babel-preset-env": "1.7.0",
52 | "babel-preset-stage-0": "6.24.1",
53 | "eslint": "^5.10.0",
54 | "jest": "^23.6.0",
55 | "raw-loader": "1.0.0",
56 | "source-map-support": "0.5.9",
57 | "start-server-webpack-plugin": "2.2.5",
58 | "supertest": "^3.3.0",
59 | "webpack": "4.27.1",
60 | "webpack-cli": "^3.2.1",
61 | "webpack-node-externals": "1.7.2"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import styled from "styled-components"
4 | import { withRouter } from "react-router-dom"
5 |
6 | import { scrollTo } from "../../utils"
7 |
8 | const Button = styled.div`
9 | display: flex;
10 | margin: ${({ margin }) => (margin ? "2rem" : 0)} auto;
11 | align-items: center;
12 | justify-content: center;
13 | background: #f26a21;
14 | width: ${({ width }) => width};
15 | height: ${({ height }) => height};
16 | border-radius: 12px;
17 | box-shadow: 0 0.3125rem 0.0625rem 0 rgba(0, 0, 0, 0.25),
18 | 0 0 0 0.0625rem rgba(255, 255, 255, 0.03),
19 | 0 0.0625rem 2px 0 rgba(0, 0, 0, 0.75),
20 | 0 0.0625rem 0.1875rem 0 rgba(0, 0, 0, 0.1);
21 |
22 | button {
23 | display: inline;
24 | margin: 0;
25 | padding: 0;
26 | background: transparent;
27 | border: none;
28 | cursor: pointer;
29 | text-decoration: none;
30 | }
31 |
32 | &:hover {
33 | background: #f9873b;
34 | transition: background 500ms;
35 | }
36 |
37 | h4 {
38 | margin: 0;
39 | color: white;
40 | font-size: 17px;
41 | font-weight: 500;
42 | letter-spacing: 1.5px;
43 | }
44 | `
45 |
46 | const ButtonCTA = ({ height, history, margin, text, to, width }) => {
47 | const navigate = () => (to === "features" ? scrollTo(to) : history.push(to))
48 |
49 | return (
50 |
51 |
52 | {text}
53 |
54 |
55 | )
56 | }
57 |
58 | ButtonCTA.defaultProps = {
59 | height: "50px",
60 | margin: true,
61 | width: "175px",
62 | text: "Learn more",
63 | to: "features"
64 | }
65 |
66 | ButtonCTA.propTypes = {
67 | height: PropTypes.string,
68 | history: PropTypes.object.isRequired,
69 | margin: PropTypes.bool,
70 | text: PropTypes.string,
71 | to: PropTypes.string,
72 | width: PropTypes.string
73 | }
74 |
75 | export default withRouter(ButtonCTA)
76 |
--------------------------------------------------------------------------------
/client/src/components/Maps/MobileMapPanel.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import styled from "styled-components"
4 |
5 | import { media } from "../../styles/theme/mixins"
6 |
7 | const MobileMapPanelStyles = styled.div`
8 | .mobile-trip-panel {
9 | display: none;
10 | }
11 |
12 | ${media.tablet`
13 | .mobile-trip-panel {
14 | display: block;
15 | }
16 |
17 | .mobile-panel {
18 | position: absolute;
19 | z-index: 6;
20 | display: flex;
21 | flex-direction: column;
22 | width: 50px;
23 | height: 100vh;
24 | overflow: hidden;
25 | background-color: ${props => props.theme.offWhite};
26 | box-shadow: 2px 2px 2px rgba(0,0,0,0.15);
27 |
28 | button {
29 | height: 50px;
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | border-radius: 0;
34 | margin: 0;
35 | }
36 | i {
37 | font-size: 1.5rem;
38 | color: ${props => props.theme.midGray};
39 | }
40 |
41 | }
42 | `}
43 |
44 | .btn-neutral {
45 | &:focus {
46 | background-color: ${props => props.theme.offWhite};
47 | border-color: ${props => props.theme.offWhite};
48 | color: midGray;
49 | }
50 | }
51 |
52 | .active-button {
53 | background-color: ${props => props.theme.primary};
54 | border-color: ${props => props.theme.primary};
55 | color: white;
56 | i {
57 | font-size: 1.5rem;
58 | color: white;
59 | }
60 | &:hover,
61 | &:focus {
62 | background-color: ${props => props.theme.primary};
63 | border-color: ${props => props.theme.primary};
64 | color: white;
65 | }
66 | }
67 | `
68 |
69 | const MobileMapPanel = ({ children }) => (
70 |
71 | {children}
72 |
73 | )
74 |
75 | MobileMapPanel.propTypes = {
76 | children: PropTypes.any
77 | }
78 |
79 | export default MobileMapPanel
80 |
--------------------------------------------------------------------------------
/client/src/components/Maps/SingleTrip/mapUtil.js:
--------------------------------------------------------------------------------
1 | // Returns distance in meters between two latlngs
2 | export const calcDistance = (fromLat, fromLng, toLat, toLng) => {
3 | return window.google.maps.geometry.spherical.computeDistanceBetween(
4 | new window.google.maps.LatLng(fromLat, fromLng),
5 | new window.google.maps.LatLng(toLat, toLng)
6 | )
7 | }
8 |
9 | export const calcTimeGap = (distance, velocity) => {
10 | return distance / (velocity * 60)
11 | }
12 |
13 | export const getElevations = latLngArr => {
14 | let elevations
15 | const elev_service = new window.google.maps.ElevationService()
16 | let G_LatLngs = latLngArr.map(latLng => {
17 | return window.google.maps.LatLng(latLng.lat, latLng.lng)
18 | })
19 |
20 | elev_service.getElevationForLocations(G_LatLngs, (results, status) => {
21 | if (status === "OK") {
22 | elevations = results
23 | } else {
24 | console.warn("Get elevation failure; status:", status)
25 | }
26 | return elevations
27 | })
28 | }
29 |
30 | export const getPathElevation = pathArr => {
31 | return new Promise((resolve, reject) => {
32 | window.elevation.getElevationAlongPath(
33 | {
34 | path: pathArr,
35 | samples: 128
36 | },
37 | (result, status) => {
38 | if (status === "OK") {
39 | let sum = 0
40 | result.forEach(elev => {
41 | sum += elev.elevation
42 | })
43 | resolve(sum / result.length)
44 | } else {
45 | reject(status)
46 | }
47 | }
48 | )
49 | })
50 | }
51 |
52 | export const calcTotalDistance = latLngArray => {
53 | return new Promise((resolve, reject) => {
54 | let G_LatLngs = latLngArray.map(latLng => {
55 | return new window.google.maps.LatLng(latLng.lat, latLng.lng)
56 | })
57 |
58 | let distance = window.google.maps.geometry.spherical.computeLength(
59 | G_LatLngs
60 | )
61 | if (distance === 0) {
62 | reject("Error")
63 | }
64 | resolve(distance)
65 | })
66 | }
67 |
--------------------------------------------------------------------------------
/client/src/components/icons/Puff.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | export const PuffIcon = props => {
5 | const { height, width } = props
6 |
7 | return (
8 |
15 |
16 |
17 |
27 |
37 |
38 |
39 |
49 |
59 |
60 |
61 |
62 | )
63 | }
64 |
65 | PuffIcon.propTypes = {
66 | height: PropTypes.string,
67 | width: PropTypes.string
68 | }
69 |
70 | export default PuffIcon
71 |
--------------------------------------------------------------------------------
/client/src/components/AppContainer.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { connect } from "react-redux"
3 | import styled from "styled-components"
4 | import { ToastContainer } from "react-toastify"
5 | import PropTypes from "prop-types"
6 |
7 | import "react-toastify/dist/ReactToastify.css"
8 |
9 | import { GlobalStyles } from "../styles/theme/GlobalStyles"
10 | import AppNav from "./AppNav"
11 | import Sidebar from "./Sidebar"
12 | import { media } from "../styles/theme/mixins"
13 | import { isProtectedPath } from "../utils"
14 |
15 | const Right = styled.div`
16 | overflow-y: scroll;
17 | background: ${props => props.theme.ghostWhite};
18 | width: 100%;
19 | padding-left: 50px;
20 | ${media.tablet`
21 | padding-left: 0;
22 | `};
23 | `
24 |
25 | const AppContainer = ({ pathname, children, isLoggedIn }) => {
26 | const authPaths = ["/register", "/login"]
27 | const showBreadcrumbs = !isProtectedPath(pathname, [...authPaths])
28 | const showSidebar = isLoggedIn && showBreadcrumbs
29 | const userOnAuthPath = authPaths.includes(pathname)
30 | const mainWrapperClassList = [
31 | "main-wrapper",
32 | userOnAuthPath ? "main-wrapper-auth" : null
33 | ]
34 |
35 | return (
36 | <>
37 |
38 |
39 |
40 | {showSidebar ? (
41 |
42 |
43 | {children}
44 |
45 | ) : (
46 |
{children}
47 | )}
48 |
49 |
50 | >
51 | )
52 | }
53 |
54 | AppContainer.propTypes = {
55 | children: PropTypes.element.isRequired,
56 | isLoggedIn: PropTypes.bool.isRequired,
57 | pathname: PropTypes.string.isRequired
58 | }
59 |
60 | const mapStateToProps = state => ({
61 | isLoggedIn: state.auth.isLoggedIn,
62 | pathname: state.router.location.pathname
63 | })
64 |
65 | export default connect(mapStateToProps)(AppContainer)
66 |
--------------------------------------------------------------------------------
/client/src/components/Trips.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { connect } from "react-redux"
3 | import PropTypes from "prop-types"
4 |
5 | import { getTripsArray } from "../utils/selectors"
6 | import { getTrips } from "../redux/actions/trips"
7 | import { TripPropTypes } from "./propTypes"
8 | import TripCard from "./TripCard"
9 | import AddTripButton from "./AddTripButton"
10 | import * as s from "../styles/TripCard.styles"
11 |
12 | class Trips extends Component {
13 | static propTypes = {
14 | getTrips: PropTypes.func.isRequired,
15 | loading: PropTypes.bool.isRequired,
16 | trips: PropTypes.arrayOf(TripPropTypes),
17 | userId: PropTypes.string
18 | }
19 |
20 | componentDidMount() {
21 | const { getTrips, userId } = this.props
22 | getTrips(userId)
23 | }
24 |
25 | renderPlaceholders = () =>
26 | this.props.trips
27 | .filter(trip => !trip.isArchived)
28 | .map((_, i) => )
29 |
30 | renderTrips = () => {
31 | const { loading, trips } = this.props
32 |
33 | return trips.map(
34 | trip =>
35 | trip.isArchived || (
36 |
42 | )
43 | )
44 | }
45 |
46 | render() {
47 | const { loading, trips } = this.props
48 |
49 | return (
50 |
51 |
52 |
53 |
57 | {loading ? this.renderPlaceholders() : this.renderTrips()}
58 |
59 |
60 |
61 | )
62 | }
63 | }
64 |
65 | const mapStateToProps = state => ({
66 | userId: state.auth.user.id,
67 | trips: getTripsArray(state),
68 | loading: state.trips.pending
69 | })
70 |
71 | export default connect(
72 | mapStateToProps,
73 | { getTrips }
74 | )(Trips)
75 |
--------------------------------------------------------------------------------
/server/src/api/resources/subscribe/subscribe.controller.js:
--------------------------------------------------------------------------------
1 | import * as userController from "../user/user.controller"
2 |
3 | export const subscribe = async (req, res, stripe) => {
4 | const { planId, source } = req.body
5 |
6 | try {
7 | const customer = await stripe.customers.create(
8 | { source: source.id },
9 | { api_key: process.env.STRIPE_KEY_SERVER_TEST }
10 | )
11 |
12 | const subscription = await stripe.subscriptions.create(
13 | {
14 | customer: customer.id,
15 | items: [{ plan: planId }]
16 | },
17 | { api_key: process.env.STRIPE_KEY_SERVER_TEST }
18 | )
19 |
20 | const updatedRequest = {
21 | ...req,
22 | body: {
23 | subscribed: true,
24 | subDate: Date.now(),
25 | customerId: customer.id,
26 | subscribeId: subscription.id
27 | }
28 | }
29 | return userController.updateUser(updatedRequest, res)
30 | } catch (error) {
31 | res.status(error.statusCode).send(error.message)
32 | }
33 | }
34 |
35 | export const cancel = async (req, res, stripe) => {
36 | const { subscribeId } = req.body
37 | try {
38 | await stripe.subscriptions.del(subscribeId, {
39 | api_key: process.env.STRIPE_KEY_SERVER_TEST
40 | })
41 |
42 | const updatedRequest = {
43 | ...req,
44 | body: {
45 | subscribed: false,
46 | subDate: "",
47 | customerId: null,
48 | subscribeId: null
49 | }
50 | }
51 | return userController.updateUser(updatedRequest, res)
52 | } catch (error) {
53 | return res.status(error.statusCode).send(error.message)
54 | }
55 | }
56 |
57 | export const retrieveInvoices = async (req, res, stripe) => {
58 | const { customerId, subscribeId } = req.body
59 | try {
60 | const result = await stripe.invoices.list(
61 | { customer: customerId, subscription: subscribeId },
62 | {
63 | api_key: process.env.STRIPE_KEY_SERVER_TEST
64 | }
65 | )
66 | return res.status(200).send(result.data)
67 | } catch (error) {
68 | return res.status(error.statusCode).send(error.message)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/server/tests/modules/testSubscribe.js:
--------------------------------------------------------------------------------
1 | import request from "supertest"
2 | import app from "../../src/server"
3 |
4 | import * as mock from "../mock"
5 |
6 | let userId
7 | let token
8 | let customerId
9 | let subscribeId
10 |
11 | describe("Test Subscribe and Cancel route", () => {
12 | beforeAll(async () => {
13 | const response = await request(app)
14 | .post("/api/login")
15 | .send({ email: mock.userOne.email, password: "testpass" })
16 |
17 | userId = response.body.user.id
18 | token = response.body.token
19 | })
20 |
21 | test("POST free users subscribe", async () => {
22 | const response = await request(app)
23 | .post(`/api/subscribe/${userId}`)
24 | .set("Authorization", `Bearer ${token}`)
25 | .send({
26 | planId: process.env.STRIPE_PLAN_ID_TEST,
27 | source: { id: "tok_visa" }
28 | })
29 |
30 | customerId = response.body.customerId
31 | subscribeId = response.body.subscribeId
32 |
33 | expect(response.statusCode).toBe(200)
34 | expect(response.body.subscribed).toEqual(true)
35 | expect(response.body.subscribeId).toBe.string
36 | })
37 |
38 | test("POST retrieve invoices", async () => {
39 | const response = await request(app)
40 | .post(`/api/subscribe/invoices`)
41 | .set("Authorization", `Bearer ${token}`)
42 | .send({ customerId, subscribeId })
43 |
44 | expect(response.statusCode).toBe(200)
45 | expect(response.body.length).toBeGreaterThan(0)
46 | })
47 |
48 | test("POST premium users cancel", async () => {
49 | const userResponse = await request(app)
50 | .get(`/api/users/${userId}`)
51 | .set("Authorization", `Bearer ${token}`)
52 |
53 | if (userResponse && userResponse.body && userResponse.body.subscribeId) {
54 | const response = await request(app)
55 | .post(`/api/subscribe/cancel/${userId}`)
56 | .set("Authorization", `Bearer ${token}`)
57 | .send({ subscribeId: userResponse.body.subscribeId })
58 |
59 | expect(response.statusCode).toBe(200)
60 | expect(response.body.subscribed).toEqual(false)
61 | }
62 | })
63 | })
64 |
--------------------------------------------------------------------------------
/server/tests/setup.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 | import dotenv from "dotenv"
3 |
4 | import * as mock from "./mock"
5 | import { User } from "../src/api/resources/user/user.model"
6 | import { Waypoint } from "../src/api/resources/waypoint/waypoint.model"
7 | import { Trip } from "../src/api/resources/trip/trip.model"
8 |
9 | dotenv.config()
10 | const options = {
11 | useNewUrlParser: true,
12 | reconnectTries: 1,
13 | reconnectInterval: 1000
14 | }
15 |
16 | beforeAll(async done => {
17 | if (mongoose.connection.readyState === 0) {
18 | await mongoose.connect(
19 | "mongodb://127.0.0.1/backwoods",
20 | options,
21 | err => {
22 | if (err) throw err
23 | }
24 | )
25 | }
26 | let user1 = new User(mock.userOne)
27 | let user2 = new User(mock.userTwo)
28 |
29 | let trip1 = new Trip(mock.tripOne)
30 | let trip2 = new Trip(mock.tripTwo)
31 | let pubTrip1 = new Trip(mock.publicTrip1)
32 | let pubTrip2 = new Trip(mock.publicTrip2)
33 | let way1 = new Waypoint(mock.waypointOne)
34 | let way2 = new Waypoint(mock.waypointTwo)
35 | let way4 = new Waypoint(mock.waypointFour)
36 | // Link user to trips
37 | user1.trips.push(trip1._id)
38 | user1.trips.push(pubTrip1._id)
39 | user1.trips.push(pubTrip2._id)
40 | user2.trips.push(trip2._id)
41 | trip1.userId = user1._id
42 | trip2.userId = user2._id
43 | pubTrip1.userId = user1._id
44 | pubTrip2.userId = user1._id
45 |
46 | // Link waypoints to trips
47 | trip1.waypoints = [way1._id, way2._id]
48 | way1.tripId = trip1._id
49 | way2.tripId = trip1._id
50 | way4.tripId = trip2._id
51 |
52 | await user1.save()
53 | await user2.save()
54 | await trip1.save()
55 | await trip2.save()
56 | await pubTrip1.save()
57 | await pubTrip2.save()
58 |
59 | await way1.save()
60 | await way2.save()
61 | await way4.save()
62 |
63 | return done()
64 | })
65 |
66 | afterAll(done => {
67 | const clearDB = () => {
68 | for (let i in mongoose.connection.collections) {
69 | mongoose.connection.collections[i].deleteMany()
70 | }
71 | return done()
72 | }
73 | clearDB()
74 | mongoose.disconnect(done)
75 | })
76 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
15 |
21 |
30 |
31 | bkwds.
32 |
33 |
34 | You need to enable JavaScript to run this app.
35 |
36 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/client/src/components/Breadcrumbs.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Link, withRouter } from "react-router-dom"
3 | import styled from "styled-components"
4 | import PropTypes from "prop-types"
5 | import Breadcrumb from "./Breadcrumb"
6 |
7 | const BreadcrumbsStyles = styled.div`
8 | background-color: ${props => props.theme.lighterGray};
9 | border-bottom: 1px solid rgba(0, 0, 0, 0.15);
10 |
11 | .current-path {
12 | cursor: default;
13 | color: ${props => props.theme.midGray};
14 | &:hover {
15 | text-decoration: none;
16 | }
17 | }
18 |
19 | ol {
20 | display: flex !important;
21 | flex-direction: row;
22 | flex-wrap: wrap;
23 | }
24 |
25 | li a {
26 | background-color: orange;
27 | color: purple;
28 | }
29 |
30 | li + li {
31 | margin-right: 10px;
32 | a {
33 | margin-left: 10px;
34 | }
35 | }
36 |
37 | /* li {
38 | &:not(:first-child) a::before {
39 | content: "\003E";
40 | }
41 | &:first-child {
42 | margin-right: 10px;
43 | }
44 | & + li {
45 | margin-right: 10px;
46 | a {
47 | margin-left: 10px;
48 | }
49 | }
50 | } */
51 | `
52 |
53 | const head = t => t[0]
54 | const tail = t => t.slice(1)
55 | const titlecase = s => head(s).toUpperCase() + tail(s)
56 | const splitPathname = pathname => tail(pathname.split("/"))
57 | const buildPath = pathsArray => index =>
58 | "/" + pathsArray.slice(0, index + 1).join("/")
59 |
60 | const Breadcrumbs = ({ location }) => {
61 | const paths = splitPathname(location.pathname)
62 | const buildPathByIndex = buildPath(paths)
63 |
64 | return (
65 |
66 |
67 |
68 | Home
69 |
70 | {paths.map((path, i, arr) => (
71 |
77 | ))}
78 |
79 |
80 | )
81 | }
82 |
83 | Breadcrumbs.propTypes = {
84 | location: PropTypes.string.isRequired
85 | }
86 |
87 | export default withRouter(Breadcrumbs)
88 |
--------------------------------------------------------------------------------
/client/src/components/ArchivedTrips.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { connect } from "react-redux"
3 | import PropTypes from "prop-types"
4 |
5 | import TripCard from "./TripCard"
6 | import * as s from "../styles/TripCard.styles"
7 | import { getTrips } from "../redux/actions/trips"
8 | import { getTripsArray } from "../utils/selectors"
9 | import { TripPropTypes } from "./propTypes"
10 |
11 | class ArchivedTrips extends Component {
12 | static propTypes = {
13 | getTrips: PropTypes.func.isRequired,
14 | loading: PropTypes.bool,
15 | trips: PropTypes.arrayOf(TripPropTypes),
16 | userId: PropTypes.string.isRequired
17 | }
18 |
19 | state = {
20 | archivedExists: false
21 | }
22 |
23 | componentDidMount() {
24 | const { getTrips, userId } = this.props
25 | getTrips(userId)
26 | }
27 |
28 | componentDidUpdate(prevProps, prevState) {
29 | const archivedExists = this.props.trips.some(trip => trip.isArchived)
30 | if (!prevState.archivedExists && archivedExists) {
31 | this.setState({ archivedExists })
32 | }
33 | }
34 |
35 | renderPlaceholders = () =>
36 | this.props.trips
37 | .filter(trip => trip.isArchived)
38 | .map((_, i) => )
39 |
40 | renderArchivedTrips = () => {
41 | const { loading, trips } = this.props
42 |
43 | return trips.map(trip =>
44 | trip.isArchived ? (
45 |
46 | ) : null
47 | )
48 | }
49 |
50 | render() {
51 | const { loading } = this.props
52 | const { archivedExists } = this.state
53 |
54 | return (
55 |
56 |
57 | {loading
58 | ? this.renderPlaceholders()
59 | : archivedExists
60 | ? this.renderArchivedTrips()
61 | : "No Archived Trips"}
62 |
63 |
64 | )
65 | }
66 | }
67 |
68 | const mapStateToProps = state => ({
69 | loading: state.trips.pending,
70 | trips: getTripsArray(state),
71 | userId: state.auth.user.id
72 | })
73 |
74 | export default connect(
75 | mapStateToProps,
76 | { getTrips }
77 | )(ArchivedTrips)
78 |
--------------------------------------------------------------------------------
/client/src/components/forms/WaypointForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { connect } from "react-redux"
3 | import PropTypes from "prop-types"
4 |
5 | import { Button, Form, Input } from "../../styles/theme/styledComponents"
6 | import { saveWaypoint } from "../../redux/actions/trips"
7 |
8 | const defaultState = {
9 | waypoint: {
10 | name: "",
11 | arrivalDate: "",
12 | arrivalTime: "",
13 | lat: "",
14 | lon: ""
15 | }
16 | }
17 |
18 | class WaypointForm extends Component {
19 | state = { ...defaultState }
20 |
21 | handleChange = key => e => {
22 | this.setState({
23 | waypoint: { ...this.state.waypoint, [key]: e.target.value }
24 | })
25 | }
26 |
27 | handleSubmit = e => {
28 | e.preventDefault()
29 | const { waypoint } = this.state
30 | this.props.saveWaypoint({ ...waypoint })
31 | this.setState({ ...defaultState })
32 | }
33 |
34 | handlePinDrop = e => {
35 | e.preventDefault()
36 | alert("Drop pin on the map!")
37 | }
38 |
39 | render() {
40 | return (
41 |
42 |
Start
43 |
+ Add
44 |
- Remove
45 |
64 |
65 | )
66 | }
67 | }
68 |
69 | WaypointForm.propTypes = {
70 | saveWaypoint: PropTypes.func.isRequired
71 | }
72 |
73 | const mapDispatchToProps = { saveWaypoint }
74 |
75 | export default connect(
76 | null,
77 | mapDispatchToProps
78 | )(WaypointForm)
79 |
--------------------------------------------------------------------------------
/client/src/components/forms/customInputs.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import { ErrorMessage } from "formik"
4 |
5 | import { Label, GhostInput, Button } from "../../styles/theme/styledComponents"
6 |
7 | const CustomError = ({ name }) => (
8 |
9 | {errorMessage => {errorMessage}
}
10 |
11 | )
12 |
13 | // Error Message needs to come first to
14 | // make it easier to target Input as next sibling with CSS:
15 | export const CustomInputWithError = ({
16 | values, // `values` is made available by Formik
17 | name, // Formik uses `name` to associate an Input with ErrorMessage
18 | type,
19 | placeholder,
20 | onChange,
21 | onBlur,
22 | showLabel,
23 | classNames = [] // allows you to override styling
24 | }) => (
25 |
26 |
27 | {showLabel && {name}: }
28 |
36 |
37 | )
38 |
39 | export const CustomButtonWithError = ({
40 | text,
41 | isSubmitting = false,
42 | classNames = [] // allows you to override styling
43 | }) => (
44 |
45 |
50 | {text}
51 |
52 |
53 | )
54 |
55 | CustomError.propTypes = {
56 | name: PropTypes.string
57 | }
58 |
59 | CustomInputWithError.propTypes = {
60 | name: PropTypes.string.isRequired,
61 | type: PropTypes.string.isRequired,
62 | values: PropTypes.object,
63 | classNames: PropTypes.array,
64 | onChange: PropTypes.any,
65 | onBlur: PropTypes.any,
66 | placeholder: PropTypes.string,
67 | showLabel: PropTypes.bool
68 | }
69 |
70 | CustomButtonWithError.propTypes = {
71 | text: PropTypes.string.isRequired,
72 | isSubmitting: PropTypes.bool.isRequired,
73 | classNames: PropTypes.array,
74 | submitError: PropTypes.string
75 | }
76 |
--------------------------------------------------------------------------------
/client/src/redux/reducers/billing.js:
--------------------------------------------------------------------------------
1 | import {
2 | INIT_NEW_SUBSCRIPTION,
3 | SUBSCRIBE_PENDING,
4 | SUBSCRIBE_SUCCESS,
5 | SUBSCRIBE_FAIL,
6 | INIT_NEW_CANCELLATION,
7 | CANCEL_SUBSCRIPTION_SUCCESS,
8 | CANCEL_SUBSCRIPTION_FAIL,
9 | INIT_NEW_INVOICES,
10 | INVOICES_SUCCESS,
11 | INVOICES_FAIL
12 | } from "../actions/types"
13 |
14 | const defaultState = {
15 | pending: false,
16 | error: null,
17 | isCheckoutFormOpen: false,
18 | invoices: null,
19 | stripe: null
20 | }
21 |
22 | export const billingReducer = (state = defaultState, action) => {
23 | switch (action.type) {
24 | case INIT_NEW_SUBSCRIPTION:
25 | return {
26 | ...state,
27 | isCheckoutFormOpen: true,
28 | stripe: action.stripe
29 | }
30 |
31 | case SUBSCRIBE_PENDING:
32 | return { ...state, pending: true }
33 |
34 | case SUBSCRIBE_SUCCESS:
35 | return {
36 | ...state,
37 | pending: false,
38 | isCheckoutFormOpen: false
39 | }
40 |
41 | case SUBSCRIBE_FAIL:
42 | return {
43 | ...state,
44 | pending: false,
45 | isCheckoutFormOpen: true,
46 | error: action.payload
47 | }
48 |
49 | case INIT_NEW_CANCELLATION:
50 | return {
51 | ...state,
52 | pending: true,
53 | isCheckoutFormOpen: false
54 | }
55 |
56 | case CANCEL_SUBSCRIPTION_SUCCESS:
57 | return {
58 | ...state,
59 | pending: false,
60 | isCheckoutFormOpen: false
61 | }
62 |
63 | case CANCEL_SUBSCRIPTION_FAIL:
64 | return {
65 | ...state,
66 | pending: false,
67 | isCheckoutFormOpen: false,
68 | error: action.payload
69 | }
70 |
71 | case INIT_NEW_INVOICES:
72 | return {
73 | ...state,
74 | pending: true
75 | }
76 |
77 | case INVOICES_SUCCESS:
78 | return {
79 | ...state,
80 | pending: false,
81 | isCheckoutFormOpen: false,
82 | invoices: action.payload
83 | }
84 | case INVOICES_FAIL:
85 | return {
86 | ...state,
87 | pending: false,
88 | isCheckoutFormOpen: false,
89 | error: action.payload
90 | }
91 |
92 | default:
93 | return state
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/client/src/components/icons/GoogleIcon.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | const GoogleIcon = () => (
4 |
11 |
12 |
13 |
14 |
15 |
20 |
25 |
30 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 |
43 | export default GoogleIcon
44 |
--------------------------------------------------------------------------------
/server/tests/mock.js:
--------------------------------------------------------------------------------
1 | let date = new Date("March 18, 2019 11:30:00 AM")
2 | let date2 = new Date("March 18, 2019 11:45:00 AM")
3 |
4 | export const userOne = {
5 | password: "testpass",
6 | email: "email@hotmail.com",
7 | displayName: "User One",
8 | contact: {
9 | name: "Bob",
10 | number: "+15005550006"
11 | }
12 | }
13 | export const userTwo = {
14 | password: "testpass2",
15 | email: "email@gmail.com",
16 | displayName: "User Two"
17 | }
18 | export const userThree = {
19 | password: "testpass3",
20 | email: "email@yahoo.com",
21 | displayName: "User Three"
22 | }
23 |
24 | export const tripOne = {
25 | userId: "",
26 | name: "tripOne",
27 | start: Date.now(),
28 | end: Date.now(),
29 | lat: 46.21,
30 | lon: 123.234,
31 | waypoints: []
32 | }
33 | export const tripTwo = {
34 | userId: "",
35 | name: "tripTwo",
36 | start: Date.now(),
37 | end: Date.now(),
38 | lat: 30.11,
39 | lon: 93.134,
40 | waypoints: []
41 | }
42 | export const tripThree = {
43 | userId: "",
44 | name: "tripThree",
45 | start: Date.now(),
46 | end: Date.now(),
47 | lat: 12.21,
48 | lon: 45.234,
49 | waypoints: []
50 | }
51 | export const publicTrip1 = {
52 | userId: "",
53 | name: "pub1",
54 | start: Date.now(),
55 | end: Date.now(),
56 | lat: 12.21,
57 | lon: 45.234,
58 | isPublic: true,
59 | waypoints: []
60 | }
61 | export const publicTrip2 = {
62 | userId: "",
63 | name: "pub2",
64 | start: Date.now(),
65 | end: Date.now(),
66 | lat: 12.21,
67 | lon: 45.234,
68 | isPublic: true,
69 | waypoints: []
70 | }
71 |
72 | export const waypointOne = {
73 | tripId: "",
74 | order: 1,
75 | name: "Checkpoint 1",
76 | lat: 30.508293960387878,
77 | lon: -97.77231216430664,
78 | start: Date.now(),
79 | end: date,
80 | complete: true
81 | }
82 | export const waypointTwo = {
83 | tripId: "",
84 | order: 2,
85 | name: "Checkpoint 2",
86 | lat: 30.508293960387878,
87 | lon: -97.77231216430664,
88 | start: Date.now(),
89 | end: date2
90 | }
91 | export const waypointFour = {
92 | tripId: "",
93 | order: 1,
94 | name: "Checkpoint 1",
95 | lat: 24.208293960387878,
96 | lon: -101.45231216430664,
97 | start: Date.now(),
98 | end: date2
99 | }
100 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/Hero.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 |
4 | import Header from "./Header"
5 | import Button from "./Button"
6 | import Typewriter from "./Typewriter"
7 | import { media } from "../../styles/theme/mixins"
8 |
9 | const HeroContainer = styled.div`
10 | display: flex;
11 | flex-direction: column;
12 | background-image: url(/images/bg.jpg);
13 | background-size: cover;
14 | height: 100vh;
15 | width: 100%;
16 | `
17 |
18 | const CallToAction = styled.div`
19 | display: grid;
20 | grid-template-columns: 1fr 1fr;
21 | padding: 200px;
22 | height: 90%;
23 | width: 100%;
24 |
25 | h1 {
26 | color: white;
27 | text-align: left;
28 | text-shadow: 1px 2px 5px rgba(0, 0, 0, 0.75);
29 | font-size: 2.5rem;
30 | font-weight: 600;
31 | white-space: nowrap;
32 | overflow: visible;
33 | }
34 |
35 | .wrapper {
36 | display: flex;
37 | flex-direction: column;
38 | align-items: center;
39 | justify-content: center;
40 | height: 100%;
41 | }
42 |
43 | .button-wrapper {
44 | display: flex;
45 | flex-direction: column;
46 | align-items: center;
47 | justify-content: center;
48 | width: 100%;
49 | }
50 |
51 | a:hover {
52 | text-decoration: none;
53 | }
54 |
55 | ${media.tablet`
56 | grid-template-columns: 1fr;
57 | padding: 100px;
58 |
59 | h1 {
60 | font-size: 2rem;
61 | }
62 | `}
63 |
64 | ${media.phone`
65 | grid-template-columns: 1fr;
66 | padding: 0;
67 |
68 |
69 | h1 {
70 | font-size: 1.95em;
71 | }
72 | `}
73 | `
74 |
75 | const Hero = () => (
76 |
77 |
78 |
79 |
80 |
The companion app for
81 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | )
98 |
99 | export default Hero
100 |
--------------------------------------------------------------------------------
/client/src/components/PublicTrips.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { connect } from "react-redux"
3 | import PropTypes from "prop-types"
4 |
5 | import PublicTripCard from "./PublicTripCard"
6 | import * as s from "../styles/TripCard.styles"
7 | import { ExploreHeader } from "../styles/Explore.styles"
8 | import { getPublicTrips } from "../redux/actions/trips"
9 | import { getTripsArray } from "../utils/selectors"
10 | import { TripPropTypes } from "./propTypes"
11 |
12 | class PublicTrips extends Component {
13 | static propTypes = {
14 | getPublicTrips: PropTypes.func.isRequired,
15 | loading: PropTypes.bool,
16 | trips: PropTypes.arrayOf(TripPropTypes),
17 | userId: PropTypes.string.isRequired
18 | }
19 |
20 | state = {
21 | publicExists: false
22 | }
23 |
24 | componentDidMount() {
25 | const { getPublicTrips } = this.props
26 | getPublicTrips()
27 | }
28 |
29 | componentDidUpdate(prevProps, prevState) {
30 | const publicExists = this.props.trips.some(trip => trip.isPublic)
31 | if (!prevState.publicExists && publicExists) {
32 | this.setState({ publicExists })
33 | }
34 | }
35 |
36 | renderPlaceholders = () =>
37 | this.props.trips
38 | .filter(trip => trip.isPublic)
39 | .map((_, i) => )
40 |
41 | renderPublicTrips = () => {
42 | const { loading, trips } = this.props
43 |
44 | return trips.map(trip =>
45 | trip.isPublic ? (
46 |
47 | ) : null
48 | )
49 | }
50 |
51 | render() {
52 | const { loading } = this.props
53 | const { publicExists } = this.state
54 |
55 | return (
56 |
57 |
Explore other trips
58 |
59 |
60 |
61 | {loading
62 | ? this.renderPlaceholders()
63 | : publicExists
64 | ? this.renderPublicTrips()
65 | : "No Public Trips"}
66 |
67 |
68 |
69 | )
70 | }
71 | }
72 |
73 | const mapStateToProps = state => ({
74 | loading: state.trips.pending,
75 | trips: getTripsArray(state),
76 | userId: state.auth.user.id
77 | })
78 |
79 | export default connect(
80 | mapStateToProps,
81 | { getPublicTrips }
82 | )(PublicTrips)
83 |
--------------------------------------------------------------------------------
/client/src/components/forms/formValidations.js:
--------------------------------------------------------------------------------
1 | import { validateEmail } from "../../utils/"
2 |
3 | export const loginValidations = values => {
4 | let errors = {}
5 | if (!values.email) errors.email = "Email is required"
6 | if (!values.password) errors.password = "Password is required"
7 | if (values.password && values.password.length < 8)
8 | errors.password = "Password must be at least 8 characters"
9 |
10 | return errors
11 | }
12 |
13 | export const registerValidations = values => {
14 | let errors = {}
15 | if (!values.email) {
16 | errors.email = "Email is required"
17 | } else if (validateEmail(values.email)) {
18 | errors.email = "Invalid email address"
19 | }
20 |
21 | if (!values.password) errors.password = "Password is required"
22 | if (values.password && values.password.length < 8)
23 | errors.password = "Password must be at least 8 characters"
24 | if (
25 | values.password &&
26 | values.passwordConfirm &&
27 | values.password !== values.passwordConfirm
28 | ) {
29 | errors.passwordConfirm = "Passwords must match"
30 | }
31 | return errors
32 | }
33 |
34 | export const newTripValidations = values => {
35 | let errors = {}
36 |
37 | if (!values.name) errors.name = "Trip name is required"
38 | if (!values.start) errors.start = "Start date is required"
39 | if (!values.end) errors.end = "End date is required"
40 | if (!values.lat) errors.lat = "Latitude is required"
41 | if (!values.lon) errors.lon = "Longitude is required"
42 | // Make sure end date is later than start date:
43 | if (values.end && values.start > values.end) {
44 | errors.end = "Trip can't end before it starts"
45 | }
46 |
47 | return errors
48 | }
49 |
50 | export const settingsValidations = values => {
51 | let errors = {}
52 | if (!values.email) {
53 | errors.email = "Email is required"
54 | } else if (validateEmail(values.email)) {
55 | errors.email = "Invalid email address"
56 | }
57 |
58 | if (!values.oldPassword && values.newPassword)
59 | errors.oldPassword = "Old password is required"
60 | if (values.oldPassword && !values.newPassword)
61 | errors.newPassword = "New password is required"
62 | if (
63 | (values.oldPassword && values.oldPassword.length < 8) ||
64 | (values.newPassword && values.newPassword.length < 8)
65 | )
66 | errors.password = "Password must be at least 8 characters"
67 | return errors
68 | }
69 |
--------------------------------------------------------------------------------
/client/src/components/CopyTripLinkModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { Button } from "../styles/theme/styledComponents"
3 | import { Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap"
4 | import { CopyToClipboard } from "react-copy-to-clipboard"
5 | import PropTypes from "prop-types"
6 | import { TripPropTypes } from "./propTypes"
7 |
8 | class CopyTripLinkModal extends Component {
9 | constructor(props) {
10 | super(props)
11 | this.state = {
12 | modal: false,
13 | copied: false
14 | }
15 |
16 | this.toggle = this.toggle.bind(this)
17 | }
18 | toggle() {
19 | this.setState(prevState => ({
20 | modal: !prevState.modal
21 | }))
22 | }
23 | handlePublic = () => {
24 | this.props.handleTogglePublic(this.props.trip.id)
25 | this.setState(prevState => ({
26 | copied: !prevState.copied
27 | }))
28 | }
29 | handlePopupStatus = () => {
30 | this.setState(prevState => ({
31 | copied: !prevState.copied
32 | }))
33 | }
34 |
35 | render() {
36 | return (
37 |
38 |
44 | {" "}
45 | {this.props.trip.isPublic ? "Make Private" : "Share!"}
46 |
47 |
48 |
49 | Click on the link below to copy
50 |
51 |
52 | {
55 | this.handlePopupStatus()
56 | }}
57 | >
58 | {`bkwds.co/public/${this.props.trip.id}`}
59 |
60 |
61 |
62 | {this.state.copied ? "Copied. Please confirm public trip!" : ""}
63 |
64 |
65 | {this.props.trip.isPublic ? "Make Private" : "Make Public!"}
66 | {" "}
67 |
68 | Cancel
69 |
70 |
71 |
72 |
73 | )
74 | }
75 | }
76 |
77 | CopyTripLinkModal.propTypes = {
78 | trip: TripPropTypes,
79 | handleTogglePublic: PropTypes.func.isRequired
80 | }
81 |
82 | export default CopyTripLinkModal
83 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/PlansCard.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import styled from "styled-components"
4 |
5 | import { media } from "../../styles/theme/mixins"
6 | import Button from "./Button"
7 |
8 | const Card = styled.div`
9 | display: flex:
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | text-align: center;
14 | height: 350px;
15 | width: 350px;
16 | border-radius: 12px;
17 | box-shadow: 0 0.3125rem 0.0625rem 0 rgba(0, 0, 0, 0.25),
18 | 0 0 0 0.0625rem rgba(255, 255, 255, 0.03),
19 | 0 0.0625rem 2px 0 rgba(0, 0, 0, 0.75),
20 | 0 0.0625rem 0.1875rem 0 rgba(0, 0, 0, 0.1);
21 | background: white;
22 | padding: 1.5rem;
23 | transition: transform 100ms;
24 | transition-timing-function: ease-in-out;
25 |
26 | &:hover {
27 | transform: translate3d(0px, -0.1875rem, 0px);
28 | }
29 |
30 | h5 {
31 | font-size: 0.8rem;
32 | font-weight: 600;
33 | text-transform: uppercase;
34 | letter-spacing: 2px;
35 | }
36 |
37 | h2 {
38 | margin: 10% 0;
39 | font-size: 1.9rem;
40 | font-weight: 400;
41 | color: ${props => props.theme.primaryDark};
42 |
43 | span {
44 | font-size: 2.5rem;
45 | color: ${props => props.theme.primary};
46 | }
47 | }
48 |
49 | span.bold {
50 | font-weight: 600;
51 | }
52 |
53 | ul {
54 | list-style: none;
55 | padding: 0;
56 | }
57 |
58 | li {
59 | padding: 1%;
60 | }
61 |
62 | ${media.tablet`
63 | height: 300px;
64 | width: 300px;
65 |
66 | h2 {
67 | margin: 10% 0;
68 | font-size: 1.7rem;
69 |
70 | span {
71 | font-size: 2.25rem;
72 | }
73 | }
74 |
75 | li {
76 | font-size: 0.9rem;
77 | }
78 | `}
79 | `
80 |
81 | const PlansCard = ({ price, title }) => (
82 |
83 | {title}
84 |
85 | ${price} /year
86 |
87 |
88 |
89 | Unlimited trips
90 |
91 |
92 | Unlimited archived trips
93 |
94 |
95 | Unlimited trip photos
96 |
97 |
98 |
105 |
106 | )
107 |
108 | PlansCard.propTypes = {
109 | price: PropTypes.string.isRequired,
110 | title: PropTypes.string.isRequired
111 | }
112 |
113 | export default PlansCard
114 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "styled-components"
3 |
4 | import { media } from "../../styles/theme/mixins"
5 |
6 | const FooterContainer = styled.div`
7 | display: grid;
8 | grid-template-columns: repeat(6, 1fr);
9 | grid-template-rows: 100px 100px;
10 | grid-template-areas: "logo logo menu terms social join";
11 | height: 100%;
12 | width: 100%;
13 | padding: 5% 0;
14 | align-content: center;
15 | background-color: #222222;
16 |
17 | button {
18 | display: block;
19 | margin: 0 0 1rem;
20 | padding: 0;
21 | background: transparent;
22 | border: none;
23 | cursor: pointer;
24 | text-decoration: none;
25 | color: white;
26 | font-size: 0.9rem;
27 | letter-spacing: 1.25px;
28 | }
29 |
30 | h5 {
31 | margin-bottom: 2rem;
32 | color: #646565;
33 | font-size: 0.9rem;
34 | font-weight: 600;
35 | letter-spacing: 1.25px;
36 | }
37 |
38 | .logo {
39 | grid-area: logo;
40 | }
41 |
42 | .menu {
43 | grid-area: menu;
44 | }
45 |
46 | .terms {
47 | grid-area: terms;
48 | margin-top: 55px;
49 | }
50 |
51 | .social {
52 | grid-area: social;
53 | }
54 |
55 | .join {
56 | grid-area: join;
57 | }
58 |
59 | ${media.tablet`
60 | grid-template-columns: 1fr;
61 | grid-template-rows: 114px repeat(4, 1fr);
62 | grid-template-areas: "logo"
63 | "menu"
64 | "terms"
65 | "social"
66 | "join";
67 |
68 | div {
69 | margin-left: 40px;
70 | }
71 | `}
72 | `
73 |
74 | const Img = styled.img`
75 | width: 114px;
76 | margin-left: 90px;
77 |
78 | ${media.tablet`
79 | margin: 20px 0;
80 | `}
81 | `
82 |
83 | const Footer = () => (
84 |
85 |
86 |
87 |
88 |
89 |
Menu
90 | Features
91 | About
92 | Community
93 | Support
94 |
95 |
96 | Business
97 | Terms
98 | Policy
99 |
100 |
101 |
Follow
102 | Facebook
103 | Twitter
104 | Instagram
105 |
106 |
107 |
Get started
108 | Sign Up
109 | Login
110 |
111 |
112 | )
113 |
114 | export default Footer
115 |
--------------------------------------------------------------------------------
/client/src/components/Trip.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { connect } from "react-redux"
3 | import { Link } from "react-router-dom"
4 | import { TripPropTypes } from "./propTypes"
5 | import { deleteTrip, toggleArchive } from "../redux/actions/trips"
6 |
7 | import { deleteTrip, toggleArchive, editTrip } from "../redux/actions/trips"
8 |
9 | class Trip extends Component {
10 | handleDelete = tripId => e => {
11 | e.preventDefault()
12 | this.props.deleteTrip(tripId)
13 | }
14 | handleedit = tripId => e => {
15 | e.preventDefault()
16 | this.props.editTrip(tripId)
17 | }
18 | toggleArchive = (tripId, archiveTrip) => () => {
19 | this.props.toggleArchive(tripId, archiveTrip)
20 | }
21 |
22 | render() {
23 | const { trip } = this.props
24 | return (
25 |
26 | {!trip.id && "Loading trip"}
27 | {trip.id && (
28 | <>
29 |
30 |
31 |
35 |
36 |
37 |
View Trip
38 |
Name: {trip.name}
39 |
ID: {trip.id}
40 |
UserID: {trip.userId}
41 |
Start: {trip.start}
42 |
End: {trip.end}
43 |
Created at: {trip.createdAt}
44 |
Updated at: {trip.updatedAt}
45 |
Archived: {trip.isArchived.toString()}
46 |
47 | Waypoints:{" "}
48 | {trip.waypoints.map((w, i) => (
49 |
{w.toString()}
50 | ))}
51 |
52 |
DELETE
53 |
54 | {archived ? "UNARCHIVE" : "ARCHIVE"}
55 |
56 |
57 |
58 |
59 | >
60 | )}
61 |
62 | )
63 | }
64 | }
65 |
66 | Trip.propTypes = {
67 | deleteTrip: PropTypes.func.isRequired,
68 | toggleArchive: PropTypes.func.isRequired,
69 | editTrip: PropTypes.func.isRequired,
70 | trip: TripPropTypes
71 | }
72 |
73 | const mapDispatchToProps = {
74 | deleteTrip,
75 | toggleArchive,
76 | editTrip
77 | }
78 |
79 | export default connect(
80 | null,
81 | mapDispatchToProps
82 | )(Trip)
83 |
--------------------------------------------------------------------------------
/client/src/styles/TripCard.styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const TripCardStyles = styled.div`
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | padding: 20px;
8 | width: 100%;
9 |
10 | a {
11 | text-decoration: none;
12 | }
13 |
14 | button {
15 | margin-top: 1.25rem;
16 | cursor: pointer;
17 | }
18 |
19 | .container {
20 | display: flex;
21 | justify-content: flex-start;
22 | justify-self: center;
23 | flex-wrap: wrap;
24 | max-width: 100%;
25 | height: 100%;
26 |
27 | @media (max-width: 40.25em) {
28 | justify-content: center;
29 | }
30 | }
31 |
32 | .card {
33 | display: flex;
34 | justify-content: flex-start;
35 | align-self: center;
36 | border-radius: 0.25rem;
37 | border: none;
38 | height: 400px;
39 | width: 380px;
40 | max-height: 100%;
41 | max-width: 100%;
42 | margin: 0 10px 20px;
43 | box-shadow: rgba(0, 0, 0, 0.05) 0px 0.0625rem 0px,
44 | rgba(0, 0, 5, 0.1) 0px 0.0625rem 0.125rem,
45 | rgba(0, 0, 0, 0.05) 0px 0.3125rem 0.9375rem;
46 | transition: transform 0.22s ease-out 0s, box-shadow;
47 |
48 | @media (max-width: 55.875em) {
49 | height: 300px;
50 | width: 280px;
51 | }
52 | }
53 |
54 | .card:hover {
55 | box-shadow: rgba(0, 0, 0, 0.05) 0px 0.3125rem 0.9375rem,
56 | rgba(0, 0, 0, 0.1) 0px 0.3125rem 0.3125rem,
57 | rgba(0, 0, 0, 0.05) 0px 0.125rem 0.3125rem;
58 | transform: translate3d(0px, -0.1875rem, 0px);
59 | img {
60 | transform: scale(1.05, 1.05);
61 | transition: all 1s ease;
62 | }
63 | }
64 |
65 | .card-content {
66 | position: relative;
67 | margin: 5%;
68 | }
69 |
70 | .card-cta {
71 | display: flex;
72 | flex-direction: row;
73 | justify-content: space-between;
74 | }
75 |
76 | .card-image {
77 | display: flex;
78 | width: 100%;
79 | height: 231px;
80 | justify-content: center;
81 | align-items: center;
82 | overflow: hidden;
83 |
84 | @media (max-width: 55.875em) {
85 | height: 131px;
86 | width: 100%;
87 | }
88 |
89 | img {
90 | transition: all 1.86s ease;
91 | }
92 |
93 | img.grayscale {
94 | -webkit-filter: grayscale(100%);
95 | filter: grayscale(100%);
96 | }
97 |
98 | .text-overlay {
99 | position: absolute;
100 | color: rgba(30, 33, 37, 0.25);
101 | font-size: 3.75rem;
102 | font-weight: 600;
103 | transform: rotate(45deg);
104 | @media all and (max-width: 894px) {
105 | font-size: 2.125rem;
106 | }
107 | }
108 | }
109 | `
110 |
--------------------------------------------------------------------------------
/client/src/components/LandingPage/Typewriter.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react"
2 | import PropTypes from "prop-types"
3 | import styled from "styled-components"
4 |
5 | const Span = styled.span`
6 | @keyframes blink {
7 | to {
8 | opacity: 0;
9 | }
10 | }
11 |
12 | color: white;
13 | font-family: Courier, monospace;
14 | animation: blink 0.5s infinite;
15 | `
16 |
17 | class Typewriter extends PureComponent {
18 | static defaultProps = {
19 | delay: 550,
20 | erasingSpeed: 50,
21 | typingSpeed: 80
22 | }
23 |
24 | static propTypes = {
25 | delay: PropTypes.number,
26 | erasingSpeed: PropTypes.number,
27 | text: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
28 | typingSpeed: PropTypes.number
29 | }
30 |
31 | _timeout
32 |
33 | state = {
34 | displayText: "",
35 | index: 0
36 | }
37 |
38 | componentDidMount() {
39 | this.startTyping()
40 | }
41 |
42 | componentWillUnmount() {
43 | if (this._timeout) {
44 | clearTimeout(this._timeout)
45 | }
46 | }
47 |
48 | getText = () =>
49 | typeof text === "string" ? [this.props.text] : [...this.props.text]
50 |
51 | erase = () => {
52 | let { displayText, index } = this.state
53 | const { erasingSpeed } = this.props
54 |
55 | if (displayText.length === 0) {
56 | const textArray = this.getText()
57 |
58 | index = index + 1 === textArray.length ? 0 : index + 1
59 |
60 | this.setState({ index }, () => this.startTyping())
61 | } else {
62 | displayText = displayText.substr(
63 | -displayText.length,
64 | displayText.length - 1
65 | )
66 |
67 | this.setState({ displayText }, () => {
68 | this.timeout = setTimeout(() => {
69 | this.erase()
70 | }, erasingSpeed)
71 | })
72 | }
73 | }
74 |
75 | type = () => {
76 | let { displayText } = this.state
77 | const { index } = this.state
78 | const { delay, typingSpeed } = this.props
79 |
80 | const text = this.getText()[index]
81 |
82 | if (text.length > displayText.length) {
83 | displayText = text.substr(0, displayText.length + 1)
84 | this.setState({ displayText }, () => {
85 | this._timeout = setTimeout(() => this.type(), typingSpeed)
86 | })
87 | } else {
88 | this._timeout = setTimeout(() => this.erase(), delay)
89 | }
90 | }
91 |
92 | startTyping = () => (this._timeout = setTimeout(() => this.type(), 550))
93 |
94 | render() {
95 | return (
96 |
97 | {this.state.displayText}
98 | |
99 |
100 | )
101 | }
102 | }
103 |
104 | export default Typewriter
105 |
--------------------------------------------------------------------------------
/client/src/redux/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | AUTH_LOADING,
3 | LOGIN_SUCCESS,
4 | LOGIN_FAILURE,
5 | LOGOUT_SUCCESS,
6 | ADD_TOKEN_TO_STATE,
7 | REGISTRATION_SUCCESS,
8 | REGISTRATION_FAILURE,
9 | QUERYING_USER_BY_TOKEN,
10 | QUERYING_USER_BY_TOKEN_SUCCESS,
11 | QUERYING_USER_BY_TOKEN_ERROR,
12 | UPDATE_USER_IN_STORE
13 | } from "../actions/types"
14 |
15 | import { normalizeUser } from "../../utils/selectors"
16 |
17 | const defaultUser = {
18 | id: null,
19 | email: "",
20 | subscribed: false,
21 | subscribeId: null,
22 | subDate: null,
23 | customerId: null
24 | }
25 |
26 | const defaultState = {
27 | user: { ...defaultUser },
28 | pending: false,
29 | isLoggedIn: false,
30 | checkedForToken: false,
31 | error: null,
32 | token: ""
33 | }
34 |
35 | export const authReducer = (state = defaultState, action) => {
36 | switch (action.type) {
37 | case AUTH_LOADING:
38 | return { ...state, pending: true, error: null }
39 |
40 | case LOGIN_SUCCESS:
41 | return {
42 | ...state,
43 | isLoggedIn: true,
44 | pending: false,
45 | error: null,
46 | user: normalizeUser(action.payload)
47 | }
48 | case LOGIN_FAILURE:
49 | return {
50 | ...state,
51 | pending: false,
52 | isLoggedIn: false,
53 | error: action.payload
54 | }
55 |
56 | case LOGOUT_SUCCESS:
57 | return {
58 | ...state,
59 | user: { ...defaultUser },
60 | error: null,
61 | isLoggedIn: false,
62 | pending: false
63 | }
64 |
65 | case REGISTRATION_SUCCESS:
66 | return {
67 | ...state,
68 | pending: false,
69 | error: null,
70 | user: action.payload
71 | }
72 | case REGISTRATION_FAILURE:
73 | return {
74 | ...state,
75 | pending: false,
76 | error: action.payload
77 | }
78 |
79 | case ADD_TOKEN_TO_STATE:
80 | return { ...state, token: action.payload }
81 | case QUERYING_USER_BY_TOKEN:
82 | return { ...state, pending: true, checkedForToken: true }
83 | case QUERYING_USER_BY_TOKEN_SUCCESS:
84 | return {
85 | ...state,
86 | pending: false,
87 | isLoggedIn: true,
88 | user: normalizeUser(action.payload)
89 | }
90 | case QUERYING_USER_BY_TOKEN_ERROR:
91 | return {
92 | ...state,
93 | pending: false,
94 | error: action.payload
95 | }
96 |
97 | case UPDATE_USER_IN_STORE:
98 | return {
99 | ...state,
100 | user: normalizeUser({ ...state.user, ...action.payload })
101 | }
102 |
103 | default:
104 | return state
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/server/src/api/resources/user/user.controller.js:
--------------------------------------------------------------------------------
1 | import { User } from "./user.model"
2 |
3 | export const getAllUsers = (req, res) => {
4 | User.find({})
5 | .then(users => {
6 | res.status(200).json(users)
7 | })
8 | .catch(err => {
9 | res.status(500).send(err)
10 | })
11 | }
12 |
13 | export const createUser = (req, res) => {
14 | const newUser = new User(req.body)
15 | User.findOne({ email: req.body.email })
16 | .then(user => {
17 | if (user) return res.status(400).send("User already exists")
18 | newUser
19 | .save()
20 | .then(user => {
21 | res.status(201).json(user)
22 | })
23 | .catch(err => {
24 | res.status(500).send(err.message)
25 | })
26 | })
27 | .catch(err => {
28 | res.status(500).send(err)
29 | })
30 | }
31 |
32 | export const getOneUser = (req, res) => {
33 | User.findOne({ _id: req.params.id })
34 | .populate("trips")
35 | .exec()
36 | .then(user => {
37 | res.status(200).json(user)
38 | })
39 | .catch(err => {
40 | return res.status(500).send(err)
41 | })
42 | }
43 |
44 | export const updateUser = (req, res) => {
45 | const id = req.params.id
46 | const update = req.body
47 |
48 | if (Object.keys(update).length === 0) {
49 | return res.status(400).send("Bad Request")
50 | }
51 | if (update.email) return res.status(401).send("Email change not allowed")
52 | if (update.password)
53 | return res.status(401).send("Password cannot be changed from this endpoint")
54 | if (update.trips)
55 | return res
56 | .status(401)
57 | .send("Trips cannot be modified from User model. Use Trip model instead")
58 |
59 | User.findOneAndUpdate({ _id: id }, update)
60 | .then(oldUser => {
61 | User.findOne({ _id: oldUser.id })
62 | .populate("trips")
63 | .exec()
64 | .then(newUser => {
65 | res.status(200).json(newUser)
66 | })
67 | .catch(() => {
68 | res.status(404).json("Not Found")
69 | })
70 | })
71 | .catch(() => {
72 | res.status(404).json("Not Found")
73 | })
74 | }
75 |
76 | export const deleteUser = (req, res) => {
77 | User.findOneAndDelete({ _id: req.params.id })
78 | .then(user => {
79 | if (!user) return res.status(404).send("User not found")
80 | const payload = {
81 | user,
82 | msg: "User was deleted"
83 | }
84 | res.status(202).json(payload)
85 | })
86 | .catch(err => {
87 | res.status(500).send(err)
88 | })
89 | }
90 |
91 | export const getUserTrips = (req, res) => {
92 | User.findById(req.params.id)
93 | .populate("trips")
94 | .exec((err, user) => {
95 | if (err) res.status(500).send(err)
96 | res.status(200).json(user.trips)
97 | })
98 | }
99 |
--------------------------------------------------------------------------------
/client/src/components/pages/Dashboard.js:
--------------------------------------------------------------------------------
1 | /* eslint react/display-name: 0 */
2 | /* eslint react/prop-types: 0 */
3 | import React from "react"
4 | import { connect } from "react-redux"
5 | import { Switch } from "react-router-dom"
6 | import { MatchPropTypes } from "../propTypes"
7 |
8 | import SingleTrip from "../Maps/SingleTrip"
9 | import AppContainer from "../AppContainer"
10 | import NewTrip from "../NewTrip"
11 | import Trips from "../Trips"
12 | import Billing from "../Billing/"
13 | import PaymentDetails from "../Billing/PaymentDetails"
14 | import Plans from "../Billing/Plans"
15 | import ArchivedTrips from "../ArchivedTrips"
16 | import DashboardHome from "../DashboardHome"
17 | import Settings from "../Settings"
18 | import EditTrip from "../EditTrip"
19 | import PublicTrips from "../PublicTrips"
20 |
21 | import CustomRoute from "../../utils/CustomRoute"
22 |
23 | const dashboardRoutes = [
24 | {
25 | path: "/dashboard",
26 | name: "Dashboard",
27 | component: DashboardHome
28 | },
29 | {
30 | path: "/trip/create",
31 | name: "NewTrip",
32 | component: NewTrip
33 | },
34 | {
35 | path: "/trip/edit",
36 | name: "EditTrip",
37 | component: EditTrip
38 | },
39 | {
40 | path: "/trip/:tripId",
41 | name: "SingleTrip",
42 | component: ({ match }) =>
43 | },
44 | {
45 | path: "/trips",
46 | name: "Trips",
47 | component: Trips,
48 | exact: true
49 | },
50 | {
51 | path: "/trips/archived",
52 | name: "ArchivedTrips",
53 | component: ArchivedTrips
54 | },
55 | {
56 | path: "/settings",
57 | name: "Settings",
58 | component: Settings
59 | },
60 | {
61 | path: "/billing/payment",
62 | name: "Payment",
63 | component: PaymentDetails
64 | },
65 | {
66 | path: "/billing",
67 | name: "Billing",
68 | component: Billing
69 | },
70 | {
71 | path: "/upgrade",
72 | name: "Upgrade",
73 | component: Plans
74 | },
75 | {
76 | path: "/trips/explore",
77 | name: "PublicTrips",
78 | component: PublicTrips
79 | }
80 | ]
81 |
82 | const Dashboard = ({ match }) => {
83 | const basePath = match.path
84 | return (
85 |
86 |
87 | {dashboardRoutes.map(({ path, ...rest }, idx) => (
88 |
94 | ))}
95 |
96 |
97 | )
98 | }
99 |
100 | Dashboard.propTypes = {
101 | match: MatchPropTypes
102 | }
103 |
104 | const mapStateToProps = ({ trips: { trips } }) => ({ trips })
105 |
106 | export default connect(mapStateToProps)(Dashboard)
107 |
--------------------------------------------------------------------------------
/client/src/components/Modals/Modal.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import styled from "styled-components"
4 | import { flexCenterMixin, media } from "../../styles/theme/mixins"
5 |
6 | const ModalContainer = styled.div`
7 | ${flexCenterMixin};
8 | background: rgba(0, 0, 0, 0.75);
9 | height: 100vh;
10 | width: 100vw;
11 | z-index: 100;
12 | position: fixed;
13 | top: 0;
14 | left: 0;
15 | ${props => (props.isOpen ? "display: flex" : "display: none")};
16 | `
17 |
18 | const ModalWrapper = styled.div`
19 | will-change: opacity, transform;
20 | -webkit-animation: fadeIn 0.4s ease;
21 | animation: fadeIn 0.4s ease;
22 | background: white;
23 | ${flexCenterMixin};
24 | border-radius: 2px;
25 | flex-direction: column;
26 | justify-content: space-evenly;
27 | top: calc(50% - (66% / 2));
28 |
29 | .modal-inner {
30 | z-index: 101;
31 | max-width: 630px;
32 | padding: 2rem;
33 | height: 700px;
34 | max-height: 100vh;
35 | ${flexCenterMixin};
36 | flex-direction: column;
37 | align-items: unset;
38 | }
39 |
40 | .flow-header {
41 | padding: 3.25rem 1.75rem 0;
42 | margin-bottom: 24px;
43 | h4 {
44 | font-size: 2rem;
45 | font-weight: 600;
46 | }
47 | }
48 |
49 | .text-align-right {
50 | text-align: right;
51 | button {
52 | width: unset;
53 | padding-left: 3.5rem;
54 | padding-right: 3.5rem;
55 | }
56 | }
57 |
58 | .dual-buttons {
59 | display: flex;
60 | justify-content: space-around;
61 | button {
62 | width: unset;
63 | padding-left: 3.5rem;
64 | padding-right: 3.5rem;
65 | }
66 | }
67 |
68 | button.close-modal-button {
69 | right: 2rem;
70 | top: 2rem;
71 | }
72 |
73 | ${media.tablet`
74 | max-height: 100vh;
75 | margin-left: 1rem;
76 | margin-right: 1rem;
77 | height: 700px;
78 | `}
79 |
80 | ${media.phone`
81 | height: 530px;
82 | div, p {
83 | font-size: 0.825rem;
84 | }
85 | .modal-inner {
86 | max-height: 100vh;
87 | padding: 1rem;
88 | button.close-modal-button {
89 | right: 1.5rem;
90 | top: 0.25rem;
91 | font-size: 2.25rem;
92 | }
93 | }
94 | .flow-header {
95 | padding: 2.75rem 1rem 0 1rem;
96 | h4 {
97 | font-size: 1.525rem;
98 | text-align: center;
99 | }
100 | }
101 |
102 | `}
103 | `
104 |
105 | const Modal = ({ children, isOpen }) => (
106 |
107 | {children()}
108 |
109 | )
110 |
111 | Modal.propTypes = {
112 | children: PropTypes.func.isRequired,
113 | isOpen: PropTypes.bool.isRequired
114 | }
115 |
116 | export default Modal
117 |
--------------------------------------------------------------------------------
/client/src/styles/TripPicturesStyles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components"
2 | import { media } from "../styles/theme/mixins"
3 |
4 | export const TripPicturesStyles = styled.div`
5 | visibility: visible;
6 | ${media.tablet`
7 | visibility: ${props => (props.toggle ? "visible" : "hidden")};
8 | `}
9 |
10 | position: relative;
11 | /*
12 | top: unset;
13 | bottom: 254px;
14 | z-index: 5;
15 | right: 0; */
16 |
17 | ${media.tablet`
18 | bottom: 50px;
19 | left: 0;
20 | padding-left: 50px;
21 | `}
22 |
23 | background: white;
24 | span.chevron-icon {
25 | position: absolute;
26 | top: 0.25rem;
27 | left: 0.5rem;
28 | height: 1.5rem;
29 | width: 1.5rem;
30 | ${media.tablet`
31 | visibility: hidden;
32 | `}
33 | }
34 |
35 | .trip-pictures-wrapper {
36 | display: flex;
37 | flex-direction: column;
38 | height: 75px;
39 | max-width: 100vw;
40 | border: 1px dashed lightgray;
41 | margin: 0 18px 10px 16px;
42 | /* height: 150px; */
43 | /* width: 500px; */
44 | /* padding: 1.5rem 0.75rem 0; */
45 | ${media.tablet`
46 | width: 100%;
47 | padding: 0.75rem 0.75rem 0;
48 | height: 200px;
49 | `}
50 | }
51 | .chevron-wrapper {
52 | cursor: pointer;
53 | }
54 |
55 | .trip-pictures-header {
56 | margin-bottom: 0.5rem;
57 | padding-right: 0.25rem;
58 | text-align: right;
59 |
60 | /* NEW STYLES */
61 | position: absolute;
62 | top: -56px;
63 | right: 15px;
64 | }
65 |
66 | .upload-button {
67 | position: relative;
68 | width: 145px;
69 | margin: 0;
70 | input {
71 | position: absolute;
72 | top: -1;
73 | left: -1;
74 | width: 145px;
75 | height: 38px;
76 | opacity: 0;
77 | }
78 | }
79 |
80 | .trip-pictures {
81 | display: flex;
82 | flex-direction: column;
83 | .trip-picture-list {
84 | display: flex;
85 | flex-wrap: wrap;
86 | overflow-y: scroll;
87 | }
88 | }
89 |
90 | ${props => props.isHidden && isHiddenStyles}
91 | `
92 |
93 | const isHiddenStyles = css`
94 | visibility: hidden !important;
95 | .trip-pictures-wrapper {
96 | height: 150px;
97 | padding: 12px 12px 8px 12px;
98 | width: 60px;
99 | span.chevron-icon {
100 | top: 0.35rem;
101 | left: 0.525rem;
102 | height: 2.5rem;
103 | width: 2.5rem;
104 | }
105 | }
106 |
107 | i {
108 | position: absolute;
109 | bottom: 12px;
110 | font-size: 2rem;
111 | }
112 |
113 | .trip-pictures {
114 | visibility: hidden;
115 | }
116 | `
117 |
118 | export const ImageThumbnails = styled.div`
119 | display: flex;
120 | flex-direction: row;
121 | padding: 0 0.25rem;
122 | width: 25%;
123 | img {
124 | width: 100%;
125 | height: 100%;
126 | }
127 | `
128 |
--------------------------------------------------------------------------------
/client/src/components/Billing/AccountType.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { connect } from "react-redux"
3 | import { compose } from "redux"
4 | import { Link, withRouter } from "react-router-dom"
5 | import styled from "styled-components"
6 | import PropTypes from "prop-types"
7 |
8 | import { cancelSubscription } from "../../redux/actions/billing"
9 | import { Button } from "../../styles/theme/styledComponents"
10 | import { media } from "../../styles/theme/mixins"
11 |
12 | const Container = styled.div`
13 | display: flex;
14 | flex-direction: column;
15 | width: 100%;
16 | max-width: 525px;
17 |
18 | div {
19 | display: grid;
20 | grid-template-columns: 2fr 1fr;
21 | margin-top: 10px;
22 | padding: 30px;
23 | background: white;
24 | border: 1px solid #d73a49;
25 | border-radius: 3px;
26 | box-shadow: 0px 8px 24px rgba(13, 13, 18, 0.04);
27 | }
28 |
29 | a {
30 | font-weight: 500;
31 | }
32 |
33 | button {
34 | margin: 0;
35 | height: 36px;
36 | }
37 |
38 | ${media.phone`
39 | max-width: 300px;
40 |
41 | div {
42 | grid-template-columns: 1fr;
43 | grid-template-rows: 2fr 1fr;
44 | grid-gap: 15px;
45 | padding: 15px;
46 | }
47 | `}
48 | `
49 |
50 | const AccountType = ({
51 | cancelSubscription,
52 | id,
53 | history,
54 | isSubscribed,
55 | subscribeId
56 | }) => {
57 | const handleCancel = () => {
58 | if (subscribeId) {
59 | cancelSubscription({ id, subscribeId })
60 | }
61 | }
62 | const upgradeButton = (
63 | history.push("/app/upgrade")}>Upgrade
64 | )
65 | const unsubscribeButton = Unsubscribe
66 |
67 | return (
68 |
69 | Change your plan
70 |
71 | Current plan: {isSubscribed ? "Premium" : "Free"}
72 |
73 |
74 |
75 | If you downgrade, you’ll lose access to{" "}
76 |
77 | unlimited archived trips and other features.
78 |
79 |
80 | {isSubscribed ? unsubscribeButton : upgradeButton}
81 |
82 |
83 | )
84 | }
85 |
86 | AccountType.propTypes = {
87 | isSubscribed: PropTypes.bool.isRequired,
88 | id: PropTypes.string.isRequired,
89 | subscribeId: PropTypes.string,
90 | cancelSubscription: PropTypes.func.isRequired,
91 | history: PropTypes.shape({
92 | push: PropTypes.func.isRequired
93 | }).isRequired
94 | }
95 |
96 | const mapStateToProps = ({ auth }) => ({
97 | isSubscribed: auth.user.subscribed,
98 | id: auth.user.id,
99 | subscribeId: auth.user.subscribeId
100 | })
101 |
102 | export default compose(
103 | withRouter,
104 | connect(
105 | mapStateToProps,
106 | { cancelSubscription }
107 | )
108 | )(AccountType)
109 |
--------------------------------------------------------------------------------
/client/src/styles/theme/variables.js:
--------------------------------------------------------------------------------
1 | // * Root Colors ----------------------
2 | const themeColor = localStorage.getItem("themeColor")
3 | const transparent = "rgba(0, 0, 0, 0)"
4 |
5 | const white = "#fff"
6 | const offWhite = "#f0f0f0"
7 | const ghostWhite = "#f5f5fa" // light light blue
8 | const lightGray = "#bababa"
9 | const midGray = "#646565"
10 | const darkGray = "#333333"
11 | const offBlack = "#222222"
12 | const black = "rgb(32, 34, 51)"
13 |
14 | const primary = themeColor || "rgba(244, 105, 4, 1)"
15 | // const primaryHover = "rgba(244, 105, 4, 0.8)"
16 | const primaryHover = "#f9873b"
17 | const primaryLight = "#facbb1"
18 | const primaryDark = "#e4580d"
19 | const secondary = midGray
20 | const secondaryDark = darkGray
21 | const tertiary = "rgba(0, 90, 132, 1)"
22 | const tertiaryHover = "rgba(0, 90, 132, 0.8)"
23 | const tertiaryLight = "#899ac6"
24 |
25 | const linkColor = primaryDark
26 | const linkColorHover = "#526699"
27 | // const linkColorHover = "#1e306e"
28 |
29 | export const theme = {
30 | // * Misc. -----------------------------------
31 | boxShadow: "0 2px 16px 1px rgba(0, 0, 0, 0.15)",
32 |
33 | // * Default Dimensions ----------------------
34 | sidebarWidth: 215,
35 | navHeight: "3.125rem",
36 |
37 | // * Buttons ---------------------------------
38 | // Standard
39 | btnWidth: 170,
40 | btnHeight: 47,
41 | btnBorderRadius: 25,
42 | btnFontSize: 1.6,
43 | btnTextColor: white,
44 | btnBgColor: transparent,
45 | btnBorderColor: primary,
46 | // Primary
47 | btnPrimaryBgColor: primary,
48 | // Dark Primary
49 | btnDarkPrimaryBgColor: primaryDark,
50 | // Dark
51 | // btnDarkBorderColor: primaryExtraDark,
52 |
53 | // * Inputs ----------------------------------
54 | inputBgColor: white,
55 | inputTextColor: black,
56 | inputBorderColor: lightGray,
57 | placeholderColor: midGray,
58 |
59 | // * Color Theme Variables -------------------
60 | // Primary Styles
61 | primary,
62 | primaryHover,
63 | secondary,
64 | tertiary,
65 | tertiaryHover,
66 | primaryDark,
67 | secondaryDark,
68 | tertiaryLight,
69 | primaryLight,
70 |
71 | linkColor,
72 | linkColorHover,
73 |
74 | transparent,
75 | white,
76 | offWhite,
77 | ghostWhite,
78 | lightGray,
79 | midGray,
80 | darkGray,
81 | offBlack,
82 | black,
83 |
84 | contentBackground: ghostWhite,
85 | textColorDark: white,
86 | inputError: secondaryDark,
87 | menuBg: offWhite,
88 | // activeItem: tertiaryLight,
89 | navTabColor: tertiary,
90 |
91 | // Links
92 | linkSelectedBg: null,
93 | linkSelected: null,
94 | linkHoverBg: null,
95 | linkHover: null,
96 |
97 | // * Typeface --------------------------------
98 | // Text
99 | lineHeight: null,
100 | primaryText: offBlack,
101 | lightText: midGray,
102 | medTextLight: midGray,
103 | lightTextOnDark: lightGray,
104 | // Headers
105 | h1: 2.4
106 | }
107 |
--------------------------------------------------------------------------------
/client/src/components/Dropdown.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { connect } from "react-redux"
3 | import PropTypes from "prop-types"
4 | import { Link } from "react-router-dom"
5 | import {
6 | Dropdown,
7 | DropdownToggle,
8 | DropdownMenu,
9 | DropdownItem
10 | } from "reactstrap"
11 | import { logout } from "../redux/actions/auth"
12 | import * as s from "../styles/Dropdown.styles"
13 | import ChevronSvg from "./icons/ChevronSvg"
14 | import { UserPropTypes } from "./propTypes"
15 |
16 | class NavDropdown extends Component {
17 | state = {
18 | dropdownOpen: false
19 | }
20 |
21 | toggle = () => {
22 | this.setState(prevState => ({
23 | dropdownOpen: !prevState.dropdownOpen
24 | }))
25 | }
26 |
27 | onMouseEnter = () => {
28 | this.setState({ dropdownOpen: true })
29 | }
30 |
31 | onMouseLeave = () => {
32 | this.setState({ dropdownOpen: false })
33 | }
34 |
35 | handleLogout = e => {
36 | e.preventDefault()
37 | this.props.logout()
38 | }
39 |
40 | render() {
41 | const { user } = this.props
42 | return (
43 |
44 |
50 |
51 | {!user.displayName && user.email}
52 | {user.displayName && "Hi, " + user.displayName + "!"}
53 |
54 |
55 |
56 |
57 |
58 | Settings
59 |
60 |
61 |
62 |
63 |
64 | Billing
65 |
66 |
67 |
68 |
69 |
70 | Profile
71 |
72 |
73 |
74 |
75 |
76 | Log out
77 |
78 |
79 |
80 |
81 |
82 | )
83 | }
84 | }
85 |
86 | NavDropdown.propTypes = {
87 | user: UserPropTypes.isRequired,
88 | logout: PropTypes.func.isRequired
89 | }
90 |
91 | const mapStateToProps = state => ({ user: state.auth.user })
92 |
93 | const mapDispatchToProps = {
94 | logout
95 | }
96 | export default connect(
97 | mapStateToProps,
98 | mapDispatchToProps
99 | )(NavDropdown)
100 |
--------------------------------------------------------------------------------
/server/tests/modules/testUser.js:
--------------------------------------------------------------------------------
1 | import request from "supertest"
2 | import app from "../../src/server"
3 |
4 | import * as mock from "../mock"
5 |
6 | let token
7 | let userID
8 |
9 | describe("Test User model and routes", () => {
10 | beforeAll(async done => {
11 | const response = await request(app)
12 | .post("/api/login")
13 | .send({ email: mock.userOne.email, password: "testpass" })
14 | userID = response.body.user.id
15 | token = response.body.token
16 | return done()
17 | })
18 | test("GET all users", done => {
19 | request(app)
20 | .get("/api/users")
21 | .set("Authorization", `Bearer ${token}`)
22 | .then(response => {
23 | expect(response.statusCode).toBe(200)
24 | expect(response.body.length).toEqual(2)
25 | done()
26 | })
27 | })
28 | test("POST create new user", done => {
29 | request(app)
30 | .post("/api/users")
31 | .set("Authorization", `Bearer ${token}`)
32 | .send(mock.userThree)
33 | .then(response => {
34 | expect(response.statusCode).toBe(201)
35 | expect(response.body.id).toBeTruthy()
36 | expect(response.body.email).toBe("email@yahoo.com")
37 | expect(response.body.subscribed).toEqual(false)
38 | done()
39 | })
40 | })
41 | test("GET single user", done => {
42 | request(app)
43 | .get(`/api/users/${userID}`)
44 | .set("Authorization", `Bearer ${token}`)
45 | .then(response => {
46 | expect(response.statusCode).toBe(200)
47 | expect(response.body.id).toEqual(userID)
48 | expect(response.body.email).toBe("email@hotmail.com")
49 | done()
50 | })
51 | })
52 | test("PUT update a user", done => {
53 | const updated = { displayName: "Updated Display Name User 1" }
54 | request(app)
55 | .put(`/api/users/${userID}`)
56 | .set("Authorization", `Bearer ${token}`)
57 | .send(updated)
58 | .then(response => {
59 | expect(response.statusCode).toBe(200)
60 | expect(response.body.id).toEqual(userID)
61 | expect(response.body.displayName).toBe("Updated Display Name User 1")
62 | done()
63 | })
64 | })
65 | test("GET all trips from a user", done => {
66 | request(app)
67 | .get(`/api/users/${userID}/trips`)
68 | .set("Authorization", `Bearer ${token}`)
69 | .then(response => {
70 | expect(response.statusCode).toBe(200)
71 | expect(response.body.length).toEqual(3)
72 | expect(response.body[0].userId).toBe(userID)
73 | expect(response.body[0].name).toBe("tripOne")
74 | done()
75 | })
76 | })
77 | test("DELETE remove a user", done => {
78 | request(app)
79 | .delete(`/api/users/${userID}`)
80 | .set("Authorization", `Bearer ${token}`)
81 | .then(response => {
82 | expect(response.statusCode).toBe(202)
83 | expect(response.body.user.id).toEqual(userID)
84 | expect(response.body.user.email).toBe("email@hotmail.com")
85 | expect(response.body.user.displayName).toBe(
86 | "Updated Display Name User 1"
87 | )
88 | expect(response.body.msg).toBe("User was deleted")
89 | done()
90 | })
91 | })
92 | })
93 |
--------------------------------------------------------------------------------
/client/src/redux/actions/settings.js:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 |
3 | import { SERVER_URI } from "../../config"
4 | import {
5 | INI_UPDATE_SETTINGS,
6 | CLOSE_MODAL,
7 | UPDATE_SETTINGS_SUCCESS,
8 | UPDATE_SETTINGS_FAILURE,
9 | UPDATE_USER_IN_STORE
10 | } from "./types"
11 |
12 | import { normalizeErrorMsg } from "../../utils/selectors"
13 | import { toast } from "react-toastify"
14 |
15 | const token = localStorage.getItem("token")
16 | if (token) {
17 | axios.defaults.headers.common["Authorization"] = token
18 | }
19 |
20 | export const updateUserWithMsg = (userId, values, msg) => dispatch => {
21 | if (!axios.defaults.headers.common["Authorization"]) {
22 | axios.defaults.headers.common["Authorization"] = localStorage.getItem(
23 | "token"
24 | )
25 | }
26 | dispatch({ type: INI_UPDATE_SETTINGS })
27 | axios
28 | .put(`${SERVER_URI}/users/${userId}`, { ...values })
29 | .then(res => {
30 | const user = res.data
31 | dispatch({ type: UPDATE_SETTINGS_SUCCESS })
32 | dispatch({ type: UPDATE_USER_IN_STORE, payload: user })
33 | toast.success(msg, {
34 | position: toast.POSITION.BOTTOM_RIGHT
35 | })
36 | dispatch({ type: CLOSE_MODAL })
37 | })
38 | .catch(err => {
39 | dispatch({
40 | type: UPDATE_SETTINGS_FAILURE,
41 | payload: normalizeErrorMsg(err)
42 | })
43 | toast.error(normalizeErrorMsg(err), {
44 | position: toast.POSITION.BOTTOM_RIGHT
45 | })
46 | })
47 | }
48 |
49 | export const updateEmail = (userId, email) => dispatch => {
50 | dispatch({ type: INI_UPDATE_SETTINGS })
51 |
52 | axios
53 | .put(`${SERVER_URI}/users/${userId}`, { email })
54 | .then(res => {
55 | const user = res.data
56 | dispatch({ type: UPDATE_SETTINGS_SUCCESS })
57 | dispatch({ type: UPDATE_USER_IN_STORE, payload: user })
58 | toast.success("Your new email has been updated.", {
59 | position: toast.POSITION.BOTTOM_RIGHT
60 | })
61 | })
62 | .catch(err => {
63 | dispatch({
64 | type: UPDATE_SETTINGS_FAILURE,
65 | payload: normalizeErrorMsg(err)
66 | })
67 | toast.error(normalizeErrorMsg(err), {
68 | position: toast.POSITION.BOTTOM_RIGHT
69 | })
70 | })
71 | }
72 |
73 | export const updatePassword = (email, oldPassword, newPassword) => dispatch => {
74 | if (oldPassword === newPassword) {
75 | toast.error("Your old and new password are the same.", {
76 | position: toast.POSITION.BOTTOM_RIGHT
77 | })
78 | }
79 |
80 | dispatch({ type: INI_UPDATE_SETTINGS })
81 |
82 | axios
83 | .post(`${SERVER_URI}/changePassword`, {
84 | email,
85 | oldPassword,
86 | newPassword
87 | })
88 | .then(() => {
89 | dispatch({ type: UPDATE_SETTINGS_SUCCESS })
90 | toast.success("Your new password has been updated.", {
91 | position: toast.POSITION.BOTTOM_RIGHT
92 | })
93 | })
94 | .catch(err => {
95 | dispatch({
96 | type: UPDATE_SETTINGS_FAILURE,
97 | payload: normalizeErrorMsg(err)
98 | })
99 | toast.error(normalizeErrorMsg(err), {
100 | position: toast.POSITION.BOTTOM_RIGHT
101 | })
102 | })
103 | }
104 |
--------------------------------------------------------------------------------
/client/src/components/icons/ChartSvg.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import styled from "styled-components"
4 |
5 | const ChartIconStyles = styled.div`
6 | svg {
7 | /* enable-background: new 0 0 ${props => props.width} ${props =>
8 | props.height}; */
9 | }
10 | `
11 |
12 | const ChartIcon = ({ height = 20, width = 20 }) => (
13 |
14 |
21 |
22 |
23 |
24 |
48 |
49 |
50 |
51 |
52 |
53 | )
54 |
55 | ChartIcon.propTypes = {
56 | height: PropTypes.number,
57 | width: PropTypes.number
58 | }
59 |
60 | export default ChartIcon
61 |
--------------------------------------------------------------------------------
/client/src/components/forms/RecoverPassword.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { Link } from "react-router-dom"
3 | import { Button, GhostInput } from "../../styles/theme/styledComponents"
4 | import styled from "styled-components"
5 |
6 | // MOVE TO REDUX
7 | import axios from "axios"
8 | import { SERVER_URI } from "../../config"
9 |
10 | export const RecoverPasswordStyles = styled.div`
11 | height: 100%;
12 | width: 100%;
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | justify-content: center;
17 | form {
18 | width: 300px;
19 | }
20 | p,
21 | input {
22 | margin-bottom: 0.625rem;
23 | font-size: 1.125rem;
24 | }
25 |
26 | input,
27 | button {
28 | width: 100%;
29 | }
30 | p {
31 | font-size: 1.125rem;
32 | }
33 | a {
34 | margin-top: 1rem;
35 | }
36 | .reset-password-form-wrapper {
37 | display: flex;
38 | flex-direction: column;
39 | align-items: center;
40 | justify-content: center;
41 | max-width: 430px;
42 | margin: 0 auto;
43 | }
44 | .reset-password-form-sent-wrapper {
45 | max-width: 360px;
46 | text-align: center;
47 | p {
48 | text-align: left;
49 | margin-top: 1rem;
50 | margin-bottom: 0.75rem;
51 | }
52 | }
53 | .password-reset-btn {
54 | padding: 0.625rem 1.25rem;
55 | font-size: 1.125rem;
56 | }
57 | `
58 |
59 | class RecoverPassword extends Component {
60 | state = {
61 | email: "",
62 | submitted: false
63 | }
64 |
65 | handleChange = e => {
66 | this.setState({ email: e.target.value })
67 | }
68 |
69 | sendPasswordResetEmail = e => {
70 | e.preventDefault()
71 | const { email } = this.state
72 | axios.post(`${SERVER_URI}/reset_password/user/${email}`)
73 | this.setState({ email: "", submitted: true })
74 | }
75 |
76 | render() {
77 | const { email, submitted } = this.state
78 |
79 | return (
80 |
81 | Reset your password
82 | {submitted ? (
83 |
84 |
85 | If that account is in our system, we emailed you a link to reset
86 | your password.
87 |
88 |
89 | Return to sign in
90 |
91 |
92 | ) : (
93 |
94 |
95 | It happens to the best of us. Enter your email and we'll send you
96 | reset instructions.
97 |
98 |
108 |
I remember my password
109 |
110 | )}
111 |
112 | )
113 | }
114 | }
115 |
116 | export default RecoverPassword
117 |
--------------------------------------------------------------------------------
/client/src/components/forms/SettingsForm.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { connect } from "react-redux"
3 | import { Formik } from "formik"
4 | import styled from "styled-components"
5 | import PropTypes from "prop-types"
6 |
7 | import { UserPropTypes } from "../propTypes"
8 | import { Form } from "../../styles/theme/styledComponents"
9 | import { CustomInputWithError, CustomButtonWithError } from "./customInputs"
10 | import { updateEmail, updatePassword } from "../../redux/actions/settings"
11 | import { settingsValidations as validate } from "./formValidations"
12 | import { authFormErrorsMixin } from "../../styles/theme/mixins"
13 |
14 | const SettingsFormStyles = styled.div`
15 | ${authFormErrorsMixin};
16 | `
17 |
18 | const SettingsForm = ({
19 | user,
20 | updateEmail,
21 | updatePassword,
22 | updateSettingsError
23 | }) => (
24 | {
32 | actions.setSubmitting(false)
33 |
34 | if (email !== user.email) {
35 | updateEmail(user.id, email)
36 | }
37 |
38 | if (oldPassword && newPassword) {
39 | updatePassword(user.email, oldPassword, newPassword)
40 | }
41 | }}
42 | render={({
43 | values,
44 | handleBlur,
45 | handleChange,
46 | handleSubmit,
47 | isSubmitting
48 | }) => (
49 |
50 |
51 |
82 |
83 |
84 | )}
85 | />
86 | )
87 |
88 | SettingsForm.propTypes = {
89 | user: UserPropTypes.isRequired,
90 | updateEmail: PropTypes.func.isRequired,
91 | updatePassword: PropTypes.func.isRequired,
92 | updateSettingsError: PropTypes.object
93 | }
94 |
95 | const mapStateToProps = state => ({
96 | updateSettingsError: state.settings.error,
97 | user: state.auth.user
98 | })
99 |
100 | const mapDispatchToProps = { updateEmail, updatePassword }
101 |
102 | export default connect(
103 | mapStateToProps,
104 | mapDispatchToProps
105 | )(SettingsForm)
106 |
--------------------------------------------------------------------------------
/server/src/api/resources/email/email.controller.js:
--------------------------------------------------------------------------------
1 | /*** Documentation:
2 |
3 | * To make this token a one-time-use token, I encourage you to
4 | * use the user’s current password hash in conjunction with
5 | * the user’s created date (in ticks) as the secret key to
6 | * generate the JWT. This helps to ensure that if the user’s
7 | * password was the target of a previous attack (on an unrelated website),
8 | * then the user’s created date will make the secret key unique
9 | * from the potentially leaked password.
10 |
11 | * With the combination of the user’s password hash and created date,
12 | * the JWT will become a one-time-use token, because once the user
13 | * has changed their password, it will generate a new password hash
14 | * invalidating the secret key that references the old password
15 | * Reference: https://www.smashingmagazine.com/2017/11/safe-password-resets-with-json-web-tokens/
16 | **/
17 |
18 | import jwt from "jsonwebtoken"
19 | import bcrypt from "bcryptjs"
20 | import { User } from "../user/user.model"
21 | import {
22 | transporter,
23 | getPasswordResetURL,
24 | resetPasswordTemplate
25 | } from "../../modules/email"
26 |
27 | // `secret` is passwordHash concatenated with user's createdAt,
28 | // so if someones gets a user token they still need a timestamp to intercept.
29 | export const usePasswordHashToMakeToken = ({
30 | password: passwordHash,
31 | _id: userId,
32 | createdAt
33 | }) => {
34 | const secret = passwordHash + "-" + createdAt
35 | const token = jwt.sign({ userId }, secret, {
36 | expiresIn: 3600 // 1 hour
37 | })
38 | return token
39 | }
40 |
41 | /*** Calling this function with a registered user's email sends an email IRL ***/
42 | /*** I think Nodemail has a free service specifically designed for mocking ***/
43 | export const sendPasswordResetEmail = async (req, res) => {
44 | const { email } = req.params
45 | let user
46 | try {
47 | user = await User.findOne({ email }).exec()
48 | } catch (err) {
49 | res.status(404).json("No user with that email")
50 | }
51 | const token = usePasswordHashToMakeToken(user)
52 | const url = getPasswordResetURL(user, token)
53 | const emailTemplate = resetPasswordTemplate(user, url)
54 |
55 | const sendEmail = () => {
56 | transporter.sendMail(emailTemplate, (err, info) => {
57 | if (err) {
58 | res.status(500).json("Error sending email")
59 | }
60 | console.log(`** Email sent **`, info.response)
61 | })
62 | }
63 | sendEmail()
64 | }
65 |
66 | export const receiveNewPassword = (req, res) => {
67 | const { userId, token } = req.params
68 | const { password } = req.body
69 |
70 | User.findOne({ _id: userId })
71 |
72 | .then(user => {
73 | const secret = user.password + "-" + user.createdAt
74 | const payload = jwt.decode(token, secret)
75 | if (payload.userId === user.id) {
76 | bcrypt.genSalt(10, function(err, salt) {
77 | if (err) return
78 | bcrypt.hash(password, salt, function(err, hash) {
79 | if (err) return
80 | User.findOneAndUpdate({ _id: userId }, { password: hash })
81 | .then(() => res.status(202).json("Password changed accepted"))
82 | .catch(err => res.status(500).json(err))
83 | })
84 | })
85 | }
86 | })
87 |
88 | .catch(() => {
89 | res.status(404).json("Invalid user")
90 | })
91 | }
92 |
--------------------------------------------------------------------------------
/client/src/components/Billing/PaymentDetails.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { Elements } from "react-stripe-elements"
3 | import { connect } from "react-redux"
4 | import styled from "styled-components"
5 | import PropTypes from "prop-types"
6 |
7 | import {
8 | closeCheckoutForm,
9 | openCheckoutForm
10 | } from "../../redux/actions/billing"
11 | import CheckoutForm from "../forms/CheckoutForm"
12 | import Pending from "./Pending"
13 | import StripeProvider from "./StripeProvider"
14 | import { media } from "../../styles/theme/mixins"
15 |
16 | const PaymentContainer = styled.div`
17 | display: flex;
18 | flex-direction: column;
19 | margin: 50px auto 0;
20 | padding: 50px 0;
21 | max-width: 500px;
22 | border-radius: 10px;
23 | box-shadow: 0px 8px 24px rgba(13, 13, 18, 0.04);
24 | background: white;
25 |
26 | h4 {
27 | padding: 0 50px;
28 | }
29 |
30 | .detail {
31 | display: flex;
32 | flex-direction: column;
33 | justify-content: space-between;
34 | background-color: #fbfbfd;
35 | border-top: 1px solid #e6e6eb;
36 | border-bottom: 1px solid #e6e6eb;
37 | box-shadow: inset 0 2px 6px 0 rgba(0, 0, 0, 0.025);
38 | margin-bottom: 0;
39 | margin-top: 16px;
40 | padding: 20px 50px;
41 | }
42 |
43 | .detail-bold {
44 | font-weight: 500;
45 | }
46 |
47 | .detail-text {
48 | display: flex;
49 | justify-content: space-between;
50 | }
51 |
52 | ${media.phone`
53 | width: 350px;
54 |
55 | h4 {
56 | padding: 0 25px;
57 | }
58 |
59 | .detail {
60 | padding: 20px 25px;
61 | }
62 | `}
63 | `
64 |
65 | class PaymentDetails extends Component {
66 | componentDidMount() {
67 | this.props.openCheckoutForm()
68 | }
69 |
70 | componentWillUnmount() {
71 | this.props.closeCheckoutForm()
72 | }
73 |
74 | componentDidUpdate(prevProps) {
75 | const { history, pending } = this.props
76 | if (prevProps.pending && !pending) {
77 | history.push("/app/billing")
78 | }
79 | }
80 |
81 | render() {
82 | return (
83 |
84 | Payment details
85 |
86 |
87 | Pro Plan
88 |
89 |
90 | Total:
91 | $9.99 /yr
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | {this.props.pending && }
100 |
101 | )
102 | }
103 | }
104 |
105 | PaymentDetails.propTypes = {
106 | pending: PropTypes.bool.isRequired,
107 | closeCheckoutForm: PropTypes.func.isRequired,
108 | openCheckoutForm: PropTypes.func.isRequired,
109 | history: PropTypes.shape({
110 | push: PropTypes.func.isRequired
111 | }).isRequired
112 | }
113 |
114 | const mapStateToProps = state => ({
115 | pending: state.billing.pending
116 | })
117 |
118 | const mapDispatchToProps = {
119 | closeCheckoutForm,
120 | openCheckoutForm
121 | }
122 |
123 | export default connect(
124 | mapStateToProps,
125 | mapDispatchToProps
126 | )(PaymentDetails)
127 |
--------------------------------------------------------------------------------
/client/src/styles/theme/GlobalStyles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components"
2 | import { fontDeclarations, fontMixin } from "./mixins"
3 |
4 | export const GlobalStyles = createGlobalStyle`
5 | ${fontDeclarations};
6 | html, body {
7 | overflow: hidden;
8 | height: 100vh;
9 | }
10 |
11 | html {
12 | background: ${props => props.theme.contentBackground};
13 | box-sizing: border-box;
14 | }
15 |
16 | body {
17 | margin: 0;
18 | padding: 0;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 |
23 | ${fontMixin};
24 |
25 | /* Bootstrap Overrides */
26 | ol,
27 | ul {
28 | margin: 0;
29 | padding: 0;
30 | list-style-type: none;
31 | }
32 |
33 | a {
34 | will-change: color;
35 | -webkit-transition: color 0.15s ease;
36 | transition: color 0.15s ease;
37 |
38 | color: ${props => props.theme.linkColor};
39 | &:hover {
40 | color: ${props => props.theme.linkColorHover};
41 | text-decoration: none;
42 | }
43 | }
44 |
45 | button:focus, input:focus, textarea:focus {
46 | outline: 0;
47 | }
48 |
49 | button:hover {
50 | text-decoration: none;
51 | }
52 |
53 | .main-wrapper {
54 | display: flex;
55 | flex-direction: column;
56 | height: 100vh;
57 | /* margin-top: ${props => "-" + props.theme.navHeight}; */
58 | }
59 | .main-wrapper-auth {
60 | background: ${props => props.theme.contentBackground};
61 | }
62 |
63 | .main-content {
64 | display: flex;
65 | height: 100%;
66 | }
67 |
68 | span.form-error {
69 | font-size: 12px;
70 | color: ${props => props.theme.secondaryDark};
71 | }
72 |
73 | button.btn-ghost, a.btn-ghost, button.ghost-btn, a.ghost-btn {
74 | background-color: ${props => props.theme.white};
75 | border-color: ${props => props.theme.white};
76 | color: ${props => props.theme.black};
77 | border: 0;
78 | display: inline-block;
79 | /* padding: 0.75rem 1.5rem 0.8125rem; */
80 | padding: 0.75rem 1.5rem;
81 | min-width: 14rem;
82 | border-radius: 0.25rem;
83 | font-size: 1rem;
84 | font-weight: 500;
85 | /* line-height: 1.3; */
86 | text-align: center;
87 | -webkit-box-shadow: 0 0.3125rem 0.9375rem 0 rgba(0,0,0,.05), 0 0 0 0.0625rem rgba(0,0,0,.03), 0 0.0625rem 0 0 rgba(0,0,0,.05), 0 0.0625rem 0.1875rem 0 rgba(0,0,0,.01);
88 | box-shadow: 0 0.3125rem 0.9375rem 0 rgba(0,0,0,.05), 0 0 0 0.0625rem rgba(0,0,0,.03), 0 0.0625rem 0 0 rgba(0,0,0,.05), 0 0.0625rem 0.1875rem 0 rgba(0,0,0,.01);
89 | -webkit-transition: background-color .15s ease-out,color .15s ease-out,-webkit-box-shadow .15s ease-out;
90 | transition: background-color .15s ease-out,color .15s ease-out,-webkit-box-shadow .15s ease-out;
91 | &:hover, &:focus {
92 | background-color: ${props => props.theme.white};
93 | border-color: ${props => props.theme.white};
94 | color: #526699;
95 | -webkit-box-shadow: 0 0.3125rem 0.9375rem 0 rgba(0,0,0,.05), 0 0 0 0.0625rem rgba(0,0,0,.03), 0 0.125rem 0.0625rem 0 rgba(0,0,0,.1), 0 0.0625rem 0.1875rem 0 rgba(0,0,0,.01);
96 | box-shadow: 0 0.3125rem 0.9375rem 0 rgba(0,0,0,.05), 0 0 0 0.0625rem rgba(0,0,0,.03), 0 0.125rem 0.0625rem 0 rgba(0,0,0,.1), 0 0.0625rem 0.1875rem 0 rgba(0,0,0,.01);
97 | }
98 | &:focus {
99 | outline: 0;
100 | }
101 | }
102 | `
103 |
--------------------------------------------------------------------------------