├── .eslintignore ├── .eslintrc ├── .firebaserc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── cpx.json └── settings.json ├── CRA.md ├── LICENSE ├── README.md ├── branding ├── ads │ ├── poster-01.png │ └── poster.ai ├── fonts │ ├── PT Serif │ │ ├── OFL.txt │ │ ├── PTSerif-Bold.ttf │ │ ├── PTSerif-BoldItalic.ttf │ │ ├── PTSerif-Italic.ttf │ │ ├── PTSerif-Regular.ttf │ │ └── PT_Serif.zip │ └── Poppins │ │ ├── OFL.txt │ │ ├── Poppins-Black.ttf │ │ ├── Poppins-BlackItalic.ttf │ │ ├── Poppins-Bold.ttf │ │ ├── Poppins-BoldItalic.ttf │ │ ├── Poppins-ExtraBold.ttf │ │ ├── Poppins-ExtraBoldItalic.ttf │ │ ├── Poppins-ExtraLight.ttf │ │ ├── Poppins-ExtraLightItalic.ttf │ │ ├── Poppins-Italic.ttf │ │ ├── Poppins-Light.ttf │ │ ├── Poppins-LightItalic.ttf │ │ ├── Poppins-Medium.ttf │ │ ├── Poppins-MediumItalic.ttf │ │ ├── Poppins-Regular.ttf │ │ ├── Poppins-SemiBold.ttf │ │ ├── Poppins-SemiBoldItalic.ttf │ │ ├── Poppins-Thin.ttf │ │ ├── Poppins-ThinItalic.ttf │ │ └── Poppins.zip └── logo │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── g-logo-01.png │ ├── g-logo-inverse-01.png │ ├── grouber-192.ai │ ├── grouber-512.ai │ ├── grouber-logo-01.png │ ├── grouber-logo-01.svg │ ├── grouber-logo-inverse-01.png │ ├── grouber-logo.ai │ ├── logo192.png │ ├── logo512.png │ └── site.webmanifest ├── database.rules.json ├── firebase.json ├── functions ├── .gitignore ├── lib │ ├── DistanceMatrix.js │ ├── index.js │ └── priorityQueue.js ├── package-lock.json ├── package.json ├── src │ ├── DistanceMatrix.ts │ ├── index.ts │ └── priorityQueue.ts ├── tsconfig.json └── tslint.json ├── images ├── RSVPPage.jpg ├── appMap.jpg ├── branding.png ├── dashboard.jpg ├── homescreen.jpg └── homescreen.png ├── package-lock.json ├── package.json ├── public ├── cover.png ├── favicon.ico ├── index.html ├── login_art.png ├── logo.png ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── _types │ ├── address.d.ts │ ├── event.d.ts │ └── people.d.ts ├── components │ ├── DemoCard.tsx │ ├── EventForm.tsx │ ├── EventList.tsx │ ├── Footer.tsx │ ├── GuestForm.tsx │ ├── Header.tsx │ ├── LinkDisplay.tsx │ ├── LinkSharing.tsx │ ├── LoginForm.tsx │ ├── Map │ │ ├── id-to-lat-lng.ts │ │ ├── index.tsx │ │ └── theme.ts │ ├── Notification.tsx │ └── PeopleList.tsx ├── hooks │ ├── UseAutocompletePlaces.tsx │ ├── UseFetch.tsx │ └── useEventPeople.tsx ├── index.tsx ├── pages │ ├── 404.tsx │ ├── Home.tsx │ ├── dashboard.tsx │ ├── form.tsx │ └── login.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts ├── styles │ ├── App.module.scss │ ├── constants.scss │ ├── index.scss │ └── theme.ts └── utils │ └── sassHelper.tsx └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | coverage/ 4 | public/ 5 | cypress/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2020, // Allows for the parsing of modern ECMAScript features 6 | "sourceType": "module", // Allows for the use of imports 7 | "ecmaFeatures": { 8 | "jsx": true // Allows for the parsing of JSX 9 | } 10 | }, 11 | "settings": { 12 | "react": { 13 | "version": "detect" // Tells eslint-plugin-react to automatically detect the version of React to use 14 | } 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint", 18 | "react" 19 | ], 20 | "extends": [ 21 | "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react 22 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin 23 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 24 | "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 25 | ], 26 | "rules": { 27 | "@typescript-eslint/explicit-module-boundary-types": "off" 28 | } 29 | } -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "find-my-carpool" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | name: Build and Deploy 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repo 13 | uses: actions/checkout@master 14 | - name: Install Dependencies 15 | run: npm install 16 | - name: Build 17 | run: CI=false npm run build 18 | - name: Deploy to Firebase Hosting 19 | uses: w9jds/firebase-action@master 20 | with: 21 | args: deploy --only hosting 22 | env: 23 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 24 | - name: Install Dependencies for Firebase Functions 25 | run: npm install 26 | working-directory: ./functions 27 | - name: Deploy to Firebase Functions 28 | uses: w9jds/firebase-action@master 29 | with: 30 | args: deploy --only functions 31 | env: 32 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | *.log 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 4 7 | } -------------------------------------------------------------------------------- /.vscode/cpx.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "python.linting.pylintArgs": [ 6 | "--init-hook", 7 | "import sys; sys.path.extend([\"\"c:\\\\Users\\\\lili3\\\\.vscode\\\\extensions\\\\ms-python.devicesimulatorexpress-2020.0.36321\\\\out\"\",\"\"c:\\\\Users\\\\lili3\\\\.vscode\\\\extensions\\\\ms-python.devicesimulatorexpress-2020.0.36321\\\\out\\\\micropython\"\",\"\"c:\\\\Users\\\\lili3\\\\.vscode\\\\extensions\\\\ms-python.devicesimulatorexpress-2020.0.36321\\\\out\\\\clue\"\",\"\"c:\\\\Users\\\\lili3\\\\.vscode\\\\extensions\\\\ms-python.devicesimulatorexpress-2020.0.36321\\\\out\\\\base_circuitpython\"\"])" 8 | ], 9 | } -------------------------------------------------------------------------------- /CRA.md: -------------------------------------------------------------------------------- 1 | 2 | # Create React App 3 | 4 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 5 | 6 | ## Available Scripts 7 | 8 | In the project directory, you can run: 9 | 10 | ### `npm start` 11 | 12 | Runs the app in the development mode.
13 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 14 | 15 | The page will reload if you make edits.
16 | You will also see any lint errors in the console. 17 | 18 | ### `npm test` 19 | 20 | Launches the test runner in the interactive watch mode.
21 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 22 | 23 | ### `npm run build` 24 | 25 | Builds the app for production to the `build` folder.
26 | It correctly bundles React in production mode and optimizes the build for the best performance. 27 | 28 | The build is minified and the filenames include the hashes.
29 | Your app is ready to be deployed! 30 | 31 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 32 | 33 | ### `npm run eject` 34 | 35 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 36 | 37 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 38 | 39 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 40 | 41 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 42 | 43 | ## Learn More 44 | 45 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 46 | 47 | To learn React, check out the [React documentation](https://reactjs.org/). 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael DeMarco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

This repository is now deprecated, and development of groUber, now called rooter, is being continued here.

2 | 3 |

4 | 5 |

6 | 7 | # groUber: schedule carpools, without the headache 8 | 9 | Hello, world! We’re groUber, an app for organizing events in the 21st century. 10 | 11 | groups + Uber = [groUber](https://grouber.online) 12 | 13 | ## About 14 | 15 | groUber is aimed to help event planners create carpools, and is being built for To the Moon and Hack. If you're going to use this project to plan your event, remember to stay 6ft apart! 16 | 17 | ### Motivation 18 | 19 | This project was built by a group of 5 students from UBC in Vancouver, BC who love automating things. And one horrific task all of us have run into when planning our events is creating a workable carpool schedule. 20 | 21 | It’s a great option for getting your group together: whether it be parents figuring out how best to get their kids to soccer practice, or friends accommodating those without access to a car, carpooling is common, but creating a plan can be painful, to say the least. 22 | 23 | You finally come up with a workable schedule: everyone can make it to the event, no driver has to go in annoying, wasteful loops, and everything can start on time. 24 | 25 | Then a driver with 4 seats drops out. And you have to do it all over again. No, thanks. 26 | 27 | ### Introducing: groUber 28 | 29 | With groUber, never go through that headache again. As an event organizer, create your event, send an invite link to your friends, and create a carpool schedule with one click. As a participant, simply receive a link, RSVP, and inform the organizer of how many seats you have available. You’ll receive a schedule on the day-of. 30 | 31 | Using the Radar API and the Google Maps API, along with a bit of algo-magic, our app will create the most optimal carpool schedule for everyone involved. We were hesitant to do this project at first; the idea of designing an algorithm to find the “best” carpool strategy was intimidating to say the least. After some research, it turns out this is actually classified an NP-hard problem. We didn't need to solve the problem generally though, and were able to design a heuristic algorithm to be able to compute this with fairly good results. Here are a [few](https://www.sciencedirect.com/science/article/pii/S1877050914006334) [examples](https://arxiv.org/pdf/1604.05609.pdf) of scholarly work in this area. It took great teamwork, persistence, and a decent amount of caffeine to get this working. 32 | 33 | Now, drivers won’t have to waste gas, and everyone will get there on time. Someone drops out? No problem, our app will allow you to adjust your schedule, painlessly. 34 | 35 | groUber is ride-sharing for your group of friends, without all the expenses and overhead. Do a favor for the environment, and for your stress-levels, and start using groUber today. 36 | 37 | Here's our [whitepaper](https://docs.google.com/document/d/e/2PACX-1vSLo8DhZ7p-VgPnmieasD01zZ2_76uMxPxSwVErq3_gQPvpokrFI4G5SHA1Pxrc9b9ouhK5yvL-4X3t/pub) where you can find a more in-depth justification for this hack! We also have a [slide deck](https://docs.google.com/presentation/d/e/2PACX-1vRY4A2dxP3vd1L7tY9gNC1Y0oegweKaiE9ZRHzulQArcLDld2zxErJQPk2TWvtwONXqdFWSECFlWEhh/pub?start=false&loop=false&delayms=15000) for the visual learners out there. 38 | 39 | ## Installation 40 | 41 | ### Stack 42 | 43 | - TypeScript, React 44 | - Node.js, npm 45 | - Firebase, including authentication, hosting, and Firestore 46 | 47 | ### Get it running 48 | 49 | `npm install` 50 | Install dependencies. 51 | 52 | `npm start` 53 | Run for development. 54 | 55 | `npm lint` 56 | Run linting over the project repository. 57 | 58 | Deployment happens entirely via GitHub Actions; on any pushes to master, the app will be re-deployed to Firebase hosting. 59 | 60 | ## Usage 61 | 62 | Head to [grouber.online](https://grouber.online). Sign-up using your Google account. 63 | 64 | Create your event with all key details, and send an invite to your to-be attendees. Once they RSVP, you'll see their details on your event dashboard. Then, generate your event's carpool schedule in one click! 65 | 66 | Participants must submit their address, if they're driving, and if so, how many seats they have available in their vehicle. 67 | 68 | ### Examples 69 | 70 | 71 | 72 |

73 | 74 |

75 | 76 |

77 | 78 |

79 | 80 |

81 | 82 |

83 | 84 |

85 | 86 |

87 | 88 | ## Contributing 89 | 90 | This will be updated after the hackathon! Stay posted for more. 91 | -------------------------------------------------------------------------------- /branding/ads/poster-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/ads/poster-01.png -------------------------------------------------------------------------------- /branding/ads/poster.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/ads/poster.ai -------------------------------------------------------------------------------- /branding/fonts/PT Serif/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, ParaType Ltd. (http://www.paratype.com/public), 2 | with Reserved Font Names "PT Sans", "PT Serif" and "ParaType". 3 | 4 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 5 | This license is copied below, and is also available with a FAQ at: 6 | http://scripts.sil.org/OFL 7 | 8 | 9 | ----------------------------------------------------------- 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 | ----------------------------------------------------------- 12 | 13 | PREAMBLE 14 | The goals of the Open Font License (OFL) are to stimulate worldwide 15 | development of collaborative font projects, to support the font creation 16 | efforts of academic and linguistic communities, and to provide a free and 17 | open framework in which fonts may be shared and improved in partnership 18 | with others. 19 | 20 | The OFL allows the licensed fonts to be used, studied, modified and 21 | redistributed freely as long as they are not sold by themselves. The 22 | fonts, including any derivative works, can be bundled, embedded, 23 | redistributed and/or sold with any software provided that any reserved 24 | names are not used by derivative works. The fonts and derivatives, 25 | however, cannot be released under any other type of license. The 26 | requirement for fonts to remain under this license does not apply 27 | to any document created using the fonts or their derivatives. 28 | 29 | DEFINITIONS 30 | "Font Software" refers to the set of files released by the Copyright 31 | Holder(s) under this license and clearly marked as such. This may 32 | include source files, build scripts and documentation. 33 | 34 | "Reserved Font Name" refers to any names specified as such after the 35 | copyright statement(s). 36 | 37 | "Original Version" refers to the collection of Font Software components as 38 | distributed by the Copyright Holder(s). 39 | 40 | "Modified Version" refers to any derivative made by adding to, deleting, 41 | or substituting -- in part or in whole -- any of the components of the 42 | Original Version, by changing formats or by porting the Font Software to a 43 | new environment. 44 | 45 | "Author" refers to any designer, engineer, programmer, technical 46 | writer or other person who contributed to the Font Software. 47 | 48 | PERMISSION & CONDITIONS 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 51 | redistribute, and sell modified and unmodified copies of the Font 52 | Software, subject to the following conditions: 53 | 54 | 1) Neither the Font Software nor any of its individual components, 55 | in Original or Modified Versions, may be sold by itself. 56 | 57 | 2) Original or Modified Versions of the Font Software may be bundled, 58 | redistributed and/or sold with any software, provided that each copy 59 | contains the above copyright notice and this license. These can be 60 | included either as stand-alone text files, human-readable headers or 61 | in the appropriate machine-readable metadata fields within text or 62 | binary files as long as those fields can be easily viewed by the user. 63 | 64 | 3) No Modified Version of the Font Software may use the Reserved Font 65 | Name(s) unless explicit written permission is granted by the corresponding 66 | Copyright Holder. This restriction only applies to the primary font name as 67 | presented to the users. 68 | 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 70 | Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except to acknowledge the contribution(s) of the 72 | Copyright Holder(s) and the Author(s) or with their explicit written 73 | permission. 74 | 75 | 5) The Font Software, modified or unmodified, in part or in whole, 76 | must be distributed entirely under this license, and must not be 77 | distributed under any other license. The requirement for fonts to 78 | remain under this license does not apply to any document created 79 | using the Font Software. 80 | 81 | TERMINATION 82 | This license becomes null and void if any of the above conditions are 83 | not met. 84 | 85 | DISCLAIMER 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 94 | OTHER DEALINGS IN THE FONT SOFTWARE. 95 | -------------------------------------------------------------------------------- /branding/fonts/PT Serif/PTSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/PT Serif/PTSerif-Bold.ttf -------------------------------------------------------------------------------- /branding/fonts/PT Serif/PTSerif-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/PT Serif/PTSerif-BoldItalic.ttf -------------------------------------------------------------------------------- /branding/fonts/PT Serif/PTSerif-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/PT Serif/PTSerif-Italic.ttf -------------------------------------------------------------------------------- /branding/fonts/PT Serif/PTSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/PT Serif/PTSerif-Regular.ttf -------------------------------------------------------------------------------- /branding/fonts/PT Serif/PT_Serif.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/PT Serif/PT_Serif.zip -------------------------------------------------------------------------------- /branding/fonts/Poppins/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-Black.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-BlackItalic.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-Bold.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-BoldItalic.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-ExtraBold.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-ExtraLight.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-Italic.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-Light.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-LightItalic.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-Medium.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-MediumItalic.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-Regular.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-SemiBold.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-Thin.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins-ThinItalic.ttf -------------------------------------------------------------------------------- /branding/fonts/Poppins/Poppins.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/fonts/Poppins/Poppins.zip -------------------------------------------------------------------------------- /branding/logo/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/android-chrome-192x192.png -------------------------------------------------------------------------------- /branding/logo/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/android-chrome-512x512.png -------------------------------------------------------------------------------- /branding/logo/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/apple-touch-icon.png -------------------------------------------------------------------------------- /branding/logo/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/favicon-16x16.png -------------------------------------------------------------------------------- /branding/logo/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/favicon-32x32.png -------------------------------------------------------------------------------- /branding/logo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/favicon.ico -------------------------------------------------------------------------------- /branding/logo/g-logo-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/g-logo-01.png -------------------------------------------------------------------------------- /branding/logo/g-logo-inverse-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/g-logo-inverse-01.png -------------------------------------------------------------------------------- /branding/logo/grouber-192.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/grouber-192.ai -------------------------------------------------------------------------------- /branding/logo/grouber-512.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/grouber-512.ai -------------------------------------------------------------------------------- /branding/logo/grouber-logo-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/grouber-logo-01.png -------------------------------------------------------------------------------- /branding/logo/grouber-logo-01.svg: -------------------------------------------------------------------------------- 1 | groUber -------------------------------------------------------------------------------- /branding/logo/grouber-logo-inverse-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/grouber-logo-inverse-01.png -------------------------------------------------------------------------------- /branding/logo/grouber-logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/grouber-logo.ai -------------------------------------------------------------------------------- /branding/logo/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/logo192.png -------------------------------------------------------------------------------- /branding/logo/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/branding/logo/logo512.png -------------------------------------------------------------------------------- /branding/logo/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ 3 | "rules": { 4 | ".read": false, 5 | ".write": false 6 | } 7 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "functions": { 6 | "predeploy": [ 7 | "npm --prefix \"$RESOURCE_DIR\" run lint", 8 | "npm --prefix \"$RESOURCE_DIR\" run build" 9 | ] 10 | }, 11 | "hosting": { 12 | "public": "build", 13 | "ignore": [ 14 | "firebase.json", 15 | "**/.*", 16 | "**/node_modules/**" 17 | ], 18 | "rewrites": [ 19 | { 20 | "source": "**", 21 | "destination": "/index.html" 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js.map 3 | 4 | # Typescript v1 declaration files 5 | typings/ 6 | 7 | node_modules/ -------------------------------------------------------------------------------- /functions/lib/DistanceMatrix.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.CustomMap = exports.DistanceMatrix = void 0; 7 | const axios_1 = __importDefault(require("axios")); 8 | class DistanceMatrix { 9 | /** 10 | * @param {People[]} people 11 | * @example distanceMatrix = new DistanceMatrix(people) 12 | */ 13 | constructor(people) { 14 | this.data = new CustomMap(); // number is the distance 15 | this.people = people; 16 | } 17 | /** generates data which is our map given a list of people. 18 | * 19 | * @example await distanceMatrix.init(); 20 | */ 21 | async init() { 22 | const apiData = await this.grabMatrixDataFromRadar(this.people); 23 | const origins = apiData.origins; 24 | const destinations = apiData.destinations; 25 | const matrix = apiData.matrix; 26 | for (let i = 0; i < origins.length; i++) { 27 | const distances = new CustomMap(); 28 | const startLoc = { 29 | latitude: this.people[i].location.latlng.lat, 30 | longitude: this.people[i].location.latlng.lng, 31 | }; 32 | for (let j = 0; j < destinations.length; j++) { 33 | const endLoc = { 34 | latitude: this.people[j].location.latlng.lat, 35 | longitude: this.people[j].location.latlng.lng, 36 | }; 37 | distances.set(endLoc, matrix[i][j].distance.value); 38 | } 39 | this.data.set(startLoc, distances); 40 | } 41 | } 42 | async grabMatrixDataFromRadar(people) { 43 | const locations = people.map((person) => { 44 | return `${person.location.latlng.lat},${person.location.latlng.lng}`; 45 | }); 46 | const result = await axios_1.default.get('https://api.radar.io/v1/route/matrix', { 47 | params: { 48 | origins: locations.join('|'), 49 | destinations: locations.join('|'), 50 | mode: 'car', 51 | units: 'metric', 52 | }, 53 | headers: { 54 | Authorization: `prj_live_pk_7a9bbe078da0cfa051f77e2c9d9d0f929b9e5955`, 55 | }, 56 | }); 57 | return result.data; 58 | } 59 | } 60 | exports.DistanceMatrix = DistanceMatrix; 61 | class CustomMap { 62 | constructor() { 63 | // fields go here 64 | // id: string 65 | this.keys = []; 66 | this.values = []; 67 | } 68 | set(key, value) { 69 | const index = this.findIndex(key); 70 | if (index == -1) { 71 | this.keys.push(key); 72 | this.values.push(value); 73 | } 74 | else { 75 | this.values[index] = value; 76 | } 77 | } 78 | get(key) { 79 | return this.values[this.findIndex(key)]; 80 | } 81 | findIndex(key) { 82 | for (let i = 0; i < this.keys.length; i++) { 83 | const item = this.keys[i]; 84 | if (item.latitude === key.latitude && item.longitude === key.longitude) { 85 | return i; 86 | } 87 | } 88 | return -1; 89 | } 90 | } 91 | exports.CustomMap = CustomMap; 92 | //# sourceMappingURL=DistanceMatrix.js.map -------------------------------------------------------------------------------- /functions/lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 10 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 11 | }) : function(o, v) { 12 | o["default"] = v; 13 | }); 14 | var __importStar = (this && this.__importStar) || function (mod) { 15 | if (mod && mod.__esModule) return mod; 16 | var result = {}; 17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 18 | __setModuleDefault(result, mod); 19 | return result; 20 | }; 21 | var __importDefault = (this && this.__importDefault) || function (mod) { 22 | return (mod && mod.__esModule) ? mod : { "default": mod }; 23 | }; 24 | Object.defineProperty(exports, "__esModule", { value: true }); 25 | exports.solve = exports.directions = void 0; 26 | const functions = __importStar(require("firebase-functions")); 27 | const DistanceMatrix_1 = require("./DistanceMatrix"); 28 | const firebase = __importStar(require("firebase")); 29 | const admin = __importStar(require("firebase-admin")); 30 | const priorityqueue_1 = __importDefault(require("priorityqueue")); 31 | require("firebase/firestore"); 32 | const axios_1 = __importDefault(require("axios")); 33 | // eslint-disable-next-line @typescript-eslint/no-var-requires 34 | const cors = require('cors')(); 35 | const firebaseConfig = { 36 | apiKey: 'AIzaSyDvCT-243TWt9Dwb9ChTOgfkFMUhIjTlRc', 37 | authDomain: 'find-my-carpool.firebaseapp.com', 38 | databaseURL: 'https://find-my-carpool.firebaseio.com', 39 | projectId: 'find-my-carpool', 40 | storageBucket: 'find-my-carpool.appspot.com', 41 | messagingSenderId: '470237283855', 42 | appId: '1:470237283855:web:d3aa289ca316787e4a457a', 43 | measurementId: 'G-YSVSBWXT23', 44 | }; 45 | firebase.initializeApp(firebaseConfig); 46 | admin.initializeApp(firebaseConfig); 47 | exports.directions = functions.https.onRequest(async (request, response) => { 48 | cors(request, response, async () => { 49 | response.set('Access-Control-Allow-Origin', '*'); 50 | const key = functions.config().directions.key; 51 | if (request.method !== 'POST') { 52 | response.status(405).json({ message: 'Method Not Allowed.' }); 53 | return; 54 | } 55 | else if (!request.get('Access-Token')) { 56 | response.status(401).json({ message: 'Unauthorized.' }); 57 | return; 58 | } 59 | else if (!key) { 60 | response.status(403).json({ message: 'Service Unavailable' }); 61 | return; 62 | } 63 | else { 64 | try { 65 | const verify = await admin.auth().verifyIdToken(request.get('Access-Token')); 66 | if (!verify.uid) { 67 | response.status(403).json({ message: 'Service Unavailable' }); 68 | return; 69 | } 70 | else { 71 | console.log(request.body.destination); 72 | const axiosResult = await axios_1.default({ 73 | method: 'GET', 74 | url: `https://maps.googleapis.com/maps/api/directions/json?destination=${request.body.destination}&key=${key}&origin=${request.body.origin}&waypoints=${request.body.waypoints}`, 75 | }); 76 | response.json(axiosResult.data); 77 | return; 78 | } 79 | } 80 | catch (err) { 81 | response.status(401).json({ message: err.message }); 82 | return; 83 | } 84 | } 85 | }); 86 | }); 87 | exports.solve = functions.https.onRequest(async (request, response) => { 88 | response.set('Access-Control-Allow-Origin', '*'); 89 | functions.logger.info('Hello logs!', { structuredData: true }); 90 | // grab event first 91 | const result = await firebase.firestore().collection('events').doc(request.query.eventId).get(); 92 | const event = result.data(); 93 | if (!event) { 94 | response.json('error couldnt find event'); 95 | return; 96 | } 97 | const people = await Promise.all(event.people.map(async (person) => { 98 | const personRef = await person.get(); 99 | return Object.assign({ id: person.id }, personRef.data()); 100 | })); 101 | event.people = people; 102 | response.json(await solveCarpoolProblem(event)); 103 | }); 104 | /** 105 | * Solves the carpool problem. Given an event (containing a list of people) 106 | * return a hashmap where the key is a driver id, and the value is a list of 107 | * passsenger ids that driver can drive 108 | * 109 | * @param {Event} event 110 | * @returns Map> 111 | */ 112 | async function solveCarpoolProblem(event) { 113 | const people = event.people; 114 | let remainingDrivers = getDrivers(people); 115 | let remainingPassengers = getPassengers(people); 116 | const distanceMatrix = new DistanceMatrix_1.DistanceMatrix(people); 117 | await distanceMatrix.init(); 118 | const solution = new Map(); 119 | while (!isDone(remainingDrivers, remainingPassengers)) { 120 | if (remainingPassengers.length > 0 && remainingDrivers.length === 0) 121 | break; 122 | // const distances = calculatePassengerDistances(remainingDrivers, remainingPassengers); 123 | if (remainingPassengers.length > 0) { 124 | const closestDriverToPassengers = new Map(); 125 | for (const passenger of remainingPassengers) { 126 | let minDriverDistance = Infinity; 127 | let minDriverId = ''; 128 | for (const driver of remainingDrivers) { 129 | // // calc distance between driver and passenger 130 | // console.log("this shit ran") 131 | // console.log(distanceMatrix.data.keys, distanceMatrix.data.values) 132 | // console.log("this is the key were trying to get", { 133 | // latitude: driver.location.latlng.lat, 134 | // longitude: driver.location.latlng.lng 135 | // }) 136 | const distanceMap = distanceMatrix.data.get({ 137 | latitude: driver.location.latlng.lat, 138 | longitude: driver.location.latlng.lng, 139 | }); 140 | const distance = distanceMap.get({ 141 | latitude: passenger.location.latlng.lat, 142 | longitude: passenger.location.latlng.lng, 143 | }); 144 | console.log(distance, ' from ', passenger.name, driver.name); 145 | // const distance = haversine({ 146 | // latitude: driver.location.latlng.lat, 147 | // longitude: driver.location.latlng.lng, 148 | // }, { 149 | // latitude: passenger.location.latlng.lat, 150 | // longitude: passenger.location.latlng.lng, 151 | // }, {unit: 'meter'}) 152 | if (distance < minDriverDistance) { 153 | minDriverDistance = distance; 154 | minDriverId = driver.id; 155 | } 156 | } 157 | closestDriverToPassengers.set(passenger.id, [minDriverId, minDriverDistance]); 158 | } 159 | const numericCompare = (a, b) => (a < b ? 1 : a > b ? -1 : 0); 160 | const comparator = (a, b) => { 161 | return numericCompare(a.minDriverDistance, b.minDriverDistance); 162 | }; 163 | const pq = new priorityqueue_1.default({ comparator }); 164 | for (const passenger of remainingPassengers) { 165 | const array = closestDriverToPassengers.get(passenger.id); 166 | if (!array) 167 | continue; 168 | const [minDriverId, minDriverDistance] = array; 169 | const node = { 170 | passengerId: passenger.id, 171 | minDriverId, 172 | minDriverDistance, 173 | }; 174 | pq.enqueue(node); 175 | } 176 | const nextPassengerNode = pq.dequeue(); 177 | if (!nextPassengerNode) 178 | continue; 179 | const nextPassenger = getPersonById(people, nextPassengerNode.passengerId); 180 | const minDriver = getPersonById(people, nextPassengerNode.minDriverId); 181 | if (!nextPassenger || !minDriver) 182 | continue; 183 | minDriver.location.latlng = { 184 | lat: nextPassenger === null || nextPassenger === void 0 ? void 0 : nextPassenger.location.latlng.lat, 185 | lng: nextPassenger === null || nextPassenger === void 0 ? void 0 : nextPassenger.location.latlng.lng, 186 | }; 187 | const passengerIds = solution.get(minDriver.id) || []; 188 | passengerIds.push(nextPassenger.id); 189 | solution.set(minDriver.id, passengerIds); 190 | minDriver.seats--; 191 | if (minDriver.seats === 0) { 192 | remainingDrivers = removePerson(remainingDrivers, minDriver.id); 193 | } 194 | remainingPassengers = removePerson(remainingPassengers, nextPassenger.id); 195 | // TODO add to database for visualization of this algorithm 196 | } 197 | // no more remaining passengers but still have drivers 198 | if (remainingDrivers.length === 1 && remainingPassengers.length === 0) { 199 | if (!solution.has(remainingDrivers[0].id)) 200 | solution.set(remainingDrivers[0].id, []); 201 | remainingDrivers = removePerson(remainingDrivers, remainingDrivers[0].id); 202 | } 203 | if (remainingPassengers.length === 0) { 204 | const driverToBeConverted = getSomeoneDrivingNoOne(remainingDrivers, solution); 205 | if (driverToBeConverted) { 206 | // exists 207 | remainingPassengers.push(driverToBeConverted); 208 | removePerson(remainingDrivers, driverToBeConverted.id); 209 | } 210 | else { 211 | break; 212 | } 213 | } 214 | } 215 | return strMapToObj(solution); 216 | } 217 | function strMapToObj(strMap) { 218 | const obj = Object.create(null); 219 | for (const [k, v] of strMap) { 220 | // We don’t escape the key '__proto__' 221 | // which can cause problems on older engines 222 | obj[k] = v; 223 | } 224 | return obj; 225 | } 226 | function getSomeoneDrivingNoOne(drivers, solution) { 227 | for (const driver of drivers) { 228 | if (!solution.has(driver.id)) { 229 | return driver; 230 | } 231 | } 232 | return undefined; 233 | } 234 | function removePerson(people, id) { 235 | for (let i = 0; i < people.length; i++) { 236 | const driver = people[i]; 237 | if (driver.id === id) { 238 | people.splice(i, 1); 239 | } 240 | } 241 | return people; 242 | } 243 | function getPersonById(people, id) { 244 | for (const person of people) { 245 | if (person.id === id) { 246 | return person; 247 | } 248 | } 249 | return undefined; 250 | } 251 | /** 252 | * returns whether the solution contains all the people in an event 253 | * 254 | * @param {People} people 255 | * @param { Map>} solution 256 | * @returns boolean 257 | */ 258 | function isDone(drivers, passengers) { 259 | return drivers.length === 0 && passengers.length === 0; 260 | } 261 | /** 262 | * Return all drivers from a list of people 263 | * 264 | * @param {People[]} people 265 | * @returns People[] 266 | */ 267 | function getDrivers(people) { 268 | return people.filter((person) => { 269 | return person.canDrive; 270 | }); 271 | } 272 | /** 273 | * Return all passengers from a list of people 274 | * 275 | * @param {People[]} people 276 | * @returns People[] 277 | */ 278 | function getPassengers(people) { 279 | return people.filter((person) => { 280 | return !person.canDrive; 281 | }); 282 | } 283 | // export const getEncoding = functions.https.onRequest(async (request: any, response: any) => { 284 | // const { route } = request.body 285 | // await Axios.get( 286 | // `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.lat},${origin.lng}&destination=${destination.lat},${destination.lng}&${waypoint}&key=${key}`, 287 | // ); 288 | // const origin = route[0]; 289 | // const destination = route[route.length - 1]; 290 | // if (route.length > 3) { 291 | // let waypoint = 'waypoints='; 292 | // // , === %2C 293 | // // | seperate locations 294 | // for (let i = 1; i < route.length - 1; i++) { 295 | // if (i == route.length - 2) { 296 | // waypoint = waypoint + route[i].lat + '%2C' + route[i].lat; 297 | // } else { 298 | // waypoint = waypoint + route[i].lat + '%2C' + route[i].lat + '|'; 299 | // } 300 | // } 301 | // const response = await axios.get( 302 | // `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.lat},${origin.lng}&destination=${destination.lat},${destination.lng}&${waypoint}&key=${key}`, 303 | // ); 304 | // // console.log(response); 305 | // } else { 306 | // // const response = await axios.get( 307 | // // `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.lat},${origin.lng}&destination=${destination.lat},${destination.lng}&key=${key}`, 308 | // // ); 309 | // // console.log(response); 310 | // } 311 | // } 312 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /functions/lib/priorityQueue.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // export class Node{ 3 | // value: any; 4 | // priority: number 5 | // constructor(value: any, priority: number){ 6 | // this.value = value 7 | // this.priority = priority 8 | // } 9 | // } 10 | // export class PriorityQueue{ 11 | // values: Node[] 12 | // constructor(){ 13 | // this.values = [] 14 | // } 15 | // //helper method that swaps the values and two indexes of an array 16 | // swap(index1: number, index2: number){ 17 | // let temp = this.values[index1]; 18 | // this.values[index1] = this.values[index2]; 19 | // this.values[index2] = temp; 20 | // return this.values; 21 | // } 22 | // //helper methods that bubbles up values from end 23 | // bubbleUp(){ 24 | // //get index of inserted element 25 | // let index = this.values.length - 1 26 | // //loop while index is not 0 or element no loger needs to bubble 27 | // while(index > 0){ 28 | // //get parent index via formula 29 | // let parentIndex = Math.floor((index - 1)/2); 30 | // //if values is greater than parent, swap the two 31 | // if(this.values[parentIndex].priority > this.values[index].priority){ 32 | // //swap with helper method 33 | // this.swap(index, parentIndex); 34 | // //change current index to parent index 35 | // index = parentIndex; 36 | // } else{ 37 | // break; 38 | // } 39 | // } 40 | // return 0; 41 | // } 42 | // // method that pushes new value onto the end and calls the bubble helper 43 | // enqueue(value: Node){ 44 | // this.values.push(value) 45 | // //calculate parent, if parent is greater swap 46 | // //while loop or recurse 47 | // this.bubbleUp(); 48 | // return this.values 49 | // } 50 | // bubbleDown(){ 51 | // let parentIndex = 0; 52 | // const length = this.values.length; 53 | // const elementPriority = this.values[0].priority; 54 | // //loop breaks if no swaps are needed 55 | // while (true){ 56 | // //get indexes of child elements by following formula 57 | // let leftChildIndex = (2 * parentIndex) + 1; 58 | // let rightChildIndex = (2 * parentIndex) + 2; 59 | // let leftChildPriority, rightChildPriority; 60 | // let indexToSwap = null; 61 | // // if left child exists, and is greater than the element, plan to swap with the left child index 62 | // if(leftChildIndex < length){ 63 | // leftChildPriority = this.values[leftChildIndex].priority 64 | // if(leftChildPriority < elementPriority){ 65 | // indexToSwap = leftChildIndex; 66 | // } 67 | // } 68 | // //if right child exists 69 | // if(rightChildIndex < length){ 70 | // rightChildPriority = this.values[rightChildIndex].priority 71 | // if( 72 | // //if right child is greater than element and there are no plans to swap 73 | // (rightChildPriority < elementPriority && indexToSwap === null) || 74 | // //OR if right child is greater than left child and there ARE plans to swap 75 | // (rightChildPriority < leftChildPriority && indexToSwap !== null)) 76 | // { 77 | // //plan to swap with the right child 78 | // indexToSwap = rightChildIndex 79 | // } 80 | // } 81 | // //if there are no plans to swap, break out of the loop 82 | // if(indexToSwap === null){ 83 | // break; 84 | // } 85 | // //swap with planned element 86 | // this.swap(parentIndex, indexToSwap); 87 | // //starting index is now index that we swapped with 88 | // parentIndex = indexToSwap; 89 | // } 90 | // } 91 | // dequeue(){ 92 | // //swap first and last element 93 | // this.swap(0, this.values.length - 1); 94 | // //pop max value off of values 95 | // let poppedNode = this.values.pop(); 96 | // //re-adjust heap if length is greater than 1 97 | // if(this.values.length > 1){ 98 | // this.bubbleDown(); 99 | // } 100 | // return poppedNode; 101 | // } 102 | // } 103 | //# sourceMappingURL=priorityQueue.js.map -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase emulators:start --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "10" 14 | }, 15 | "main": "lib/index.js", 16 | "dependencies": { 17 | "@types/cors": "^2.8.7", 18 | "@types/node": "^14.0.27", 19 | "axios": "^1.6.0", 20 | "cors": "^2.8.5", 21 | "firebase": "^7.24.0", 22 | "firebase-admin": "^11.3.0", 23 | "firebase-functions": "^3.6.1", 24 | "haversine": "^1.1.1", 25 | "priorityqueue": "^1.0.0", 26 | "radar-sdk-js": "^3.0.1", 27 | "tslint": "^5.20.1", 28 | "typescript": "^3.8.0" 29 | }, 30 | "devDependencies": { 31 | "firebase-functions-test": "^0.2.0" 32 | }, 33 | "private": true 34 | } 35 | -------------------------------------------------------------------------------- /functions/src/DistanceMatrix.ts: -------------------------------------------------------------------------------- 1 | import { People } from '../../src/_types/people'; 2 | import axios from 'axios'; 3 | 4 | export interface Location { 5 | latitude: number; 6 | longitude: number; 7 | } 8 | 9 | export class DistanceMatrix { 10 | people: People[]; 11 | data: CustomMap> = new CustomMap(); // number is the distance 12 | /** 13 | * @param {People[]} people 14 | * @example distanceMatrix = new DistanceMatrix(people) 15 | */ 16 | constructor(people: People[]) { 17 | this.people = people; 18 | } 19 | /** generates data which is our map given a list of people. 20 | * 21 | * @example await distanceMatrix.init(); 22 | */ 23 | async init() { 24 | const apiData = await this.grabMatrixDataFromRadar(this.people); 25 | const origins = apiData.origins; 26 | const destinations = apiData.destinations; 27 | const matrix = apiData.matrix; 28 | 29 | for (let i = 0; i < origins.length; i++) { 30 | const distances: CustomMap = new CustomMap(); 31 | 32 | const startLoc: Location = { 33 | latitude: this.people[i].location.latlng.lat, 34 | longitude: this.people[i].location.latlng.lng, 35 | }; 36 | for (let j = 0; j < destinations.length; j++) { 37 | const endLoc: Location = { 38 | latitude: this.people[j].location.latlng.lat, 39 | longitude: this.people[j].location.latlng.lng, 40 | }; 41 | distances.set(endLoc, matrix[i][j].distance.value); 42 | } 43 | this.data.set(startLoc, distances); 44 | } 45 | } 46 | 47 | async grabMatrixDataFromRadar(people: People[]): Promise { 48 | const locations: string[] = people.map((person) => { 49 | return `${person.location.latlng.lat},${person.location.latlng.lng}`; 50 | }); 51 | 52 | const result = await axios.get('https://api.radar.io/v1/route/matrix', { 53 | params: { 54 | origins: locations.join('|'), 55 | destinations: locations.join('|'), 56 | mode: 'car', 57 | units: 'metric', 58 | }, 59 | headers: { 60 | Authorization: `prj_live_pk_7a9bbe078da0cfa051f77e2c9d9d0f929b9e5955`, 61 | }, 62 | }); 63 | 64 | return result.data; 65 | } 66 | } 67 | 68 | export class CustomMap { 69 | // fields go here 70 | // id: string 71 | 72 | keys: Array = []; 73 | values: Array = []; 74 | 75 | set(key: Location, value: B) { 76 | const index = this.findIndex(key); 77 | if (index == -1) { 78 | this.keys.push(key); 79 | this.values.push(value); 80 | } else { 81 | this.values[index] = value; 82 | } 83 | } 84 | 85 | get(key: Location) { 86 | return this.values[this.findIndex(key)]; 87 | } 88 | 89 | findIndex(key: Location): number { 90 | for (let i = 0; i < this.keys.length; i++) { 91 | const item = this.keys[i]; 92 | if (item.latitude === key.latitude && item.longitude === key.longitude) { 93 | return i; 94 | } 95 | } 96 | return -1; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import { Event } from '../../src/_types/event'; 3 | import { People } from '../../src/_types/people'; 4 | 5 | import { DistanceMatrix } from './DistanceMatrix'; 6 | import * as firebase from 'firebase'; 7 | import * as admin from 'firebase-admin'; 8 | import PriorityQueue from 'priorityqueue'; 9 | import 'firebase/firestore'; 10 | import axios from 'axios'; 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-var-requires 13 | const cors = require('cors')(); 14 | 15 | const firebaseConfig = { 16 | apiKey: 'AIzaSyDvCT-243TWt9Dwb9ChTOgfkFMUhIjTlRc', 17 | authDomain: 'find-my-carpool.firebaseapp.com', 18 | databaseURL: 'https://find-my-carpool.firebaseio.com', 19 | projectId: 'find-my-carpool', 20 | storageBucket: 'find-my-carpool.appspot.com', 21 | messagingSenderId: '470237283855', 22 | appId: '1:470237283855:web:d3aa289ca316787e4a457a', 23 | measurementId: 'G-YSVSBWXT23', 24 | }; 25 | firebase.initializeApp(firebaseConfig); 26 | 27 | admin.initializeApp(firebaseConfig); 28 | 29 | export const directions = functions.https.onRequest(async (request: any, response: any) => { 30 | cors(request, response, async () => { 31 | response.set('Access-Control-Allow-Origin', '*'); 32 | const key = functions.config().directions.key; 33 | if (request.method !== 'POST') { 34 | response.status(405).json({ message: 'Method Not Allowed.' }); 35 | return; 36 | } else if (!request.get('Access-Token')) { 37 | response.status(401).json({ message: 'Unauthorized.' }); 38 | return; 39 | } else if (!key) { 40 | response.status(403).json({ message: 'Service Unavailable' }); 41 | return; 42 | } else { 43 | try { 44 | const verify = await admin.auth().verifyIdToken(request.get('Access-Token')); 45 | if (!verify.uid) { 46 | response.status(403).json({ message: 'Service Unavailable' }); 47 | return; 48 | } else { 49 | console.log(request.body); 50 | const axiosResult = await axios({ 51 | method: 'GET', 52 | url: `https://maps.googleapis.com/maps/api/directions/json?destination=${ 53 | request.body.destination 54 | }&key=${key}&origin=${request.body.origin}${ 55 | request.body.waypoints && `&waypoints=${request.body.waypoints}` 56 | }`, 57 | }); 58 | response.json(axiosResult.data); 59 | return; 60 | } 61 | } catch (err) { 62 | response.status(401).json({ message: err.message }); 63 | return; 64 | } 65 | } 66 | }); 67 | }); 68 | 69 | export const solve = functions.https.onRequest(async (request: any, response: any) => { 70 | response.set('Access-Control-Allow-Origin', '*'); 71 | functions.logger.info('Hello logs!', { structuredData: true }); 72 | 73 | // grab event first 74 | const result = await firebase.firestore().collection('events').doc(request.query.eventId).get(); 75 | 76 | const event = result.data(); 77 | 78 | if (!event) { 79 | response.json('error couldnt find event'); 80 | return; 81 | } 82 | 83 | const people = await Promise.all( 84 | event.people.map(async (person: firebase.firestore.DocumentReference) => { 85 | const personRef = await person.get(); 86 | return { 87 | id: person.id, 88 | ...personRef.data(), 89 | }; 90 | }), 91 | ); 92 | 93 | event.people = people; 94 | 95 | response.json(await solveCarpoolProblem(event as Event)); 96 | }); 97 | 98 | /** 99 | * Solves the carpool problem. Given an event (containing a list of people) 100 | * return a hashmap where the key is a driver id, and the value is a list of 101 | * passsenger ids that driver can drive 102 | * 103 | * @param {Event} event 104 | * @returns Map> 105 | */ 106 | async function solveCarpoolProblem(event: Event): Promise>> { 107 | const people = event.people; 108 | let remainingDrivers = getDrivers(people as People[]); 109 | let remainingPassengers = getPassengers(people as People[]); 110 | const distanceMatrix: DistanceMatrix = new DistanceMatrix(people as People[]); 111 | await distanceMatrix.init(); 112 | 113 | const solution = new Map>(); 114 | 115 | while (!isDone(remainingDrivers, remainingPassengers)) { 116 | if (remainingPassengers.length > 0 && remainingDrivers.length === 0) break; 117 | // const distances = calculatePassengerDistances(remainingDrivers, remainingPassengers); 118 | 119 | if (remainingPassengers.length > 0) { 120 | const closestDriverToPassengers = new Map(); 121 | 122 | for (const passenger of remainingPassengers) { 123 | let minDriverDistance = Infinity; 124 | let minDriverId = ''; 125 | for (const driver of remainingDrivers) { 126 | // // calc distance between driver and passenger 127 | // console.log("this shit ran") 128 | // console.log(distanceMatrix.data.keys, distanceMatrix.data.values) 129 | 130 | // console.log("this is the key were trying to get", { 131 | // latitude: driver.location.latlng.lat, 132 | // longitude: driver.location.latlng.lng 133 | // }) 134 | 135 | const distanceMap = distanceMatrix.data.get({ 136 | latitude: driver.location.latlng.lat, 137 | longitude: driver.location.latlng.lng, 138 | }); 139 | 140 | const distance = distanceMap.get({ 141 | latitude: passenger.location.latlng.lat, 142 | longitude: passenger.location.latlng.lng, 143 | }); 144 | 145 | console.log(distance, ' from ', passenger.name, driver.name); 146 | 147 | // const distance = haversine({ 148 | // latitude: driver.location.latlng.lat, 149 | // longitude: driver.location.latlng.lng, 150 | // }, { 151 | // latitude: passenger.location.latlng.lat, 152 | // longitude: passenger.location.latlng.lng, 153 | // }, {unit: 'meter'}) 154 | 155 | if (distance < minDriverDistance) { 156 | minDriverDistance = distance; 157 | minDriverId = driver.id; 158 | } 159 | } 160 | 161 | closestDriverToPassengers.set(passenger.id, [minDriverId, minDriverDistance]); 162 | } 163 | 164 | const numericCompare = (a: number, b: number) => (a < b ? 1 : a > b ? -1 : 0); 165 | 166 | const comparator = (a: any, b: any) => { 167 | return numericCompare(a.minDriverDistance, b.minDriverDistance); 168 | }; 169 | 170 | const pq = new PriorityQueue({ comparator }); 171 | 172 | for (const passenger of remainingPassengers) { 173 | const array = closestDriverToPassengers.get(passenger.id); 174 | if (!array) continue; 175 | const [minDriverId, minDriverDistance] = array; 176 | 177 | const node = { 178 | passengerId: passenger.id, 179 | minDriverId, 180 | minDriverDistance, 181 | }; 182 | 183 | pq.enqueue(node); 184 | } 185 | 186 | const nextPassengerNode: any = pq.dequeue(); 187 | if (!nextPassengerNode) continue; 188 | const nextPassenger = getPersonById(people as People[], nextPassengerNode.passengerId); 189 | const minDriver = getPersonById(people as People[], nextPassengerNode.minDriverId); 190 | 191 | if (!nextPassenger || !minDriver) continue; 192 | 193 | minDriver.location.latlng = { 194 | lat: nextPassenger?.location.latlng.lat, 195 | lng: nextPassenger?.location.latlng.lng, 196 | }; 197 | 198 | const passengerIds: string[] = solution.get(minDriver.id) || []; 199 | passengerIds.push(nextPassenger.id); 200 | solution.set(minDriver.id, passengerIds); 201 | 202 | minDriver.seats--; 203 | if (minDriver.seats === 0) { 204 | remainingDrivers = removePerson(remainingDrivers, minDriver.id); 205 | } 206 | remainingPassengers = removePerson(remainingPassengers, nextPassenger.id); 207 | // TODO add to database for visualization of this algorithm 208 | } 209 | // no more remaining passengers but still have drivers 210 | if (remainingDrivers.length === 1 && remainingPassengers.length === 0) { 211 | if (!solution.has(remainingDrivers[0].id)) solution.set(remainingDrivers[0].id, []); 212 | remainingDrivers = removePerson(remainingDrivers, remainingDrivers[0].id); 213 | } 214 | 215 | if (remainingPassengers.length === 0) { 216 | const driverToBeConverted = getSomeoneDrivingNoOne(remainingDrivers, solution); 217 | if (driverToBeConverted) { 218 | // exists 219 | remainingPassengers.push(driverToBeConverted); 220 | removePerson(remainingDrivers, driverToBeConverted.id); 221 | } else { 222 | break; 223 | } 224 | } 225 | } 226 | return strMapToObj(solution); 227 | } 228 | 229 | function strMapToObj(strMap: any): any { 230 | const obj = Object.create(null); 231 | for (const [k, v] of strMap) { 232 | // We don’t escape the key '__proto__' 233 | // which can cause problems on older engines 234 | obj[k] = v; 235 | } 236 | return obj; 237 | } 238 | 239 | function getSomeoneDrivingNoOne(drivers: People[], solution: Map>): People | undefined { 240 | for (const driver of drivers) { 241 | if (!solution.has(driver.id)) { 242 | return driver; 243 | } 244 | } 245 | return undefined; 246 | } 247 | 248 | function removePerson(people: People[], id: string): People[] { 249 | for (let i = 0; i < people.length; i++) { 250 | const driver = people[i]; 251 | if (driver.id === id) { 252 | people.splice(i, 1); 253 | } 254 | } 255 | return people; 256 | } 257 | 258 | function getPersonById(people: People[], id: string): People | undefined { 259 | for (const person of people) { 260 | if (person.id === id) { 261 | return person; 262 | } 263 | } 264 | return undefined; 265 | } 266 | 267 | /** 268 | * returns whether the solution contains all the people in an event 269 | * 270 | * @param {People} people 271 | * @param { Map>} solution 272 | * @returns boolean 273 | */ 274 | function isDone(drivers: People[], passengers: People[]): boolean { 275 | return drivers.length === 0 && passengers.length === 0; 276 | } 277 | 278 | /** 279 | * Return all drivers from a list of people 280 | * 281 | * @param {People[]} people 282 | * @returns People[] 283 | */ 284 | function getDrivers(people: People[]): People[] { 285 | return people.filter((person) => { 286 | return person.canDrive; 287 | }); 288 | } 289 | 290 | /** 291 | * Return all passengers from a list of people 292 | * 293 | * @param {People[]} people 294 | * @returns People[] 295 | */ 296 | function getPassengers(people: People[]): People[] { 297 | return people.filter((person) => { 298 | return !person.canDrive; 299 | }); 300 | } 301 | 302 | // export const getEncoding = functions.https.onRequest(async (request: any, response: any) => { 303 | // const { route } = request.body 304 | // await Axios.get( 305 | // `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.lat},${origin.lng}&destination=${destination.lat},${destination.lng}&${waypoint}&key=${key}`, 306 | // ); 307 | // const origin = route[0]; 308 | // const destination = route[route.length - 1]; 309 | // if (route.length > 3) { 310 | // let waypoint = 'waypoints='; 311 | // // , === %2C 312 | // // | seperate locations 313 | // for (let i = 1; i < route.length - 1; i++) { 314 | // if (i == route.length - 2) { 315 | // waypoint = waypoint + route[i].lat + '%2C' + route[i].lat; 316 | // } else { 317 | // waypoint = waypoint + route[i].lat + '%2C' + route[i].lat + '|'; 318 | // } 319 | // } 320 | 321 | // const response = await axios.get( 322 | // `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.lat},${origin.lng}&destination=${destination.lat},${destination.lng}&${waypoint}&key=${key}`, 323 | // ); 324 | // // console.log(response); 325 | // } else { 326 | // // const response = await axios.get( 327 | // // `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.lat},${origin.lng}&destination=${destination.lat},${destination.lng}&key=${key}`, 328 | // // ); 329 | // // console.log(response); 330 | // } 331 | // } 332 | -------------------------------------------------------------------------------- /functions/src/priorityQueue.ts: -------------------------------------------------------------------------------- 1 | 2 | // export class Node{ 3 | // value: any; 4 | // priority: number 5 | // constructor(value: any, priority: number){ 6 | // this.value = value 7 | // this.priority = priority 8 | // } 9 | // } 10 | 11 | // export class PriorityQueue{ 12 | 13 | // values: Node[] 14 | 15 | // constructor(){ 16 | // this.values = [] 17 | // } 18 | 19 | // //helper method that swaps the values and two indexes of an array 20 | // swap(index1: number, index2: number){ 21 | // let temp = this.values[index1]; 22 | // this.values[index1] = this.values[index2]; 23 | // this.values[index2] = temp; 24 | // return this.values; 25 | // } 26 | // //helper methods that bubbles up values from end 27 | // bubbleUp(){ 28 | // //get index of inserted element 29 | // let index = this.values.length - 1 30 | // //loop while index is not 0 or element no loger needs to bubble 31 | // while(index > 0){ 32 | // //get parent index via formula 33 | // let parentIndex = Math.floor((index - 1)/2); 34 | // //if values is greater than parent, swap the two 35 | // if(this.values[parentIndex].priority > this.values[index].priority){ 36 | // //swap with helper method 37 | // this.swap(index, parentIndex); 38 | // //change current index to parent index 39 | // index = parentIndex; 40 | // } else{ 41 | // break; 42 | // } 43 | // } 44 | // return 0; 45 | // } 46 | // // method that pushes new value onto the end and calls the bubble helper 47 | // enqueue(value: Node){ 48 | // this.values.push(value) 49 | // //calculate parent, if parent is greater swap 50 | // //while loop or recurse 51 | // this.bubbleUp(); 52 | // return this.values 53 | // } 54 | 55 | // bubbleDown(){ 56 | // let parentIndex = 0; 57 | // const length = this.values.length; 58 | // const elementPriority = this.values[0].priority; 59 | // //loop breaks if no swaps are needed 60 | // while (true){ 61 | // //get indexes of child elements by following formula 62 | // let leftChildIndex = (2 * parentIndex) + 1; 63 | // let rightChildIndex = (2 * parentIndex) + 2; 64 | // let leftChildPriority, rightChildPriority; 65 | // let indexToSwap = null; 66 | // // if left child exists, and is greater than the element, plan to swap with the left child index 67 | // if(leftChildIndex < length){ 68 | // leftChildPriority = this.values[leftChildIndex].priority 69 | // if(leftChildPriority < elementPriority){ 70 | // indexToSwap = leftChildIndex; 71 | // } 72 | // } 73 | // //if right child exists 74 | // if(rightChildIndex < length){ 75 | // rightChildPriority = this.values[rightChildIndex].priority 76 | 77 | // if( 78 | // //if right child is greater than element and there are no plans to swap 79 | // (rightChildPriority < elementPriority && indexToSwap === null) || 80 | // //OR if right child is greater than left child and there ARE plans to swap 81 | // (rightChildPriority < leftChildPriority && indexToSwap !== null)) 82 | // { 83 | // //plan to swap with the right child 84 | // indexToSwap = rightChildIndex 85 | // } 86 | // } 87 | // //if there are no plans to swap, break out of the loop 88 | // if(indexToSwap === null){ 89 | // break; 90 | // } 91 | // //swap with planned element 92 | // this.swap(parentIndex, indexToSwap); 93 | // //starting index is now index that we swapped with 94 | // parentIndex = indexToSwap; 95 | // } 96 | // } 97 | 98 | // dequeue(){ 99 | // //swap first and last element 100 | // this.swap(0, this.values.length - 1); 101 | // //pop max value off of values 102 | // let poppedNode = this.values.pop(); 103 | // //re-adjust heap if length is greater than 1 104 | // if(this.values.length > 1){ 105 | // this.bubbleDown(); 106 | // } 107 | 108 | // return poppedNode; 109 | // } 110 | // } 111 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017", 10 | "esModuleInterop": true, 11 | 12 | }, 13 | "compileOnSave": true, 14 | "include": [ 15 | "src" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /functions/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": true, 14 | 15 | // Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars. 16 | "no-parameter-reassignment": true, 17 | 18 | // Force the use of ES6-style imports instead of /// imports. 19 | "no-reference": true, 20 | 21 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 22 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 23 | "no-unnecessary-type-assertion": true, 24 | 25 | // Disallow nonsensical label usage. 26 | "label-position": true, 27 | 28 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 29 | "no-conditional-assignment": true, 30 | 31 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 32 | "no-construct": true, 33 | 34 | // Do not allow super() to be called twice in a constructor. 35 | "no-duplicate-super": true, 36 | 37 | // Do not allow the same case to appear more than once in a switch block. 38 | "no-duplicate-switch-case": true, 39 | 40 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 41 | // rule. 42 | "no-duplicate-variable": [true, "check-parameters"], 43 | 44 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 45 | // instead use a separate variable name. 46 | "no-shadowed-variable": true, 47 | 48 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 49 | "no-empty": [true, "allow-empty-catch"], 50 | 51 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 52 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 53 | "no-floating-promises": true, 54 | 55 | // Do not allow any imports for modules that are not in package.json. These will almost certainly fail when 56 | // deployed. 57 | "no-implicit-dependencies": true, 58 | 59 | // The 'this' keyword can only be used inside of classes. 60 | "no-invalid-this": true, 61 | 62 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 63 | "no-string-throw": true, 64 | 65 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 66 | "no-unsafe-finally": true, 67 | 68 | // Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid(); 69 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 70 | 71 | // Disallow duplicate imports in the same file. 72 | "no-duplicate-imports": true, 73 | 74 | 75 | // -- Strong Warnings -- 76 | // These rules should almost never be needed, but may be included due to legacy code. 77 | // They are left as a warning to avoid frustration with blocked deploys when the developer 78 | // understand the warning and wants to deploy anyway. 79 | 80 | // Warn when an empty interface is defined. These are generally not useful. 81 | "no-empty-interface": {"severity": "warning"}, 82 | 83 | // Warn when an import will have side effects. 84 | "no-import-side-effect": {"severity": "warning"}, 85 | 86 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 87 | // most values and let for values that will change. 88 | "no-var-keyword": {"severity": "warning"}, 89 | 90 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 91 | "triple-equals": {"severity": "warning"}, 92 | 93 | // Warn when using deprecated APIs. 94 | "deprecation": {"severity": "warning"}, 95 | 96 | // -- Light Warnings -- 97 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 98 | // if TSLint supported such a level. 99 | 100 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 101 | // (Even better: check out utils like .map if transforming an array!) 102 | "prefer-for-of": {"severity": "warning"}, 103 | 104 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 105 | "unified-signatures": {"severity": "warning"}, 106 | 107 | // Prefer const for values that will not change. This better documents code. 108 | "prefer-const": {"severity": "warning"}, 109 | 110 | // Multi-line object literals and function calls should have a trailing comma. This helps avoid merge conflicts. 111 | "trailing-comma": {"severity": "warning"} 112 | }, 113 | 114 | "defaultSeverity": "error" 115 | } 116 | -------------------------------------------------------------------------------- /images/RSVPPage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/images/RSVPPage.jpg -------------------------------------------------------------------------------- /images/appMap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/images/appMap.jpg -------------------------------------------------------------------------------- /images/branding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/images/branding.png -------------------------------------------------------------------------------- /images/dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/images/dashboard.jpg -------------------------------------------------------------------------------- /images/homescreen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/images/homescreen.jpg -------------------------------------------------------------------------------- /images/homescreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/images/homescreen.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carpool", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@date-io/luxon": "^1.3.13", 7 | "@googlemaps/google-maps-services-js": "^3.3.16", 8 | "@material-ui/core": "^4.11.0", 9 | "@material-ui/icons": "^4.9.1", 10 | "@material-ui/lab": "^4.0.0-alpha.56", 11 | "@material-ui/pickers": "^3.2.10", 12 | "@react-google-maps/api": "^1.9.9", 13 | "@testing-library/jest-dom": "^4.2.4", 14 | "@testing-library/react": "^9.5.0", 15 | "@testing-library/user-event": "^7.2.1", 16 | "@types/autosuggest-highlight": "^3.1.0", 17 | "@types/haversine": "^1.1.4", 18 | "@types/jest": "^24.9.1", 19 | "@types/lodash": "^4.14.158", 20 | "@types/luxon": "^1.24.3", 21 | "@types/node": "^12.12.53", 22 | "@types/react": "^16.9.43", 23 | "@types/react-dom": "^16.9.8", 24 | "@types/react-places-autocomplete": "^7.2.6", 25 | "@types/react-router-dom": "^5.1.5", 26 | "@types/react-window": "^1.8.2", 27 | "@typescript-eslint/eslint-plugin": "^3.7.1", 28 | "@typescript-eslint/parser": "^3.7.1", 29 | "autosuggest-highlight": "^3.1.1", 30 | "axios": "^1.6.0", 31 | "eslint-plugin-react": "^7.20.5", 32 | "fastpriorityqueue": "^0.6.3", 33 | "firebase": "^7.17.1", 34 | "firebase-admin": "^12.0.0", 35 | "firebase-functions": "^3.9.0", 36 | "google-maps-react": "^2.0.6", 37 | "haversine": "^1.1.1", 38 | "lodash": "^4.17.21", 39 | "luxon": "^1.28.1", 40 | "node-sass": "^9.0.0", 41 | "priorityqueue": "^1.0.0", 42 | "react": "^16.13.1", 43 | "react-dom": "^16.13.1", 44 | "react-firebase-hooks": "^2.2.0", 45 | "react-places-autocomplete": "^7.3.0", 46 | "react-router-dom": "^5.2.0", 47 | "react-scripts": "5.0.1", 48 | "react-window": "^1.8.5", 49 | "tslint": "^6.1.2", 50 | "typescript": "^3.7.5" 51 | }, 52 | "scripts": { 53 | "lint": "eslint src --ext js,jsx,ts,tsx --fix", 54 | "start": "react-scripts start", 55 | "build": "react-scripts build", 56 | "test": "react-scripts test", 57 | "eject": "react-scripts eject" 58 | }, 59 | "eslintConfig": { 60 | "extends": "react-app" 61 | }, 62 | "browserslist": { 63 | "production": [ 64 | ">0.2%", 65 | "not dead", 66 | "not op_mini all" 67 | ], 68 | "development": [ 69 | "last 1 chrome version", 70 | "last 1 firefox version", 71 | "last 1 safari version" 72 | ] 73 | }, 74 | "devDependencies": { 75 | "eslint-config-prettier": "^6.11.0", 76 | "eslint-plugin-prettier": "3.1.4", 77 | "prettier": "2.0.5", 78 | "prettier-eslint": "11.0.0", 79 | "prettier-eslint-cli": "5.0.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/public/cover.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 29 | 30 | 32 | 36 | 37 | groUber 38 | 39 | 40 | 41 |
42 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /public/login_art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/public/login_art.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/public/logo.png -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelfromyeg/groUber/ceee9ce08cf1da37904944c9ac5ca34afc6476fe/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "groUber", 3 | "name": "groUber", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#FFFFFF", 24 | "background_color": "#202020" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactElement } from 'react'; 2 | import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; 3 | import Index from './pages/Home'; 4 | import Dashboard from './pages/dashboard'; 5 | import Form from './pages/form'; 6 | import Notification from './components/Notification'; 7 | import theme from './styles/theme'; 8 | import ThemeProvider from '@material-ui/styles/ThemeProvider'; 9 | import NotFound from './pages/404'; 10 | import Login from './pages/login'; 11 | import * as firebase from 'firebase'; 12 | import 'firebase/auth'; 13 | 14 | function PrivateRoute({ component: Component, ...rest }: any): ReactElement { 15 | return ( 16 | { 19 | if (firebase.auth().currentUser) { 20 | return ; 21 | } else { 22 | window.localStorage.setItem('location', props?.location?.pathname); 23 | return ; 24 | } 25 | }} 26 | /> 27 | ); 28 | } 29 | 30 | function PublicRoute({ component: Component, onUserLogin, ...rest }: any): ReactElement { 31 | return ( 32 | { 35 | if (!firebase.auth().currentUser) { 36 | return ; 37 | } else { 38 | const route = window.localStorage.getItem('location') || '/'; 39 | window.localStorage.removeItem('location'); 40 | return ; 41 | } 42 | }} 43 | /> 44 | ); 45 | } 46 | 47 | const App = (): ReactElement => { 48 | const [, setUser] = useState(firebase.auth().currentUser); 49 | firebase.auth().onAuthStateChanged(setUser); 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default App; 67 | -------------------------------------------------------------------------------- /src/_types/address.d.ts: -------------------------------------------------------------------------------- 1 | export interface Address { 2 | _id: string; 3 | location: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/_types/event.d.ts: -------------------------------------------------------------------------------- 1 | import { People } from './people'; 2 | 3 | export interface Event { 4 | id: string; 5 | name: string; 6 | date: Date; 7 | host: People | firebase.firestore.DocumentReference; 8 | people: People[] | firebase.firestore.DocumentReference[]; 9 | destination: { 10 | address: string; 11 | latlng: { 12 | lat: number; 13 | lng: number; 14 | }; 15 | }; 16 | path?: any; // TODO: 17 | } 18 | -------------------------------------------------------------------------------- /src/_types/people.d.ts: -------------------------------------------------------------------------------- 1 | import { Address } from './address'; 2 | 3 | export interface People { 4 | id: string; 5 | name: string; 6 | email: string; 7 | profilePicture: string; 8 | userId: string; 9 | canDrive: boolean; 10 | seats: number; 11 | isHost?: boolean; 12 | event?: Event | firebase.firestore.DocumentReference; 13 | location: { 14 | address: Address; 15 | latlng: { 16 | lat: number; 17 | lng: number; 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/DemoCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Card from '@material-ui/core/Card'; 4 | import CardActions from '@material-ui/core/CardActions'; 5 | import CardContent from '@material-ui/core/CardContent'; 6 | import Button from '@material-ui/core/Button'; 7 | import Typography from '@material-ui/core/Typography'; 8 | import { Link } from 'react-router-dom'; 9 | import { Container } from '@material-ui/core'; 10 | import * as firebase from 'firebase'; 11 | 12 | const useStyles = makeStyles({ 13 | root: { 14 | minWidth: 200, 15 | }, 16 | bullet: { 17 | display: 'inline-block', 18 | margin: '0 2px', 19 | transform: 'scale(0.8)', 20 | }, 21 | title: { 22 | fontSize: 14, 23 | }, 24 | pos: { 25 | marginBottom: 12, 26 | }, 27 | }); 28 | 29 | export default function DemoCard() { 30 | const classes = useStyles(); 31 | const eventId = 'NI4yopwnFbu3sbZNbRTa'; 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | Want to try out a demo? 39 |
40 |
41 | 50 |
51 |
52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/EventForm.tsx: -------------------------------------------------------------------------------- 1 | import 'luxon'; 2 | import React, { useState } from 'react'; 3 | import Button from '@material-ui/core/Button'; 4 | import CssBaseline from '@material-ui/core/CssBaseline'; 5 | import TextField from '@material-ui/core/TextField'; 6 | import Box from '@material-ui/core/Box'; 7 | import Typography from '@material-ui/core/Typography'; 8 | import { makeStyles } from '@material-ui/core/styles'; 9 | import Container from '@material-ui/core/Container'; 10 | import * as firebase from 'firebase'; 11 | import 'firebase/auth'; 12 | import { useHistory } from 'react-router-dom'; 13 | import useAutoCompletePlaces from '../hooks/UseAutocompletePlaces'; 14 | import LuxonUtils from '@date-io/luxon'; 15 | import { DateTime } from 'luxon'; 16 | import { MuiPickersUtilsProvider, KeyboardDateTimePicker } from '@material-ui/pickers'; 17 | 18 | const useStyles = makeStyles((theme) => ({ 19 | paper: { 20 | marginTop: theme.spacing(8), 21 | display: 'flex', 22 | flexDirection: 'column', 23 | alignItems: 'center', 24 | }, 25 | avatar: { 26 | margin: theme.spacing(1), 27 | backgroundColor: theme.palette.secondary.main, 28 | }, 29 | form: { 30 | width: '100%', // Fix IE 11 issue. 31 | marginTop: theme.spacing(1), 32 | }, 33 | submit: { 34 | margin: theme.spacing(3, 0, 2), 35 | }, 36 | })); 37 | 38 | function EventForm() { 39 | const classes = useStyles(); 40 | const history = useHistory(); 41 | const db = firebase.firestore(); 42 | 43 | const [eventName, setName] = useState(''); 44 | const [selectedDate, handleDateChange] = useState(null); 45 | 46 | const [address, latlng, AutoCompletePlaces] = useAutoCompletePlaces('Add the Destination Address'); 47 | 48 | const currentUser = firebase.auth().currentUser; 49 | 50 | return ( 51 | 52 | 53 |
54 | 55 | You are making this event as: {currentUser?.displayName} ({currentUser?.email}) 56 | 57 |
58 | 59 | Enter Event Details! 60 | 61 |
{ 64 | e.preventDefault(); 65 | const result = await db.collection('people').add({ 66 | name: currentUser.displayName, 67 | email: currentUser.email, 68 | profilePicture: currentUser.photoURL, 69 | userId: currentUser.uid, 70 | canDrive: false, 71 | seats: 0, 72 | isHost: true, 73 | location: { 74 | latlng, 75 | address, 76 | }, 77 | }); 78 | console.log(selectedDate); 79 | const eventRef = await db.collection('events').add({ 80 | name: eventName, 81 | host: (await result.get()).ref, 82 | people: [], 83 | date: firebase.firestore.Timestamp.fromDate(selectedDate?.toJSDate()), 84 | destination: { 85 | latlng, 86 | address, 87 | }, 88 | }); 89 | result.update({ 90 | event: (await eventRef.get()).ref, 91 | }); 92 | history.push(`/event/${eventRef.id}/dashboard`); 93 | }} 94 | > 95 | setName(e.target.value)} 105 | /> 106 | 107 | 119 | 120 | {AutoCompletePlaces} 121 | 124 | 125 |
126 | 127 |
128 | ); 129 | } 130 | 131 | export default EventForm; 132 | -------------------------------------------------------------------------------- /src/components/EventList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Card from '@material-ui/core/Card'; 4 | import CardActions from '@material-ui/core/CardActions'; 5 | import { useHistory } from 'react-router-dom'; 6 | import CardContent from '@material-ui/core/CardContent'; 7 | import Button from '@material-ui/core/Button'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import Grid from '@material-ui/core/Grid'; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | root: { 13 | flexGrow: 1, 14 | }, 15 | card: { 16 | position: 'relative', 17 | width: 300, 18 | height: 300, 19 | padding: theme.spacing(2), 20 | }, 21 | title: { 22 | fontSize: 14, 23 | }, 24 | pos: { 25 | marginBottom: 12, 26 | }, 27 | actions: { 28 | position: 'absolute', 29 | bottom: 2, 30 | left: 2, 31 | }, 32 | })); 33 | 34 | const Event = (props: any) => { 35 | const history = useHistory(); 36 | const classes = useStyles(); 37 | return ( 38 | 39 | 40 | 41 | May 25, 2020 42 | 43 | 44 | {props.name} 45 | 46 | 47 | {props.destination} 48 | 49 | 50 | This is an event description, some helper text, or something similar. 51 | 52 | 53 | 54 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | const EventList = (props: any) => { 68 | console.log(props.eventList); 69 | return ( 70 | 71 | {props.eventList.map((event: any, index: any) => ( 72 | 73 | 74 | 75 | ))} 76 | 77 | ); 78 | }; 79 | 80 | export default EventList; 81 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => { 4 | return
Grouber © 2020
; 5 | }; 6 | 7 | export default Footer; 8 | -------------------------------------------------------------------------------- /src/components/GuestForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import CssBaseline from '@material-ui/core/CssBaseline'; 4 | import TextField from '@material-ui/core/TextField'; 5 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 6 | import Checkbox from '@material-ui/core/Checkbox'; 7 | import Box from '@material-ui/core/Box'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | import Container from '@material-ui/core/Container'; 11 | import * as firebase from 'firebase'; 12 | import { useParams } from 'react-router-dom'; 13 | import useAutoCompletePlaces from '../hooks/UseAutocompletePlaces'; 14 | import { Collapse, CircularProgress } from '@material-ui/core'; 15 | import { useDocument, useCollectionData } from 'react-firebase-hooks/firestore'; 16 | import { Event } from '../_types/event'; 17 | import 'firebase/auth'; 18 | import { People } from 'src/_types/people'; 19 | import throttle from 'lodash/throttle'; 20 | 21 | const autocompleteService: any = { current: null }; 22 | 23 | const useStyles = makeStyles((theme) => ({ 24 | paper: { 25 | marginTop: theme.spacing(8), 26 | display: 'flex', 27 | flexDirection: 'column', 28 | alignItems: 'center', 29 | }, 30 | avatar: { 31 | margin: theme.spacing(1), 32 | backgroundColor: theme.palette.secondary.main, 33 | }, 34 | form: { 35 | width: '100%', // Fix IE 11 issue. 36 | marginTop: theme.spacing(1), 37 | }, 38 | submit: { 39 | margin: theme.spacing(3, 0, 2), 40 | }, 41 | })); 42 | 43 | function GuestForm() { 44 | const classes = useStyles(); 45 | const db = firebase.firestore(); 46 | const { eventId } = useParams(); 47 | const [host, setHost] = useState(null); 48 | 49 | const [event, loading] = useDocument(firebase.firestore().collection('events').doc(eventId), { 50 | snapshotListenOptions: { 51 | includeMetadataChanges: true, 52 | }, 53 | }); 54 | 55 | const [address, latlng, AutoCompletePlaces, setLatLng, setInputValue, setValue] = useAutoCompletePlaces( 56 | 'Add Your Location', 57 | ); 58 | const [seats, setSeats] = useState('0'); 59 | const [submitted, setSubmit] = useState(false); 60 | 61 | const [checked, setChecked] = React.useState(false); 62 | 63 | const eventData: Event = event?.data() as Event; 64 | useEffect(() => { 65 | const eventHost = eventData?.host as firebase.firestore.DocumentReference; 66 | eventHost?.get()?.then((host) => { 67 | setHost(host.data() as People); 68 | }); 69 | if (event?.ref) { 70 | db.collection('people') 71 | .where('userId', '==', firebase.auth().currentUser.uid) 72 | .where('event', '==', event.ref) 73 | .get() 74 | .then((data) => { 75 | const oldData: People = data.docs[0].data() as People; 76 | new (window as any).google.maps.places.AutocompleteService().getPlacePredictions( 77 | { input: String(oldData?.location?.address) }, 78 | (results?: PlaceType[]) => { 79 | setValue(results[0]); 80 | }, 81 | ); 82 | setInputValue(oldData?.location?.address); 83 | setLatLng(oldData?.location?.latlng); 84 | setChecked(oldData?.canDrive); 85 | setSeats(String(oldData?.seats || 0)); 86 | }); 87 | } 88 | }, [event]); 89 | 90 | const currentUser = firebase.auth().currentUser; 91 | async function handleSubmit(e: any) { 92 | e.preventDefault(); 93 | const exists = await db 94 | .collection('people') 95 | .where('userId', '==', firebase.auth().currentUser.uid) 96 | .where('event', '==', event.ref) 97 | .get(); 98 | if (exists.docs.length > 0) { 99 | exists.docs[0].ref.update({ 100 | canDrive: checked, 101 | seats: checked ? Number(seats) : 0, 102 | location: { 103 | latlng, 104 | address, 105 | }, 106 | }); 107 | } else { 108 | const result = await db.collection('people').add({ 109 | name: currentUser.displayName, 110 | email: currentUser.email, 111 | profilePicture: currentUser.photoURL, 112 | userId: currentUser.uid, 113 | canDrive: checked, 114 | seats: checked ? Number(seats) : 0, 115 | location: { 116 | latlng, 117 | address, 118 | }, 119 | event: event.ref, 120 | }); 121 | await event.ref.update({ 122 | people: firebase.firestore.FieldValue.arrayUnion(result), 123 | }); 124 | } 125 | setSubmit(true); 126 | } 127 | 128 | return ( 129 | <> 130 | {loading ? ( 131 | 132 | ) : !submitted ? ( 133 | 134 | 135 |
136 | 137 | {host?.name} is inviting you to attend {eventData?.name}. 138 | 139 |
140 | 141 | This event is at {eventData?.destination?.address}. 142 |
143 |
144 | It is happening on {new Date((eventData?.date as any)?.seconds * 1000).toString()}. 145 |
146 |
147 | 148 | You are RSVPing as: {currentUser?.displayName} ({currentUser?.email}) 149 | 150 |
151 |
152 | 153 | Where are you coming from? 154 | 155 | {AutoCompletePlaces} 156 | { 161 | setChecked(!checked); 162 | }} 163 | name="isDriver" 164 | /> 165 | } 166 | label="Are you Driving?" 167 | /> 168 | 169 | setSeats(e.target.value)} 176 | InputLabelProps={{ 177 | shrink: true, 178 | }} 179 | /> 180 | 181 | 190 | 191 |
192 | 193 |
194 | ) : ( 195 | Form Submitted! 196 | )} 197 | 198 | ); 199 | } 200 | 201 | interface PlaceType { 202 | description: string; 203 | structured_formatting: { 204 | main_text: string; 205 | secondary_text: string; 206 | main_text_matched_substrings: [ 207 | { 208 | offset: number; 209 | length: number; 210 | }, 211 | ]; 212 | }; 213 | } 214 | 215 | export default GuestForm; 216 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { AppBar, Button, Toolbar, IconButton, Typography, makeStyles, Grid } from '@material-ui/core'; 4 | import HomeIcon from '@material-ui/icons/Home'; 5 | import MenuIcon from '@material-ui/icons/Menu'; 6 | import * as firebase from 'firebase'; 7 | 8 | const useStyles = makeStyles((theme) => ({ 9 | menuButton: { 10 | marginRight: theme.spacing(2), 11 | }, 12 | })); 13 | 14 | const Header = ({ setSidebarOpen }: { setSidebarOpen?: React.Dispatch> }) => { 15 | const history = useHistory(); 16 | const classes = useStyles(); 17 | return ( 18 | 19 | 20 | 21 | 22 | {setSidebarOpen && ( 23 | { 29 | setSidebarOpen(true); 30 | }} 31 | > 32 | 33 | 34 | )} 35 | { 41 | history.push('/'); 42 | }} 43 | > 44 | 45 | 46 | 47 | 48 | 49 | groUber 50 | 51 | 52 | 53 | 62 | 63 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default Header; 70 | -------------------------------------------------------------------------------- /src/components/LinkDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; 4 | 5 | const useStyles = makeStyles((theme: Theme) => 6 | createStyles({ 7 | root: { 8 | '& .MuiTextField-root': { 9 | margin: theme.spacing(1), 10 | width: '25ch', 11 | }, 12 | }, 13 | }), 14 | ); 15 | 16 | export default function LinkDisplay(props: any) { 17 | const classes = useStyles(); 18 | 19 | return ( 20 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/LinkSharing.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; 4 | 5 | const useStyles = makeStyles((theme: Theme) => 6 | createStyles({ 7 | root: { 8 | '& .MuiTextField-root': { 9 | margin: theme.spacing(1), 10 | width: '25ch', 11 | }, 12 | }, 13 | }), 14 | ); 15 | 16 | export default function FormPropsTextFields(props: any) { 17 | const classes = useStyles(); 18 | return ( 19 |
20 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import CssBaseline from '@material-ui/core/CssBaseline'; 4 | import Box from '@material-ui/core/Box'; 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | import Container from '@material-ui/core/Container'; 7 | import * as firebase from 'firebase'; 8 | import 'firebase/auth'; 9 | 10 | //eslint-disable-next-line 11 | const globalAny: any = global 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | paper: { 15 | marginTop: theme.spacing(8), 16 | display: 'flex', 17 | flexDirection: 'column', 18 | alignItems: 'center', 19 | }, 20 | avatar: { 21 | margin: theme.spacing(1), 22 | backgroundColor: theme.palette.secondary.main, 23 | }, 24 | form: { 25 | width: '100%', // Fix IE 11 issue. 26 | marginTop: theme.spacing(1), 27 | }, 28 | submit: { 29 | margin: theme.spacing(3, 0, 2), 30 | }, 31 | })); 32 | 33 | function LoginForm() { 34 | const classes = useStyles(); 35 | const provider = new firebase.auth.GoogleAuthProvider(); 36 | 37 | return ( 38 | 39 | 40 |
41 | 61 |
62 | 63 |
64 | ); 65 | } 66 | 67 | export default LoginForm; 68 | -------------------------------------------------------------------------------- /src/components/Map/id-to-lat-lng.ts: -------------------------------------------------------------------------------- 1 | import { Dictionary } from 'lodash'; 2 | 3 | interface LatLng { 4 | lat: number; 5 | lng: number; 6 | } 7 | function idToLatLngFunc(members: any) { 8 | const res: { [id: string]: LatLng } = {}; 9 | for (const member of members) { 10 | // console.log(member); 11 | res[member.id] = member.location.latlng; 12 | } 13 | return res; 14 | } 15 | 16 | export default idToLatLngFunc; 17 | -------------------------------------------------------------------------------- /src/components/Map/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState, useEffect } from 'react'; 2 | import { Map, GoogleApiWrapper, Marker, Polyline } from 'google-maps-react'; 3 | import theme from './theme'; 4 | import useEventPeople from '../../hooks/useEventPeople'; 5 | import { useParams } from 'react-router-dom'; 6 | import { useDocumentData } from 'react-firebase-hooks/firestore'; 7 | import firebase from 'firebase'; 8 | import { People } from 'src/_types/people'; 9 | import idToLatLngFunc from './id-to-lat-lng'; 10 | import { Event } from 'src/_types/event'; 11 | import axios from 'axios'; 12 | import { useFetch } from 'src/hooks/UseFetch'; 13 | import { Client, Status } from '@googlemaps/google-maps-services-js'; 14 | 15 | const client = new Client({}); 16 | const key = 'AIzaSyCBcFhe366d-l6wIvrX2lNfsmuthtkZKGM'; 17 | 18 | const MapContainer = ({ google, center }: { google: any; center: google.maps.LatLngLiteral }): ReactElement => { 19 | // each polyline represents a route for a driver. 20 | 21 | // const newPath = google.maps.geometry.encoding.decodePath( 22 | // 'a~l~Fjk~uOnzh@vlbBtc~@tsE`vnApw{A`dw@~w\\|tNtqf@l{Yd_Fblh@rxo@b}@xxSfytAblk@xxaBeJxlcBb~t@zbh@jc|Bx}C`rv@rw|@rlhA~dVzeo@vrSnc}Axf]fjz@xfFbw~@dz{A~d{A|zOxbrBbdUvpo@`cFp~xBc`Hk@nurDznmFfwMbwz@bbl@lq~@loPpxq@bw_@v|{CbtY~jGqeMb{iF|n\\~mbDzeVh_Wr|Efc\\x`Ij{kE}mAb~uF{cNd}xBjp]fulBiwJpgg@|kHntyArpb@bijCk_Kv~eGyqTj_|@`uV`k|DcsNdwxAott@r}q@_gc@nu`CnvHx`k@dse@j|p@zpiAp|gEicy@`omFvaErfo@igQxnlApqGze~AsyRzrjAb__@ftyB}pIlo_BflmA~yQftNboWzoAlzp@mz`@|}_@fda@jakEitAn{fB_a]lexClshBtmqAdmY_hLxiZd~XtaBndgC', 23 | // ); 24 | 25 | // console.log(newPath); 26 | const { eventId } = useParams(); 27 | const [eventData] = useDocumentData(firebase.firestore().collection('events').doc(eventId), { 28 | snapshotListenOptions: { 29 | includeMetadataChanges: true, 30 | }, 31 | }); 32 | const [solution, setSolution] = useState(undefined); 33 | useEffect(() => { 34 | const data = axios 35 | .get(`https://us-central1-find-my-carpool.cloudfunctions.net/solve?eventId=${eventId}`) 36 | .then((res) => { 37 | setSolution(res.data); 38 | }); 39 | }, [eventData]); 40 | 41 | // (eventData); 42 | const members = useEventPeople(eventData); 43 | const idToLatLng = idToLatLngFunc(members); 44 | // console.log(idToLatLng, members); 45 | // console.log('members: ' + members); 46 | const answer: any = []; 47 | for (const driver in solution) { 48 | const path = [...solution[driver]]; 49 | console.log(path); 50 | path.unshift(driver); 51 | const newPath = path.map((id) => idToLatLng[id]); 52 | answer.push(newPath); 53 | } 54 | console.log(answer, solution); 55 | /** 56 | * answer is 57 | * [ [{lat, lng}, {lat, lng} ...], [{lat, lng}, {lat, lng ...}]] 58 | * 59 | * encodedPaths should be 60 | * [ 61 | * ['String', 'String' ...], ['String', 'String' ...] 62 | * ] 63 | * 64 | */ 65 | /** 66 | * newPath SHOULD be [ 67 | * [{lat, lng}, {lat, lng} ... ], [{lat, lng}, {lat, lng} ... ] 68 | * ] 69 | */ 70 | 71 | const [newPath, setNewPath] = useState([]); 72 | 73 | const generatePaths = async () => { 74 | // console.log('generate paths'); 75 | 76 | const newPath2 = await Promise.all( 77 | answer.map( 78 | async (route: any): Promise => { 79 | const destination = eventData.destination.latlng.lat + ',' + eventData.destination.latlng.lng; 80 | const begin = route[0].lat + ',' + route[0].lng; 81 | let response; 82 | if (route.length >= 2) { 83 | let waypoint = ''; 84 | // , === %2C 85 | // | seperate locations 86 | for (let i = 1; i < route.length; i++) { 87 | if (i == route.length - 1) { 88 | waypoint = waypoint + route[i].lat + ',' + route[i].lng; 89 | } else { 90 | waypoint = waypoint + route[i].lat + ',' + route[i].lng + '|'; 91 | } 92 | } 93 | // const response = await axios.get( 94 | // `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.lat},${origin.lng}&destination=${destination.lat},${destination.lng}&${waypoint}&key=${key}`, 95 | // ); 96 | route.shift(); 97 | 98 | console.log(destination, begin, waypoint); 99 | 100 | response = await axios.post( 101 | 'https://us-central1-find-my-carpool.cloudfunctions.net/directions', 102 | { 103 | destination, 104 | origin: begin, 105 | waypoints: waypoint, 106 | }, 107 | { 108 | headers: { 109 | 'Access-Token': await firebase.auth().currentUser.getIdToken(), 110 | }, 111 | }, 112 | ); 113 | 114 | // console.log(response); 115 | } else { 116 | // const response = await axios.get( 117 | // `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.lat},${origin.lng}&destination=${destination.lat},${destination.lng}&key=${key}`, 118 | // ); 119 | // console.log(response); 120 | console.log(begin, destination); 121 | response = await axios.post( 122 | 'https://us-central1-find-my-carpool.cloudfunctions.net/directions', 123 | { 124 | destination, 125 | origin: begin, 126 | waypoint: '', 127 | }, 128 | { 129 | headers: { 130 | 'Access-Token': await firebase.auth().currentUser.getIdToken(), 131 | }, 132 | }, 133 | ); 134 | console.log(response); 135 | } 136 | 137 | // newPath.push('_h~jHpbtnVBgg@?eb@BiW@{G@uH@_E?_F}EB{NDuD?aF@sB?gCGeE?sQ?yID{DAmCBeAJYJ[RaAp@]HqEFuF?oBIsE?uCA?k@@{EiCAKEGQAY?}E') 138 | console.log(response.data.routes[0].overview_polyline.points); 139 | if (response.data.routes[0]) { 140 | return google.maps.geometry.encoding.decodePath( 141 | response.data.routes[0].overview_polyline.points, 142 | ); 143 | } 144 | }, 145 | ), 146 | ); 147 | setNewPath(newPath2); 148 | console.log(newPath2); 149 | }; 150 | 151 | console.log(newPath); 152 | 153 | // console.log("newpath:", newPath[0], newPath.length, newPath) 154 | 155 | // console.log(window.google); 156 | 157 | useEffect(() => { 158 | if (google.maps.geometry) { 159 | // generate an encoded response per route 160 | // for each route in answer 161 | // encode it 162 | generatePaths(); 163 | } 164 | }, [solution]); 165 | 166 | const colors: any = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FF00', '#FFFFFF', '#000000']; 167 | let i = 0; 168 | 169 | return ( 170 | 177 | 178 | {members.map((person: People) => { 179 | return ( 180 | 189 | ); 190 | })} 191 | {newPath?.map((path: any) => { 192 | const randomColor = colors[i]; 193 | i++; 194 | return ; 195 | })} 196 | 197 | ); 198 | }; 199 | 200 | export default GoogleApiWrapper((props) => ({ 201 | ...props, 202 | libraries: ['geometry'], 203 | apiKey: 'AIzaSyD2mfMm4KHPGErUYWr3bNFwALDVP-EKkXc', 204 | }))(MapContainer); 205 | -------------------------------------------------------------------------------- /src/components/Map/theme.ts: -------------------------------------------------------------------------------- 1 | const theme: any[] = [ 2 | { elementType: 'geometry', stylers: [{ color: '#242f3e' }] }, 3 | { elementType: 'labels.text.stroke', stylers: [{ color: '#242f3e' }] }, 4 | { elementType: 'labels.text.fill', stylers: [{ color: '#746855' }] }, 5 | { 6 | featureType: 'administrative.locality', 7 | elementType: 'labels.text.fill', 8 | stylers: [{ color: '#d59563' }], 9 | }, 10 | { 11 | featureType: 'poi', 12 | elementType: 'labels.text.fill', 13 | stylers: [{ color: '#d59563' }], 14 | }, 15 | { 16 | featureType: 'poi.park', 17 | elementType: 'geometry', 18 | stylers: [{ color: '#263c3f' }], 19 | }, 20 | { 21 | featureType: 'poi.park', 22 | elementType: 'labels.text.fill', 23 | stylers: [{ color: '#6b9a76' }], 24 | }, 25 | { 26 | featureType: 'road', 27 | elementType: 'geometry', 28 | stylers: [{ color: '#38414e' }], 29 | }, 30 | { 31 | featureType: 'road', 32 | elementType: 'geometry.stroke', 33 | stylers: [{ color: '#212a37' }], 34 | }, 35 | { 36 | featureType: 'road', 37 | elementType: 'labels.text.fill', 38 | stylers: [{ color: '#9ca5b3' }], 39 | }, 40 | { 41 | featureType: 'road.highway', 42 | elementType: 'geometry', 43 | stylers: [{ color: '#746855' }], 44 | }, 45 | { 46 | featureType: 'road.highway', 47 | elementType: 'geometry.stroke', 48 | stylers: [{ color: '#1f2835' }], 49 | }, 50 | { 51 | featureType: 'road.highway', 52 | elementType: 'labels.text.fill', 53 | stylers: [{ color: '#f3d19c' }], 54 | }, 55 | { 56 | featureType: 'transit', 57 | elementType: 'geometry', 58 | stylers: [{ color: '#2f3948' }], 59 | }, 60 | { 61 | featureType: 'transit.station', 62 | elementType: 'labels.text.fill', 63 | stylers: [{ color: '#d59563' }], 64 | }, 65 | { 66 | featureType: 'water', 67 | elementType: 'geometry', 68 | stylers: [{ color: '#17263c' }], 69 | }, 70 | { 71 | featureType: 'water', 72 | elementType: 'labels.text.fill', 73 | stylers: [{ color: '#515c6d' }], 74 | }, 75 | { 76 | featureType: 'water', 77 | elementType: 'labels.text.stroke', 78 | stylers: [{ color: '#17263c' }], 79 | }, 80 | ]; 81 | 82 | export default theme; 83 | -------------------------------------------------------------------------------- /src/components/Notification.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState } from 'react'; 2 | import Snackbar from '@material-ui/core/Snackbar'; 3 | import MuiAlert, { Color } from '@material-ui/lab/Alert'; 4 | 5 | //eslint-disable-next-line 6 | const globalAny: any = global 7 | 8 | const Notification = (): ReactElement => { 9 | const [open, setOpen] = useState(false); 10 | 11 | const [message, setMessage] = useState(''); 12 | 13 | const [type, setType] = useState('info'); 14 | 15 | globalAny.setNotification = (type: Color, message: string): void => { 16 | setOpen(true); 17 | setMessage(message); 18 | setType(type); 19 | }; 20 | 21 | return ( 22 | { 26 | setOpen(false); 27 | setMessage(''); 28 | setType('info'); 29 | }} 30 | anchorOrigin={{ 31 | vertical: 'bottom', 32 | horizontal: 'left', 33 | }} 34 | > 35 | 36 | {message} 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default Notification; 43 | -------------------------------------------------------------------------------- /src/components/PeopleList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'; 3 | import List from '@material-ui/core/List'; 4 | import ListItem from '@material-ui/core/ListItem'; 5 | import Divider from '@material-ui/core/Divider'; 6 | import ListItemText from '@material-ui/core/ListItemText'; 7 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'; 8 | import Avatar from '@material-ui/core/Avatar'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import Button from '@material-ui/core/Button'; 11 | import { People } from '../_types/people'; 12 | import { Link, useParams } from 'react-router-dom'; 13 | import LinkDisplay from '../components/LinkDisplay'; 14 | import { IconButton, Dialog, DialogTitle, DialogContent } from '@material-ui/core'; 15 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; 16 | import * as firebase from 'firebase'; 17 | import { useDocumentData } from 'react-firebase-hooks/firestore'; 18 | import { useFetch } from 'src/hooks/UseFetch'; 19 | 20 | const useStyles = makeStyles((theme: Theme) => 21 | createStyles({ 22 | root: { 23 | width: '100%', 24 | maxWidth: '36ch', 25 | backgroundColor: theme.palette.background.paper, 26 | }, 27 | inline: { 28 | display: 'inline', 29 | }, 30 | toolbar: { 31 | display: 'flex', 32 | alignItems: 'center', 33 | justifyContent: 'flex-start', 34 | padding: theme.spacing(0, 1), 35 | // necessary for content to be below app bar 36 | ...theme.mixins.toolbar, 37 | }, 38 | nested: { 39 | paddingLeft: theme.spacing(4), 40 | }, 41 | }), 42 | ); 43 | 44 | const Person = ({ person }: { person: People }) => { 45 | const classes = useStyles(); 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | 56 | 57 | {person.location.address + ' - '} 58 | 59 | {person.seats === 0 ? 'Passenger' : 'Seats: ' + String(person.seats)} 60 | 61 | } 62 | /> 63 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | const Passenger = ({ personId }: { personId: string }) => { 70 | const classes = useStyles(); 71 | const [person] = useDocumentData(firebase.firestore().collection('people').doc(personId), { 72 | snapshotListenOptions: { 73 | includeMetadataChanges: true, 74 | }, 75 | }); 76 | return ( 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | }; 88 | 89 | const Driver = ({ personId, passengerIds }: { personId: string; passengerIds: string[] }) => { 90 | const [person] = useDocumentData(firebase.firestore().collection('people').doc(personId), { 91 | snapshotListenOptions: { 92 | includeMetadataChanges: true, 93 | }, 94 | }); 95 | return ( 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | {passengerIds?.map((passengerId) => ( 106 | 107 | ))} 108 | 109 | 110 | ); 111 | }; 112 | 113 | export default function ListView(props: any) { 114 | const { eventId } = useParams(); 115 | const classes = useStyles(); 116 | const y = window.innerHeight * 0.6; 117 | const members: People[] = props.members; 118 | const [showResult, setShowResult] = useState(false); 119 | 120 | const result = useFetch(`https://us-central1-find-my-carpool.cloudfunctions.net/solve?eventId=${eventId}`); 121 | 122 | return ( 123 |
124 | 125 |
126 | { 128 | props?.setSidebarOpen(false); 129 | }} 130 | > 131 | 132 | 133 |
134 | {members.map((person: People) => ( 135 | 136 | ))} 137 | {members.length === 0 &&

No members yet

} 138 |
139 |
140 |
141 | 150 |
151 |
152 | 156 |
157 |
158 | 159 | setShowResult(false)} aria-labelledby="customized-dialog-title" open={showResult}> 160 | Assignments 161 | 162 | 163 | {Boolean(!result.loading && result.data) && 164 | Object.keys(result?.data).map((driverId) => ( 165 | 166 | ))} 167 | 168 | 169 | 170 |
171 | ); 172 | } 173 | -------------------------------------------------------------------------------- /src/hooks/UseAutocompletePlaces.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | import Autocomplete from '@material-ui/lab/Autocomplete'; 4 | import LocationOnIcon from '@material-ui/icons/LocationOn'; 5 | import Grid from '@material-ui/core/Grid'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import { makeStyles } from '@material-ui/core/styles'; 8 | import parse from 'autosuggest-highlight/parse'; 9 | import throttle from 'lodash/throttle'; 10 | import { geocodeByAddress, getLatLng } from 'react-places-autocomplete'; 11 | 12 | const autocompleteService: any = { current: null }; 13 | 14 | const useStyles = makeStyles((theme) => ({ 15 | icon: { 16 | color: theme.palette.text.secondary, 17 | marginRight: theme.spacing(2), 18 | }, 19 | })); 20 | 21 | interface PlaceType { 22 | description: string; 23 | structured_formatting: { 24 | main_text: string; 25 | secondary_text: string; 26 | main_text_matched_substrings: [ 27 | { 28 | offset: number; 29 | length: number; 30 | }, 31 | ]; 32 | }; 33 | } 34 | 35 | export default function useAutocompletePlaces(locationType: string) { 36 | const classes = useStyles(); 37 | const [value, setValue] = React.useState(null); 38 | const [inputValue, setInputValue] = React.useState(''); 39 | const [latLng, setLatLng] = React.useState({}); 40 | const [options, setOptions] = React.useState([]); 41 | const loaded = React.useRef(false); 42 | 43 | if (typeof window !== 'undefined' && !loaded.current) { 44 | loaded.current = true; 45 | } 46 | 47 | const fetch = React.useMemo( 48 | () => 49 | throttle((request: { input: string }, callback: (results?: PlaceType[]) => void) => { 50 | (autocompleteService.current as any).getPlacePredictions(request, callback); 51 | }, 200), 52 | [], 53 | ); 54 | 55 | React.useEffect(() => { 56 | let active = true; 57 | 58 | if (!autocompleteService.current && (window as any).google) { 59 | autocompleteService.current = new (window as any).google.maps.places.AutocompleteService(); 60 | } 61 | if (!autocompleteService.current) { 62 | return undefined; 63 | } 64 | 65 | if (inputValue === '') { 66 | setOptions(value ? [value] : []); 67 | return undefined; 68 | } 69 | 70 | fetch({ input: inputValue }, (results?: PlaceType[]) => { 71 | if (active) { 72 | let newOptions = [] as PlaceType[]; 73 | 74 | if (value) { 75 | newOptions = [value]; 76 | } 77 | 78 | if (results) { 79 | newOptions = [...newOptions, ...results]; 80 | } 81 | 82 | setOptions(newOptions); 83 | } 84 | }); 85 | 86 | return () => { 87 | active = false; 88 | }; 89 | }, [value, inputValue, fetch]); 90 | 91 | const render = ( 92 | (typeof option === 'string' ? option : option.description)} 95 | filterOptions={(x) => x} 96 | options={options} 97 | autoComplete 98 | includeInputInList 99 | filterSelectedOptions 100 | value={value} 101 | onChange={async (event: any, newValue: any) => { 102 | setOptions(newValue ? [newValue, ...options] : options); 103 | setValue(newValue); 104 | const address = typeof newValue === 'string' ? newValue : (newValue as PlaceType)?.description; 105 | 106 | const results = await geocodeByAddress(address); 107 | const latlng = await getLatLng(results[0]); 108 | setLatLng(latlng); 109 | }} 110 | onInputChange={(event, newInputValue) => { 111 | setInputValue(newInputValue); 112 | }} 113 | renderInput={(params) => ( 114 | 115 | )} 116 | renderOption={(option) => { 117 | const matches = option.structured_formatting.main_text_matched_substrings; 118 | const parts = parse( 119 | option.structured_formatting.main_text, 120 | matches.map((match: any) => [match.offset, match.offset + match.length]), 121 | ); 122 | 123 | return ( 124 | 125 | 126 | 127 | 128 | 129 | {parts.map((part: any, index: any) => ( 130 | 131 | {part.text} 132 | 133 | ))} 134 | 135 | {option.structured_formatting.secondary_text} 136 | 137 | 138 | 139 | ); 140 | }} 141 | /> 142 | ); 143 | 144 | const address = typeof inputValue === 'string' ? inputValue : (inputValue as PlaceType).description; 145 | 146 | return [address, latLng, render, setLatLng, setInputValue, setValue]; 147 | } 148 | -------------------------------------------------------------------------------- /src/hooks/UseFetch.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | 4 | export const useFetch = (url: string): { loading: boolean; data: Record } => { 5 | const [data, setData] = useState>(null); 6 | const [loading, setLoading] = useState(true); 7 | 8 | async function fetchData() { 9 | const response = await axios.get(url); 10 | const json = await response.data; 11 | console.log(json); 12 | setData(json); 13 | setLoading(false); 14 | } 15 | 16 | useEffect(() => { 17 | fetchData(); 18 | }, [url]); 19 | 20 | return { loading, data }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/hooks/useEventPeople.tsx: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase'; 2 | import { useState, useEffect } from 'react'; 3 | import { Event } from 'src/_types/event'; 4 | import { People } from 'src/_types/people'; 5 | 6 | export default function useEventPeople(event: Event): People[] { 7 | const [people, setPeople] = useState([]); 8 | 9 | useEffect(() => { 10 | if (event) { 11 | Promise.all( 12 | ((event.people as unknown) as firebase.firestore.DocumentReference[]).map( 13 | async (person: firebase.firestore.DocumentReference) => { 14 | const personRef = await person.get(); 15 | return { 16 | id: personRef.id, 17 | ...personRef.data(), 18 | }; 19 | }, 20 | ), 21 | ).then((result) => { 22 | setPeople(result); 23 | }); 24 | } 25 | }, [event]); 26 | 27 | return people; 28 | } 29 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './styles/index.scss'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import * as firebase from 'firebase'; 7 | import 'firebase/firestore'; 8 | 9 | const firebaseConfig = { 10 | apiKey: 'AIzaSyDvCT-243TWt9Dwb9ChTOgfkFMUhIjTlRc', 11 | authDomain: 'find-my-carpool.firebaseapp.com', 12 | databaseURL: 'https://find-my-carpool.firebaseio.com', 13 | projectId: 'find-my-carpool', 14 | storageBucket: 'find-my-carpool.appspot.com', 15 | messagingSenderId: '470237283855', 16 | appId: '1:470237283855:web:d3aa289ca316787e4a457a', 17 | measurementId: 'G-YSVSBWXT23', 18 | }; 19 | firebase.initializeApp(firebaseConfig); 20 | 21 | ReactDOM.render( 22 | 23 | 24 | , 25 | document.getElementById('root'), 26 | ); 27 | 28 | // If you want your app to work offline and load faster, you can change 29 | // unregister() to register() below. Note this comes with some pitfalls. 30 | // Learn more about service workers: https://bit.ly/CRA-PWA 31 | serviceWorker.unregister(); 32 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from '../styles/App.module.scss'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const index = () => { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Page not found. Sorry :( 12 |
13 | Click here to go back home. 14 |

15 |
16 |
17 | ); 18 | }; 19 | 20 | export default index; 21 | -------------------------------------------------------------------------------- /src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState, useEffect } from 'react'; 2 | // import Event from '../_types/event'; 3 | import styles from '../styles/App.module.scss'; 4 | import EventForm from '../components/EventForm'; 5 | import Header from '../components/Header'; 6 | import Footer from '../components/Footer'; 7 | import EventList from '../components/EventList'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import * as firebase from 'firebase'; 10 | import { useCollectionData } from 'react-firebase-hooks/firestore'; 11 | import { People } from 'src/_types/people'; 12 | import { Event } from 'src/_types/event'; 13 | 14 | const Home = (): ReactElement => { 15 | const [events, setEvents] = useState(null); 16 | 17 | const [eventSnapshot, loading] = useCollectionData( 18 | firebase 19 | .firestore() 20 | .collection('people') 21 | .where('userId', '==', firebase.auth().currentUser.uid) 22 | .where('isHost', '==', true), 23 | { 24 | idField: 'id', 25 | snapshotListenOptions: { 26 | includeMetadataChanges: true, 27 | }, 28 | }, 29 | ); 30 | 31 | const fetchEvents = async () => { 32 | if (eventSnapshot) { 33 | const events: Event[] = []; 34 | await Promise.all( 35 | eventSnapshot?.map(async (doc) => { 36 | const eventRef = doc.event as firebase.firestore.DocumentReference; 37 | const event = await eventRef?.get(); 38 | if (event) events.push({ id: event.id, ...(event.data() as Event) }); 39 | }), 40 | ); 41 | setEvents(events); 42 | } 43 | }; 44 | 45 | useEffect(() => { 46 | fetchEvents(); 47 | }, [eventSnapshot]); 48 | 49 | return ( 50 |
51 |
52 | 53 | {events == null || loading ? ( 54 | Loading... 55 | ) : ( 56 | 57 | )} 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default Home; 64 | -------------------------------------------------------------------------------- /src/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import * as firebase from 'firebase'; 3 | import { Event } from '../_types/event'; 4 | import { useParams, useHistory } from 'react-router-dom'; 5 | import { useDocumentData } from 'react-firebase-hooks/firestore'; 6 | import Map from '../components/Map'; 7 | import ListView from '../components/PeopleList'; 8 | import useEventPeople from 'src/hooks/useEventPeople'; 9 | import Header from '../components/Header'; 10 | import { Hidden, Drawer } from '@material-ui/core'; 11 | 12 | //eslint-disable-next-line 13 | const globalAny: any = global 14 | 15 | const Dashboard = () => { 16 | const { eventId } = useParams(); 17 | const history = useHistory(); 18 | const [sidebarOpen, setSidebarOpen] = useState(true); 19 | 20 | // const [eventCoord, setEventCoord] = useState({ 21 | // lat: 0.0, 22 | // lng: 0.0, 23 | // }); 24 | 25 | const [event, loading] = useDocumentData(firebase.firestore().collection('events').doc(eventId), { 26 | snapshotListenOptions: { 27 | includeMetadataChanges: true, 28 | }, 29 | }); 30 | 31 | const people = useEventPeople(event); 32 | 33 | if (!loading && !event) { 34 | globalAny.setNotification('error', 'Event not found.'); 35 | history.push('/'); 36 | } 37 | 38 | // useEffect(() => { 39 | // const showPosition = (position: any) => { 40 | // const lat = position.coords.latitude; 41 | // const lng = position.coords.longitude; 42 | // console.log(lat, lng); 43 | // setCoord({ 44 | // lat: lat, 45 | // lng: lng, 46 | // }); 47 | // }; 48 | 49 | // Get the location of event 50 | 51 | // useEffect(() => { 52 | // for (let i = 0; i < people.length; i++) { 53 | // if (people[i].isHost) { 54 | // const host = people[i]; 55 | // people.splice(i, 1); 56 | // setEventCoord(host.location.latlng); 57 | // } 58 | // } 59 | // }, []); 60 | 61 | return ( 62 | <> 63 |
64 |
65 | { 70 | setSidebarOpen(false); 71 | }} 72 | > 73 | 74 | 75 | {/* */} 76 | 77 |
78 | 79 | ); 80 | }; 81 | 82 | export default Dashboard; 83 | -------------------------------------------------------------------------------- /src/pages/form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from '../styles/App.module.scss'; 3 | import GuestForm from '../components/GuestForm'; 4 | import { Grid, Hidden } from '@material-ui/core'; 5 | 6 | const Form = () => { 7 | return ( 8 |
9 | 10 | 11 |
12 |
13 | logo 14 |

15 | Create carpools, without the headache. 16 |

17 |
18 | 19 |
20 |
21 | 22 | 35 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Form; 42 | -------------------------------------------------------------------------------- /src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import styles from '../styles/App.module.scss'; 3 | import LoginForm from '../components/LoginForm'; 4 | import { Grid, Hidden } from '@material-ui/core'; 5 | import DemoCard from '../components/DemoCard'; 6 | 7 | const index = (): ReactElement => { 8 | return ( 9 |
10 | 11 | 12 |
13 |
14 | logo 15 |

16 | Create carpools, without the headache. 17 |

18 |
19 |
20 | 21 | 22 | 23 |
24 | 25 | 36 | 37 | 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default index; 44 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/), 19 | ); 20 | 21 | type Config = { 22 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 23 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 24 | }; 25 | 26 | export function register(config?: Config) { 27 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 28 | // The URL constructor is available in all browsers that support SW. 29 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 30 | if (publicUrl.origin !== window.location.origin) { 31 | // Our service worker won't work if PUBLIC_URL is on a different origin 32 | // from what our page is served on. This might happen if a CDN is used to 33 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 34 | return; 35 | } 36 | 37 | window.addEventListener('load', () => { 38 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 39 | 40 | if (isLocalhost) { 41 | // This is running on localhost. Let's check if a service worker still exists or not. 42 | checkValidServiceWorker(swUrl, config); 43 | 44 | // Add some additional logging to localhost, pointing developers to the 45 | // service worker/PWA documentation. 46 | navigator.serviceWorker.ready.then(() => { 47 | console.log( 48 | 'This web app is being served cache-first by a service ' + 49 | 'worker. To learn more, visit https://bit.ly/CRA-PWA', 50 | ); 51 | }); 52 | } else { 53 | // Is not localhost. Just register service worker 54 | registerValidSW(swUrl, config); 55 | } 56 | }); 57 | } 58 | } 59 | 60 | function registerValidSW(swUrl: string, config?: Config) { 61 | navigator.serviceWorker 62 | .register(swUrl) 63 | .then((registration) => { 64 | registration.onupdatefound = () => { 65 | const installingWorker = registration.installing; 66 | if (installingWorker == null) { 67 | return; 68 | } 69 | installingWorker.onstatechange = () => { 70 | if (installingWorker.state === 'installed') { 71 | if (navigator.serviceWorker.controller) { 72 | // At this point, the updated precached content has been fetched, 73 | // but the previous service worker will still serve the older 74 | // content until all client tabs are closed. 75 | console.log( 76 | 'New content is available and will be used when all ' + 77 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.', 78 | ); 79 | 80 | // Execute callback 81 | if (config && config.onUpdate) { 82 | config.onUpdate(registration); 83 | } 84 | } else { 85 | // At this point, everything has been precached. 86 | // It's the perfect time to display a 87 | // "Content is cached for offline use." message. 88 | console.log('Content is cached for offline use.'); 89 | 90 | // Execute callback 91 | if (config && config.onSuccess) { 92 | config.onSuccess(registration); 93 | } 94 | } 95 | } 96 | }; 97 | }; 98 | }) 99 | .catch((error) => { 100 | console.error('Error during service worker registration:', error); 101 | }); 102 | } 103 | 104 | function checkValidServiceWorker(swUrl: string, config?: Config) { 105 | // Check if the service worker can be found. If it can't reload the page. 106 | fetch(swUrl, { 107 | headers: { 'Service-Worker': 'script' }, 108 | }) 109 | .then((response) => { 110 | // Ensure service worker exists, and that we really are getting a JS file. 111 | const contentType = response.headers.get('content-type'); 112 | if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then((registration) => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log('No internet connection found. App is running in offline mode.'); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready 132 | .then((registration) => { 133 | registration.unregister(); 134 | }) 135 | .catch((error) => { 136 | console.error(error.message); 137 | }); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/styles/App.module.scss: -------------------------------------------------------------------------------- 1 | @import './constants.scss'; 2 | 3 | .app { 4 | text-align: center; 5 | } 6 | 7 | .logo { 8 | pointer-events: none; 9 | } 10 | 11 | .header { 12 | // display: flex; 13 | // flex-direction: column; 14 | // align-items: center; 15 | // justify-content: center; 16 | font-size: calc(10px + 2vmin); 17 | color: $dark-gray; 18 | } 19 | 20 | .link { 21 | color: $blue; 22 | } -------------------------------------------------------------------------------- /src/styles/constants.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | // white, black 3 | $white: #FFFFFF; 4 | $off-white: #DEDEDE; 5 | $dark-gray: #202020; 6 | $black: #000000; 7 | 8 | $blue: #276EF1; 9 | $green: #05A357; 10 | $yellow: #FFC043; 11 | $red: #E11900; 12 | $brown: #99644C; 13 | $orange: #FF6937; 14 | $purple: #7356BF; 15 | 16 | // Fonts 17 | $primary-font: 'Poppins', sans-serif; 18 | $secondary-font: 'PT Serif', serif; 19 | 20 | // Add SCSS variables to body 21 | $prefix: "--"; 22 | 23 | @function custom-property-name($name) { 24 | @return $prefix + $name; 25 | } 26 | 27 | @mixin define-custom-property($name, $value) { 28 | #{custom-property-name($name)}: $value; 29 | } 30 | 31 | body { 32 | @include define-custom-property('white', $white); 33 | @include define-custom-property('off-white', $off-white); 34 | @include define-custom-property('dark-gray', $dark-gray); 35 | @include define-custom-property('black', $black); 36 | @include define-custom-property('blue', $blue); 37 | @include define-custom-property('green', $green); 38 | @include define-custom-property('yellow', $yellow); 39 | @include define-custom-property('red', $red); 40 | @include define-custom-property('brown', $brown); 41 | @include define-custom-property('orange', $orange); 42 | @include define-custom-property('purple', $purple); 43 | @include define-custom-property('primary-font', $primary-font); 44 | @include define-custom-property('secondary-font', $secondary-font); 45 | 46 | } -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './constants.scss'; 2 | 3 | body { 4 | margin: 0; 5 | font-family: $primary-font; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | footer { 16 | margin-top:calc(5% + 60px); 17 | bottom: 0; 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core'; 2 | import { readProperty } from '../utils/sassHelper'; 3 | 4 | export default createMuiTheme({ 5 | palette: { 6 | primary: { main: readProperty('dark-gray') }, 7 | secondary: { main: readProperty('orange') }, 8 | error: { main: readProperty('red') }, 9 | warning: { main: readProperty('yellow') }, 10 | info: { main: readProperty('blue') }, 11 | success: { main: readProperty('green') }, 12 | }, 13 | typography: { 14 | fontFamily: [readProperty('primary-font')].join(','), 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/sassHelper.tsx: -------------------------------------------------------------------------------- 1 | const PREFIX = '--'; 2 | 3 | export function readProperty(name: string): string { 4 | const bodyStyles = window.getComputedStyle(document.body); 5 | return bodyStyles.getPropertyValue(PREFIX + name).trim(); 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react", 20 | "baseUrl": ".", 21 | "skipLibCheck": true, 22 | "strictNullChecks": false 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------