From f6642a94db2f779429d167fdeef9574aa05c3cb7 Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Sat, 24 Jul 2021 18:00:29 -0400 Subject: [PATCH] map based mvp --- .env | 1 + package.json | 2 + public/markers/blue.png | Bin 0 -> 1309 bytes public/markers/green.png | Bin 0 -> 1190 bytes public/markers/lightblue.png | Bin 0 -> 1269 bytes public/markers/orange.png | Bin 0 -> 3413 bytes public/markers/pink.png | Bin 0 -> 1321 bytes public/markers/purple.png | Bin 0 -> 1324 bytes public/markers/red.png | Bin 0 -> 1305 bytes public/markers/yellow.png | Bin 0 -> 1309 bytes src/components/UI/UIDatetimeInput.tsx | 6 +- src/components/UI/UIPlacesAutocomplete.tsx | 2 +- src/components/UI/UITextInput.tsx | 9 +- src/components/useOptimalPathWithDriver.ts | 31 ++ src/index.css | 19 +- src/index.tsx | 11 +- src/lib/estimateoptimalpath.ts | 26 +- src/mvp/App.tsx | 33 ++ src/mvp/EventAuthenticator.tsx | 75 ++++ src/mvp/EventPage.tsx | 387 +++++++++++++++++++++ src/mvp/PlanEvent.tsx | 70 ++++ src/mvp/SignIn.tsx | 46 +++ src/mvp/SignUp.tsx | 53 +++ src/mvp/WheelShareContext.ts | 34 ++ src/mvp/WheelShareProvider.tsx | 202 +++++++++++ src/mvp/routeOptimization.ts | 86 +++++ src/mvp/types.ts | 37 ++ yarn.lock | 12 + 28 files changed, 1116 insertions(+), 26 deletions(-) create mode 100644 public/markers/blue.png create mode 100644 public/markers/green.png create mode 100644 public/markers/lightblue.png create mode 100644 public/markers/orange.png create mode 100644 public/markers/pink.png create mode 100644 public/markers/purple.png create mode 100644 public/markers/red.png create mode 100644 public/markers/yellow.png create mode 100644 src/components/useOptimalPathWithDriver.ts create mode 100644 src/mvp/App.tsx create mode 100644 src/mvp/EventAuthenticator.tsx create mode 100644 src/mvp/EventPage.tsx create mode 100644 src/mvp/PlanEvent.tsx create mode 100644 src/mvp/SignIn.tsx create mode 100644 src/mvp/SignUp.tsx create mode 100644 src/mvp/WheelShareContext.ts create mode 100644 src/mvp/WheelShareProvider.tsx create mode 100644 src/mvp/routeOptimization.ts create mode 100644 src/mvp/types.ts diff --git a/.env b/.env index 9643b21..c3f11e3 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ REACT_APP_API_LOCAL=http://localhost:5000/ +REACT_APP_API_LOCAL0=https://api.wheelshare.app/ REACT_APP_API_PROD=https://api.wheelshare.app/ diff --git a/package.json b/package.json index 1dfa54a..7de87da 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "@types/google-maps-react": "^2.0.5", "@types/immutable": "^3.8.7", "@types/node": "^14.14.37", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.9", "@types/react-router-dom": "^5.1.7", "dotenv": "^8.2.0", + "google-maps-react": "^2.0.6", "immutable": "^4.0.0-rc.14", "react": "^17.0.2", "react-bootstrap": "^1.5.2", diff --git a/public/markers/blue.png b/public/markers/blue.png new file mode 100644 index 0000000000000000000000000000000000000000..69590b92ab25dbd8fc82d7d6b0eb5b7f6a1ec1ce GIT binary patch literal 1309 zcmV+&1>*XNP)Z z5tfPI80dhNjXizOZ4M^WIE{~~m@T?2q8U@v=+tB;lFVdoCfj^0CPoPxa6GrfGHmPM zPMduB)As-G{r+>$J@;M$0K^@Zu3t!T_jO|IxJ0ryoF-|-FAsEUt5^S!`sb3c&QIwZ&OqhB(xN~U z(9rS)a5&uAB*VT8CIMZ;S&V}E6$C1Gx~YDiz-ZB73yLw|+bc$NxGpdWn6ZNC8$XHU zc?qsdwf039BYGc}Z!E|B-?eog#+o~?v91Nbk}|C2s^S-Qn8}mL1yA30M$rG2mLoLt zZ>AUz7MAwiaJ0iL;E=#Zl%~OAY!uenPI#OZbeQywV>s>{8iAV9zPW{neKFd6#Ysu; zB#B9D?Gp&aVClMw%kM%7E7rQQzCXb)FcQLLRr{&5x$9?Q-tvDc<`$Q6l|Ixsir{<9g4h` zbIt|I+7B3&YrtRT0|}bGTH0YQ-hV4h0-D-U7xYb^;u4*BGBja+jiA@WoSh)>37pW? zpB3mE#~oX^TVbvP|0K8hOOl-bl%B^IK+)#QbGcxj2H(JNh)QW{r;*atkGa4kkXbR} zO69wFeaCB=!0$q;=`h&#VdgiDYF}ud3)vvtSx)pEm>73$;t@Lv$WD} zcoOJ_1AGI*#xymfxE+7bEldJR^9ycK-FJ+vR&C1j_FaKFa1%EjDp(b3>Q-00P0~w_ zxrIppGu|yq^AVDi|AfR#ApApH_YwH`t%SI&?u_)3mkB+TxPwU`LA%WZb|TgF6K|pT zyV%T5xJxMdO;PZSD2+zQG~^aG0Zp$*WcnS%Ulj3yI5-u@KjeIB(eot5cnCS@7AAqH zltzzWStkDC=+~ABgt+O9J}uR4C-K=m9{FDc@XT<4Rz<(S@Z-4kB6uwG2$MjH@qkBY zVg<(*^-uvxlzu-R$2`s&3X_1RDMeA2zeS=`AFxpYJOQ<4R2(NNoCTivH-WzZfyOD! T^E$RX00000NkvXXu0mjfg)VJP literal 0 HcmV?d00001 diff --git a/public/markers/green.png b/public/markers/green.png new file mode 100644 index 0000000000000000000000000000000000000000..0f79315e427e3401061bd85d197b033ca24e5212 GIT binary patch literal 1190 zcmV;X1X=ruP)JW3b9v#cu@&% zgPs-gCf_VvQJ}>7Vh}Ht1H4zC=+AbXd{}-iNx+^c;KERq%DHSnxzrh1;U#&ipYYy1 z!rS~LW0Od^QvT-eL-=4YF8V61e*cev!!rRFOuLImJ0h>kCNWq|@J$oJ$rgg2TM{il zi=i6An|-7J&*1#L`_J{Ckbtim0K3ASY?OFv6V85UCOFYiqLOV>UwgKe4rxXF{Ciwh2Con+Xv6szH`rJ|M53 zVvX!S9be3;R>F=PQen>lu{WlS1mI)&O-f1<`)dfkZyuk3@0v**tRs~o2_FhxZ6rXI zdmoo&UN+V=_SD<)V>8K`3{nN@Pnrn;+$K4yt-u?va}D>y+XFf)dJP4@cAOjF^`C%% z`=*k%V<Ouq$au!f48cz<;KVpa&&wufXOfC~`^BqK!%D~% z4EkeXl`2o-k`Nb{ON<3wgjR-0Df~_Bt}HdKgm8%Wf};%}HWpvWW%V?!E*!fe@^|Dt zga7{v*A(5T`)fPq0{#`z9cgEyBz9IfX9X{lcs)jZJ|M9&+-W%I-?R7O*?gW-Z0ws0~UVPKIZaaw{$39`SZvQeQIx zyDO7iZ|3aLlGynxhX*B6|+siHq>9pOe}CSXspA+fIt@QK2!RH`EQFrJ77 z&U+bj6W$qEWQ~~f{$k7oyik_RcB@TM>8>wvZG_jG-4T1%6$7pc`<2J_hvK1jJf<%mge9IrB(9@bzrJtl}F{p8F6!TLkzFv&J4X0doV+bmjsdR(A0+ zkSs_a#HIPb!RUyYfI9-t+|2??y_@xoNlx?hVS5hX9G@d*Y4~)JGaHJ5BfpCeoFrSa zdc^Y_fSe&tmLg;7DQyaFjNVG5Pun-^DvUW%qZo}b;3OSJq5(xi6qP>+Y{n-nqlFFI{3w+uK>(-n}fmrbZH&n;Yli=T~QEXV=CU zYp2%5*SEnhBV$SA=FQUB-Md-xkt4!58c*|xtwCa+1-N~iWnaF`mYqDw!p!CfTdQ7!*)a;{~>bAmIN*U9s0kAM$_r3(LmADr%*UDGVR^dv3VyY(e8V6ot=C}WI!iO>FNrS zI5>=}tE;DNM$`}O8_+}k&>cUnGVnTK4puVqaxB9sm`HD<8ydcNBj|go8QA}CO=Dwr z{=Iv`Jlp`J0-Pjf=gvHw6q4H7py=5%QR7*5>J*(jL}CH^Scv$G(Y0$6%D;P;EnKzg zM@NMMa&F!f%J1aGh5Q&A0(_`IfWgp0(AbX|Zrl*W1(0>|;)LjNfe#Z2UdusT0E(@m zK%oy?w+u8gVf21Aj&iP z`-Mt6SzG{1j~)9&DW^|Uk?NgXBWU*#n>K;tym=$K6DJ(;<_J%?$>R0_Yo(q!!=mcz zQ}Jp-_SLIk932&2N6x;!5M(md&~EA8TZ;=I;qc*4)!Is}%wf))(U@JkpybUPz^H&k zB6*Ad_ooP1@18pXOAa2isYHu~dBw*!`y?mB>hW<{Y&L_lQh9}r2(PJ4aRJc#-j?F` z?`I23OP4#jxe2!G9vuzJl$1K{p+mxVw!{U1A+{-kh^njemdWLRe%H|fat{wsRaPd@ zLa}@H*c2Cldi{EFRadjG6pG={iVDE?ceH6Y0uzJg0>EEFq6*)*k)iAT;vNwJ&OScB z#q8L@G~2g}iVGmvWD*tHGMgI^(B&YP1GfEY1j1w#6&FB|(I~2~OG+5Nn13QybWtk1 z0}Td-<|Hb<7ZHX|OtDQ(OqrVc0|9tzwJrE_O!U@}xB$ei6iM6C!h(y6Rucd|Jk9hu fe&0S(eGcGXd0TozQ6I=600000NkvXXu0mjfKWb4+ literal 0 HcmV?d00001 diff --git a/public/markers/orange.png b/public/markers/orange.png new file mode 100644 index 0000000000000000000000000000000000000000..8bee9da397e37a094f7c7f4d1169d1282b0e41e1 GIT binary patch literal 3413 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 literal 0 HcmV?d00001 diff --git a/public/markers/purple.png b/public/markers/purple.png new file mode 100644 index 0000000000000000000000000000000000000000..b57873893261255efa18f98e7c47dcc0473d9bb7 GIT binary patch literal 1324 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 P00000NkvXXu0mjfV_aiO literal 0 HcmV?d00001 diff --git a/public/markers/yellow.png b/public/markers/yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..a9d65acca6b10c03c259ec547ba1f6bc85437e33 GIT binary patch literal 1309 zcmV+&1>*XNP)Pe%s z1_r!wnTrsT6YXJZx+A1)b^v#oIpFaLJm0u2;~DSf`XEmHkq8Xlj3V!5IBmu!9f=HI z30E$Kf@WBamn2M~`70KF=vWWxXd4`njK7)Y`0DL{?q8I^$dHO0%#|hMpio-qJsEI3 z4m9`FFn(7x#l$@oz*rofPlS`@et@qB_T)&@MuuY81%CZGf}E~i?Sa8Zj~Dykp(HNQ z%)tMJ6T$1MD&%w~G^Z-%ih;feHi3@T5aJ>coQ;ZD$vxS8!Dm9R$7F?7VzZWSThaKK&XJxFt&K1gk*VmSoYQ1h&5ZG##jnRiH33!g^5x z32}CGpduy#h5xb)vDgeIu;?r}2k3GfjRUDf0B$njb|!)R58U0bpo}y(1OrUOFYM_t z0l#k#r3FcIaKB8>ByizeFp>LPRx120IRVXm^}|0sPJgJ<3VVdGmi7fEfq`q0q_s)u zz~}P@cV|l`i_t?Cp+mVc;PLeDHhmT7bgM6tP2lz~*bDBcNO`eOFecJ)-XZsS6Ox|) z7<&d@KX&x6=Y~H;o{y+^6hGM|M-W1~1w)s^0_x?z7>-r>20@u1XOhGpn z{k`ElN3mHO2ADGo^mop7XAnHGXJVqKdj$R7ztQCj|Ic+cr z6c>1Dg-X2to|_eB{r&S_2f^}Xk3r_!PVx#2REB>_w812>D@&pk>wTcsWI+6D-R_ zEHeAd*vJq>4yN_^Cxl=!>V!$)rDO-4q@{|9)LBqKtD>!q$rrfwBC2Q`oiGU$ void; disabled?: boolean; + style?: CSSProperties; }) { const onChange = useCallback( (e) => { @@ -28,7 +30,7 @@ export default function UIDatetimeInput({ ); return ( void; + password?: boolean; + style?: CSSProperties; }) { const onChange = useCallback( (e) => onChangeText(e.target.value), @@ -24,10 +29,12 @@ export default function UITextInput({ ); return ( ); } diff --git a/src/components/useOptimalPathWithDriver.ts b/src/components/useOptimalPathWithDriver.ts new file mode 100644 index 0000000..cf6be5c --- /dev/null +++ b/src/components/useOptimalPathWithDriver.ts @@ -0,0 +1,31 @@ +import { useDebugValue, useMemo } from 'react'; +import estimateOptimalPath, { Location } from '../lib/estimateoptimalpath'; + +export default function useOptimalPathWithDriver< + M extends Location, + D extends Location +>(driver: M, members: M[], destination: D) { + const path = useMemo(() => { + if (members.length === 0) { + return null; + } + + // O(n) + const passengerLocations = members.filter( + (location) => location !== driver + ); + + // O(n) + const path = estimateOptimalPath({ + from: driver, + waypoints: passengerLocations, + to: destination, + }); + + return path; + }, [destination, driver, members]); + + useDebugValue(path); + + return path; +} diff --git a/src/index.css b/src/index.css index 1229018..1bd9c8f 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,14 @@ body { - margin: 0; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + margin: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', + 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + padding: 1rem; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} \ No newline at end of file + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/src/index.tsx b/src/index.tsx index 85f18a6..8c222fc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,16 +1,19 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; -import App from './components/App'; +import App from './mvp/App'; +import WheelShareProvider from './mvp/WheelShareProvider'; import reportWebVitals from './reportWebVitals'; -import AuthenticationWrapper from './components/Authentication/AuthenticationWrapper'; import * as serviceWorkerRegistration from './serviceWorkerRegistration'; ReactDOM.render( - + {/* - + */} + + + , document.getElementById('root') ); diff --git a/src/lib/estimateoptimalpath.ts b/src/lib/estimateoptimalpath.ts index 50b8f5c..d339052 100644 --- a/src/lib/estimateoptimalpath.ts +++ b/src/lib/estimateoptimalpath.ts @@ -23,26 +23,34 @@ export default function estimateOptimalPath< const { from, to, waypoints } = path; let sequence = [from, to]; + console.log('Sequence:', sequence, '; distance:', getDistance(...sequence)); + // Calculates all possible paths from the start to the end of the given path // and returns the one with the minimum distance for (let waypoint of waypoints) { // Iterate over all possible insertion points for the waypoint let minDistance = Infinity; - let insertionPoint = 0; + let minI = 0; for (let i = 0; i < sequence.length - 1; i++) { - const [start, end] = sequence.slice(i, i + 2); - - const distance = getDistance(start, waypoint, end); + const temporarySequence = [ + ...sequence.slice(0, i + 1), + waypoint, + ...sequence.slice(i + 1), + ]; + const distance = getDistance(...temporarySequence); if (distance < minDistance) { minDistance = distance; - insertionPoint = i; + minI = i; } } - sequence = sequence - .slice(0, insertionPoint + 1) - .concat([waypoint]) - .concat(sequence.slice(insertionPoint + 1)); + sequence = [ + ...sequence.slice(0, minI + 1), + waypoint, + ...sequence.slice(minI + 1), + ]; + + console.log('Sequence:', sequence, '; distance:', getDistance(...sequence)); } const newWaypoints = sequence.slice(1, sequence.length - 1); diff --git a/src/mvp/App.tsx b/src/mvp/App.tsx new file mode 100644 index 0000000..a246334 --- /dev/null +++ b/src/mvp/App.tsx @@ -0,0 +1,33 @@ +import EventAuthenticator from './EventAuthenticator'; +import PlanEvent from './PlanEvent'; + +function Home() { + return ( +
+

+ WheelShare +

+ +
+ ); +} + +export default function App() { + if (window.location.pathname === '/') { + return ; + } else { + // const eventName = window.location.pathname.slice(1); + + return ; + } +} diff --git a/src/mvp/EventAuthenticator.tsx b/src/mvp/EventAuthenticator.tsx new file mode 100644 index 0000000..5aafa73 --- /dev/null +++ b/src/mvp/EventAuthenticator.tsx @@ -0,0 +1,75 @@ +import { useEffect } from 'react'; +import { useState } from 'react'; +import { useContext } from 'react'; +import EventPage from './EventPage'; +import SignIn from './SignIn'; +import SignUp from './SignUp'; +import WheelShareContext from './WheelShareContext'; + +const useAuthenticated = () => { + const { authenticated } = useContext(WheelShareContext); + + return authenticated; +}; + +const eventUrl = window.location.pathname.slice(1); + +export default function EventAuthenticator() { + const authenticated = useAuthenticated(); + const [name, setName] = useState(''); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchName() { + const res = await fetch(`//localhost:5000/events/${eventUrl}/preview`); + if (res.status === 200) { + const json = await res.json(); + setName(json.data.event.name); + } else { + setName(''); + } + } + + fetchName().finally(() => setLoading(false)); + }, []); + + const hasEvent = !!useContext(WheelShareContext).event; + + if (!name && !loading) { + return ( +
+

Event not found

+
+ ); + } + + if (!authenticated) { + return ( +
+

{name}

+ + +
+ ); + } + + if (!hasEvent) { + return null; + } + + return ; +} diff --git a/src/mvp/EventPage.tsx b/src/mvp/EventPage.tsx new file mode 100644 index 0000000..6cfd3a9 --- /dev/null +++ b/src/mvp/EventPage.tsx @@ -0,0 +1,387 @@ +import { Map, Marker, Polyline } from 'google-maps-react'; +import { useEffect } from 'react'; +import { useCallback, useContext, useMemo, useState } from 'react'; +import UIButton from '../components/UI/UIButton'; +import UIPlacesAutocomplete from '../components/UI/UIPlacesAutocomplete'; +import UIPressable from '../components/UI/UIPressable'; +import getDistance from '../lib/getdistance'; +import { + distanceAddedByWaypoint, + estimateOptimalWaypointOrder, +} from './routeOptimization'; +import { Signup } from './types'; +import WheelShareContext from './WheelShareContext'; + +function ll(x: { latitude: number; longitude: number }) { + return { lat: x.latitude, lng: x.longitude }; +} + +// eslint-disable-next-line +const markerColors = [ + 'blue', + 'green', + 'lightblue', + 'orange', + 'pink', + 'purple', + 'red', + 'yellow', +]; + +export default function EventPage() { + const ctx = useContext(WheelShareContext); + const api = ctx.api; + const event = ctx.event!; + + const [map, setMap] = useState>(); + + const signupsWithoutCarpool = useMemo(() => { + const signups = event?.signups ?? []; + const users: Signup[] = []; + for (let signup of Object.values(signups)) { + if (signup.groupId === null) { + users.push(signup); + } + } + return users; + }, [event?.signups]); + + const mySignupId = event.me.signupId; + const mySignup = event.signups[mySignupId]; + const currentPlaceId = mySignup.placeId; + + const myLatLng = mySignup.latitude ? ll(mySignup) : undefined; + + const canDrive = event.me.driving; + + const myCarpoolExtraInfo = event.me.carpool; + const myCarpool = myCarpoolExtraInfo + ? event.carpools.find( + (carpool) => carpool.groupId === myCarpoolExtraInfo.groupId + )! + : null; + + const myCarpoolHasOtherMembers = myCarpool && myCarpool.signupIds.length > 1; + + const focusMany = useCallback( + (locations: google.maps.LatLngLiteral[]) => { + const bounds = new google.maps.LatLngBounds(); + for (let location of locations) { + bounds.extend(location); + } + map?.fitBounds(bounds); + }, + [map] + ); + + const focus = useCallback( + (signupId: string) => { + const highlightedSignup = event.signups[signupId]; + const highlightedSignupLocation = highlightedSignup.latitude + ? ll(highlightedSignup) + : undefined; + + if (highlightedSignupLocation) { + map?.setCenter(highlightedSignupLocation); + map?.setZoom(14); + } + }, + [event, map] + ); + + const [invitedSignupIds, setInvitedSignupIds] = useState< + Record + >({}); + + const invitedSignups = useMemo( + () => Object.keys(invitedSignupIds).map((id) => event.signups[id]), + [event.signups, invitedSignupIds] + ); + + const optimalInvitedSignupPath = useMemo(() => { + if (!mySignup || !mySignup.latitude) { + return null; + } + + const invitedSignupsWithLocation = invitedSignups.filter( + (signup) => signup.latitude + ); + + const path = estimateOptimalWaypointOrder({ + from: mySignup, + // @ts-ignore + waypoints: invitedSignupsWithLocation, + to: event, + }); + + return path; + }, [event, invitedSignups, mySignup]); + + useEffect(() => { + if (optimalInvitedSignupPath && event.latitude && mySignup.latitude) { + focusMany([ + ll(event), + ll(mySignup), + ...optimalInvitedSignupPath.path.waypoints.map(ll), + ]); + } + }, [event, focusMany, mySignup, optimalInvitedSignupPath]); + + return ( +
+

{event.name}

+ setMap(map)} + containerStyle={{ + width: '30rem', + height: '25rem', + position: 'relative', + borderRadius: '0.5rem', + overflow: 'hidden', + }} + google={window.google} + style={{ width: '100%', height: '100%' }} + centerAroundCurrentLocation + > + {invitedSignups.length > 0 && + mySignup.latitude && + optimalInvitedSignupPath && ( + + )} + + + {myLatLng && ( + + )} + {Object.entries(event.signups).map(([id, signup]) => { + if (id === mySignupId) { + return null; + } + if (signup && signup.latitude) { + return ( + focus(id)} + icon={ + id in invitedSignupIds + ? '/markers/lightblue.png' + : '/markers/yellow.png' + } + /> + ); + } + + return null; + })} + +
+
+
+ My location + { + api.joinEvent(placeId); + }} + style={{ + border: '2px solid ' + (currentPlaceId ? '#30ff30' : 'gray'), + marginRight: '0.5rem', + }} + /> +
+ {!myCarpoolHasOtherMembers && ( +
+ Can I drive? + { + if (canDrive) { + setInvitedSignupIds({}); + api.setDriving(false); + } else { + api.setDriving(true); + } + }} + style={{ border: '2px solid #30ff00' }} + > + {canDrive ? 'Yes' : 'No'} + +
+ )} +
+ {myCarpoolHasOtherMembers && ( + <> +

My Carpool

+
+ + Driver: + {myCarpool?.driverName} + + Members: + {myCarpoolExtraInfo!.members.length > 1 ? ( +
    + {myCarpoolExtraInfo!.members.map((member) => { + if (member.signupId === event.me.signupId) { + return null; + } + const signup = event.signups[member.signupId]; + const name = member.name; + return ( +
  • + {name} +
  • + ); + })} +
+ ) : ( + <>(no members) + )} +
+ + )} + {canDrive && ( + <> +

People who need a ride

+ + {invitedSignups.length === 1 + ? '1 person' + : `${invitedSignups.length} people`}{' '} + in temporary carpool. Estimated distance (linear):{' '} + {optimalInvitedSignupPath?.distance.toFixed(1)} miles + +
+ +
+ {signupsWithoutCarpool.map((signup, index) => { + // Don't show people who don't have a location + if (!signup.latitude) { + return null; + } + + const name = 'Person ' + (index + 1); + const distanceAdded = (() => { + if (signup.id in invitedSignupIds) { + return null; + } + if (optimalInvitedSignupPath) { + return distanceAddedByWaypoint( + optimalInvitedSignupPath.path, + signup + ); + } + if (signup.latitude && mySignup.latitude) { + let distanceWithThem = getDistance(mySignup, signup, event); + let distanceWithoutThem = getDistance(mySignup, event); + return distanceWithThem - distanceWithoutThem; + } + return null; + })(); + const invited = signup.id in invitedSignupIds; + return ( +
+ {name} has no carpool.{' '} + {distanceAdded !== null && ( + <>+{distanceAdded.toFixed(1)} miles + )}{' '} +
+ focus(signup.id)} + style={{ marginRight: '0.5rem' }} + > + View on map + + {!invited ? ( + + setInvitedSignupIds((ids) => ({ + ...ids, + [signup.id]: true, + })) + } + > + Add + + ) : ( + { + setInvitedSignupIds((ids) => { + const newIds = { ...ids }; + delete newIds[signup.id]; + return newIds; + }); + }} + > + Remove + + )} +
+
+ ); + })} +
+ + )} +

Carpools

+
+ {event.carpools.map((carpool) => { + const isMe = carpool.groupId === mySignup.groupId; + const driverDisplayName = isMe + ? carpool.driverName + ' (my group)' + : carpool.driverName; + + const passengerCount = carpool.signupIds.length - 1; + + if (passengerCount === 0) { + return ( + + {driverDisplayName}: Available to drive + + ); + } + + return ( + + {driverDisplayName}: Driving +
+ {passengerCount} member + {passengerCount !== 1 ? 's' : ''} +
+ ); + })} +
+
+ ); +} diff --git a/src/mvp/PlanEvent.tsx b/src/mvp/PlanEvent.tsx new file mode 100644 index 0000000..2d2d4ca --- /dev/null +++ b/src/mvp/PlanEvent.tsx @@ -0,0 +1,70 @@ +import { useCallback } from 'react'; +import { useContext } from 'react'; +import { useState } from 'react'; +import UIButton from '../components/UI/UIButton'; +import UIDatetimeInput from '../components/UI/UIDatetimeInput'; +import UIPlacesAutocomplete from '../components/UI/UIPlacesAutocomplete'; +import UITextInput from '../components/UI/UITextInput'; +import WheelShareContext from './WheelShareContext'; + +export default function PlanEvent() { + const [name, setName] = useState(''); + const [startTime, setStartTime] = useState(null); + const [endTime, setEndTime] = useState(null); + const [placeId, setPlaceId] = useState(null); + const [moderatorCode, setModeratorCode] = useState(''); + + const { api } = useContext(WheelShareContext); + + const create = useCallback(() => { + if (!startTime || !endTime || !name || !moderatorCode || !placeId) { + console.error('Tried to create event with incomplete fields.'); + console.error({ startTime, endTime, name, moderatorCode, placeId }); + return; + } + + api + .createEvent(name, startTime, endTime, moderatorCode, placeId) + .then(({ eventId }) => { + console.log('resulting eventId:', eventId); + }); + }, [api, endTime, moderatorCode, name, placeId, startTime]); + + return ( +
+

Plan an event

+ Event Name + +
+ Location + setPlaceId(placeId)} + /> +
+ Start time + +
+ End time + +
+ Admin code (used to modify details about the event) + + Create +
+ ); +} diff --git a/src/mvp/SignIn.tsx b/src/mvp/SignIn.tsx new file mode 100644 index 0000000..4b64ea1 --- /dev/null +++ b/src/mvp/SignIn.tsx @@ -0,0 +1,46 @@ +import { useContext } from 'react'; +import { useState } from 'react'; +import UIButton from '../components/UI/UIButton'; +import UITextInput from '../components/UI/UITextInput'; +import WheelShareContext from './WheelShareContext'; + +export default function SignIn() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + + const { api } = useContext(WheelShareContext); + + return ( + <> +

Sign In

+ Email + +
+ Password + +
+ { + api.signin(email, password).catch((error) => { + setError('error'); + }); + }} + > + Sign In + +
+ {error === 'error' && 'Password was incorrect or signup was not found'} + + ); +} diff --git a/src/mvp/SignUp.tsx b/src/mvp/SignUp.tsx new file mode 100644 index 0000000..4834435 --- /dev/null +++ b/src/mvp/SignUp.tsx @@ -0,0 +1,53 @@ +import { useCallback, useContext, useState } from 'react'; +import UIButton from '../components/UI/UIButton'; +import UITextInput from '../components/UI/UITextInput'; +import WheelShareContext from './WheelShareContext'; + +export default function SignUp() { + const { api } = useContext(WheelShareContext); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [name, setName] = useState(''); + const [error, setError] = useState(''); + + const signup = useCallback(() => { + api.signup(email, password, name).catch((e) => { + console.error(e); + setError('There was an error signing up for the event.'); + }); + }, [api, email, name, password]); + + return ( + <> +

Sign Up

+ Name + +
+ Email + +
+ Password + +
+ + Sign Up + +
+ {error} + + ); +} diff --git a/src/mvp/WheelShareContext.ts b/src/mvp/WheelShareContext.ts new file mode 100644 index 0000000..23fb00a --- /dev/null +++ b/src/mvp/WheelShareContext.ts @@ -0,0 +1,34 @@ +import { createContext } from 'react'; +import { Event } from './types'; + +const WheelShareContext = createContext({ + event: null as Event | null, + authenticated: false, + api: { + /** Returns token */ + async signup(email: string, password: string, name: string): Promise { + throw new Error('Method not implemented.'); + }, + /** Returns token */ + async signin(email: string, password: string): Promise { + throw new Error('Method not implemented.'); + }, + async setDriving(driving: boolean): Promise { + throw new Error('Method not implemented.'); + }, + async joinEvent(placeId: string | null): Promise { + throw new Error('Method not implemented.'); + }, + async createEvent( + name: string, + startTime: Date, + endTime: Date, + moderatorCode: string, + placeId: string + ): Promise<{ eventId: string }> { + throw new Error('Method not implemented.'); + }, + }, +}); + +export default WheelShareContext; diff --git a/src/mvp/WheelShareProvider.tsx b/src/mvp/WheelShareProvider.tsx new file mode 100644 index 0000000..e0bd35f --- /dev/null +++ b/src/mvp/WheelShareProvider.tsx @@ -0,0 +1,202 @@ +import { useDebugValue } from 'react'; +import { useEffect } from 'react'; +import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { Event } from './types'; +import WheelShareContext from './WheelShareContext'; + +const getToken = () => localStorage.getItem('token'); +const requestedEventUrl = window.location.pathname.slice(1); + +export default function WheelShareProvider({ + children, +}: { + children: ReactNode; +}) { + const [event, setEvent] = useState(null); + + const fetchEvent = useCallback(async function fetchEvent() { + const res = await fetch('//localhost:5000/events/' + requestedEventUrl, { + headers: { Authorization: 'Bearer ' + getToken() }, + }); + const json = await res.json(); + const { status, event } = json; + if (status !== 'success') { + console.error({ json }); + setEvent(null); + localStorage.removeItem('token'); + window.location.reload(); + } else { + setEvent(event); + } + }, []); + + useEffect(() => { + const token = getToken(); + + if (!token) { + return; + } + + fetchEvent(); + }, [fetchEvent]); + + const signup = useCallback( + async (email: string, password: string, name: string) => { + const result = await fetch( + `//localhost:5000/events/${requestedEventUrl}/signup`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name, + email, + password, + }), + } + ); + + if (result.status !== 200) { + throw new Error(`Failed to signup: ${result.status}`); + } + + const { token } = await result.json(); + + localStorage.setItem('token', token); + + window.location.reload(); + }, + [] + ); + + const signin = useCallback(async (email: string, password: string) => { + const result = await fetch( + `//localhost:5000/events/${requestedEventUrl}/signin`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + password, + }), + } + ); + + if (result.status !== 200) { + throw new Error(`Failed to signup: ${result.status}`); + } + + const { token } = await result.json(); + + localStorage.setItem('token', token); + + console.log(result); + + window.location.reload(); + }, []); + + const joinEvent = useCallback( + async (placeId: string) => { + const result = await fetch( + `//localhost:5000/events/${requestedEventUrl}/join`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${getToken()}`, + }, + body: JSON.stringify({ + placeId, + }), + } + ); + if (result.status !== 200) { + throw new Error(`Failed to join event: ${result.status}`); + } + + await fetchEvent(); + }, + [fetchEvent] + ); + + const createEvent = useCallback( + async ( + name: string, + startTime: Date, + endTime: Date, + moderatorCode: string, + placeId: string + ) => { + const result = await fetch(`//localhost:5000/events`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name, + startTime, + endTime, + moderatorCode, + placeId, + }), + }); + + if (result.status !== 200) { + throw new Error(`Failed to create event: ${result.status}`); + } + + const { eventId, token } = await result.json(); + + localStorage.setItem('token', token); + + return { eventId }; + }, + [] + ); + + const setDriving = useCallback( + async (driving: boolean) => { + const result = await fetch( + `//localhost:5000/events/${requestedEventUrl}/set_driving`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${getToken()}`, + }, + body: JSON.stringify({ + driving, + }), + } + ); + if (result.status !== 200) { + throw new Error(`Failed to join event: ${result.status}`); + } + + await fetchEvent(); + }, + [fetchEvent] + ); + + const value = useMemo( + () => ({ + api: { + signup, + signin, + joinEvent, + createEvent, + setDriving, + }, + event, + authenticated: !!getToken(), + }), + [createEvent, event, joinEvent, setDriving, signin, signup] + ); + + useDebugValue(value); + + return ( + + {children} + + ); +} diff --git a/src/mvp/routeOptimization.ts b/src/mvp/routeOptimization.ts new file mode 100644 index 0000000..fa0db3d --- /dev/null +++ b/src/mvp/routeOptimization.ts @@ -0,0 +1,86 @@ +import getDistance from '../lib/getdistance'; + +export type Location = { + latitude: number; + longitude: number; +}; + +export type Path = { + from: M; + waypoints: M[]; + to: D; +}; + +export function addWaypointOptimally( + path: Path, + waypoint: M +): { + path: Path; + distance: number; +} { + const { from, to, waypoints } = path; + let sequence = [from, ...waypoints, to]; + + // Iterate over all possible insertion points for the waypoint + let minDistance = Infinity; + let minI = 0; + for (let i = 0; i < sequence.length - 1; i++) { + const temporarySequence = [ + ...sequence.slice(0, i + 1), + waypoint, + ...sequence.slice(i + 1), + ]; + const distance = getDistance(...temporarySequence); + if (distance < minDistance) { + minDistance = distance; + minI = i; + } + } + + sequence = [ + ...sequence.slice(0, minI + 1), + waypoint, + ...sequence.slice(minI + 1), + ]; + + const newWaypoints = sequence.slice(1, sequence.length - 1); + + return { + path: { + from, + to, + waypoints: newWaypoints as M[], + }, + distance: getDistance(from, ...sequence, to), + }; +} + +export function estimateOptimalWaypointOrder< + M extends Location, + D extends Location +>(path: Path) { + let newPath: Path = { + from: path.from, + to: path.to, + waypoints: [], + }; + let distance = getDistance(path.from, path.to); + + for (let waypoint of path.waypoints) { + const result = addWaypointOptimally(newPath, waypoint); + newPath = result.path; + distance = result.distance; + } + + return { path: newPath, distance }; +} + +export function distanceAddedByWaypoint( + path: Path, + waypoint: M +): number { + const originalDistance = getDistance(path.from, ...path.waypoints, path.to); + const { distance: newDistance } = addWaypointOptimally(path, waypoint); + + return newDistance - originalDistance; +} diff --git a/src/mvp/types.ts b/src/mvp/types.ts new file mode 100644 index 0000000..8908b24 --- /dev/null +++ b/src/mvp/types.ts @@ -0,0 +1,37 @@ +export type Signup = { + id: string; + groupId: string | null; +} & ( + | { + placeId: string; + latitude: number; + longitude: number; + } + | { + placeId: null; + latitude: null; + longitude: null; + } +); + +export type Event = { + url: string; + name: string; + latitude: number; + longitude: number; + me: { + signupId: string; + name: string; + driving: boolean; + carpool: { + groupId: string; + members: { signupId: string; name: string; email: string }[]; + } | null; + }; + signups: Record; + carpools: { + groupId: string; + driverName: string; + signupIds: string[]; + }[]; +}; diff --git a/yarn.lock b/yarn.lock index 16e7a69..e103e7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2600,6 +2600,13 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/google-maps-react@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/google-maps-react/-/google-maps-react-2.0.5.tgz#17cdf20cb6d8ae481f9b2fcfd93dcdaaa14cd069" + integrity sha512-qcsYlHiNH169Vf7jmkEwbzBDqBfqqzYkTgK1vL7qkWVZI04wFESADYVITuQunrZ9swY/SG+tTWUIXMlY4W8byw== + dependencies: + google-maps-react "*" + "@types/googlemaps@*": version "3.43.3" resolved "https://registry.npmjs.org/@types/googlemaps/-/googlemaps-3.43.3.tgz" @@ -6339,6 +6346,11 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +google-maps-react@*, google-maps-react@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/google-maps-react/-/google-maps-react-2.0.6.tgz#0473356207ab6b47227b393b89e4b83f6eab06da" + integrity sha512-M8Eo9WndfQEfxcmm6yRq03qdJgw1x6rQmJ9DN+a+xPQ3K7yNDGkVDbinrf4/8vcox7nELbeopbm4bpefKewWfQ== + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.6" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz"