map based mvp
1
.env
|
@ -1,2 +1,3 @@
|
||||||
REACT_APP_API_LOCAL=http://localhost:5000/
|
REACT_APP_API_LOCAL=http://localhost:5000/
|
||||||
|
REACT_APP_API_LOCAL0=https://api.wheelshare.app/
|
||||||
REACT_APP_API_PROD=https://api.wheelshare.app/
|
REACT_APP_API_PROD=https://api.wheelshare.app/
|
||||||
|
|
|
@ -8,12 +8,14 @@
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"@types/google-maps-react": "^2.0.5",
|
||||||
"@types/immutable": "^3.8.7",
|
"@types/immutable": "^3.8.7",
|
||||||
"@types/node": "^14.14.37",
|
"@types/node": "^14.14.37",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.3",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
"google-maps-react": "^2.0.6",
|
||||||
"immutable": "^4.0.0-rc.14",
|
"immutable": "^4.0.0-rc.14",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-bootstrap": "^1.5.2",
|
"react-bootstrap": "^1.5.2",
|
||||||
|
|
BIN
public/markers/blue.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
public/markers/green.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/markers/lightblue.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/markers/orange.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
public/markers/pink.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
public/markers/purple.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
public/markers/red.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
public/markers/yellow.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback } from 'react';
|
import { CSSProperties, useCallback } from 'react';
|
||||||
|
|
||||||
const baseStyle = {
|
const baseStyle = {
|
||||||
marginTop: '0.5em',
|
marginTop: '0.5em',
|
||||||
|
@ -12,9 +12,11 @@ const baseStyle = {
|
||||||
export default function UIDatetimeInput({
|
export default function UIDatetimeInput({
|
||||||
onChangedDate,
|
onChangedDate,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
style = {},
|
||||||
}: {
|
}: {
|
||||||
onChangedDate: (date: Date | null) => void;
|
onChangedDate: (date: Date | null) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
}) {
|
}) {
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
@ -28,7 +30,7 @@ export default function UIDatetimeInput({
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
style={baseStyle}
|
style={{ ...baseStyle, ...style }}
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|
|
@ -93,7 +93,7 @@ export default function UIPlacesAutocomplete({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
// width: '100%',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { CSSProperties } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
const baseStyle = {
|
const baseStyle = {
|
||||||
|
@ -13,10 +14,14 @@ export default function UITextInput({
|
||||||
value,
|
value,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onChangeText,
|
onChangeText,
|
||||||
|
password = false,
|
||||||
|
style = {},
|
||||||
}: {
|
}: {
|
||||||
value: string;
|
value: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onChangeText: (text: string) => void;
|
onChangeText: (text: string) => void;
|
||||||
|
password?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
}) {
|
}) {
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(e) => onChangeText(e.target.value),
|
(e) => onChangeText(e.target.value),
|
||||||
|
@ -24,10 +29,12 @@ export default function UITextInput({
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
style={baseStyle}
|
style={{ ...baseStyle, ...style }}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
type={password ? 'password' : 'text'}
|
||||||
|
autoCapitalize="new-password"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
31
src/components/useOptimalPathWithDriver.ts
Normal file
|
@ -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<M, D>({
|
||||||
|
from: driver,
|
||||||
|
waypoints: passengerLocations,
|
||||||
|
to: destination,
|
||||||
|
});
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}, [destination, driver, members]);
|
||||||
|
|
||||||
|
useDebugValue(path);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './components/App';
|
import App from './mvp/App';
|
||||||
|
import WheelShareProvider from './mvp/WheelShareProvider';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
import AuthenticationWrapper from './components/Authentication/AuthenticationWrapper';
|
|
||||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<AuthenticationWrapper>
|
{/* <AuthenticationWrapper>
|
||||||
<App />
|
<App />
|
||||||
</AuthenticationWrapper>
|
</AuthenticationWrapper> */}
|
||||||
|
<WheelShareProvider>
|
||||||
|
<App />
|
||||||
|
</WheelShareProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,26 +23,34 @@ export default function estimateOptimalPath<
|
||||||
const { from, to, waypoints } = path;
|
const { from, to, waypoints } = path;
|
||||||
let sequence = [from, to];
|
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
|
// Calculates all possible paths from the start to the end of the given path
|
||||||
// and returns the one with the minimum distance
|
// and returns the one with the minimum distance
|
||||||
for (let waypoint of waypoints) {
|
for (let waypoint of waypoints) {
|
||||||
// Iterate over all possible insertion points for the waypoint
|
// Iterate over all possible insertion points for the waypoint
|
||||||
let minDistance = Infinity;
|
let minDistance = Infinity;
|
||||||
let insertionPoint = 0;
|
let minI = 0;
|
||||||
for (let i = 0; i < sequence.length - 1; i++) {
|
for (let i = 0; i < sequence.length - 1; i++) {
|
||||||
const [start, end] = sequence.slice(i, i + 2);
|
const temporarySequence = [
|
||||||
|
...sequence.slice(0, i + 1),
|
||||||
const distance = getDistance(start, waypoint, end);
|
waypoint,
|
||||||
|
...sequence.slice(i + 1),
|
||||||
|
];
|
||||||
|
const distance = getDistance(...temporarySequence);
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
minDistance = distance;
|
minDistance = distance;
|
||||||
insertionPoint = i;
|
minI = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sequence = sequence
|
sequence = [
|
||||||
.slice(0, insertionPoint + 1)
|
...sequence.slice(0, minI + 1),
|
||||||
.concat([waypoint])
|
waypoint,
|
||||||
.concat(sequence.slice(insertionPoint + 1));
|
...sequence.slice(minI + 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('Sequence:', sequence, '; distance:', getDistance(...sequence));
|
||||||
}
|
}
|
||||||
|
|
||||||
const newWaypoints = sequence.slice(1, sequence.length - 1);
|
const newWaypoints = sequence.slice(1, sequence.length - 1);
|
||||||
|
|
33
src/mvp/App.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import EventAuthenticator from './EventAuthenticator';
|
||||||
|
import PlanEvent from './PlanEvent';
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontSize: '2rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
WheelShare
|
||||||
|
</h1>
|
||||||
|
<PlanEvent />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
if (window.location.pathname === '/') {
|
||||||
|
return <Home />;
|
||||||
|
} else {
|
||||||
|
// const eventName = window.location.pathname.slice(1);
|
||||||
|
|
||||||
|
return <EventAuthenticator />;
|
||||||
|
}
|
||||||
|
}
|
75
src/mvp/EventAuthenticator.tsx
Normal file
|
@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
maxWidth: 'min(100vw, 30rem)',
|
||||||
|
margin: '0 auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 style={{ textAlign: 'center' }}>Event not found</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authenticated) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
maxWidth: 'min(100vw, 30rem)',
|
||||||
|
margin: '0 auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 style={{ textAlign: 'center' }}>{name}</h1>
|
||||||
|
<SignIn />
|
||||||
|
<SignUp />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasEvent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EventPage />;
|
||||||
|
}
|
387
src/mvp/EventPage.tsx
Normal file
|
@ -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<google.maps.Map<Element>>();
|
||||||
|
|
||||||
|
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<string, boolean>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<h1>{event.name}</h1>
|
||||||
|
<Map
|
||||||
|
onReady={(_props, map) => 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 && (
|
||||||
|
<Polyline
|
||||||
|
path={[
|
||||||
|
ll(mySignup),
|
||||||
|
...optimalInvitedSignupPath.path.waypoints.map(ll),
|
||||||
|
ll(event),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Marker key="event" position={ll(event)} icon="/markers/green.png" />
|
||||||
|
{myLatLng && (
|
||||||
|
<Marker key="me" position={myLatLng} icon="/markers/red.png" />
|
||||||
|
)}
|
||||||
|
{Object.entries(event.signups).map(([id, signup]) => {
|
||||||
|
if (id === mySignupId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (signup && signup.latitude) {
|
||||||
|
return (
|
||||||
|
<Marker
|
||||||
|
key={id}
|
||||||
|
position={{ lat: signup.latitude, lng: signup.longitude }}
|
||||||
|
onClick={() => focus(id)}
|
||||||
|
icon={
|
||||||
|
id in invitedSignupIds
|
||||||
|
? '/markers/lightblue.png'
|
||||||
|
: '/markers/yellow.png'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</Map>
|
||||||
|
<br />
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
My location
|
||||||
|
<UIPlacesAutocomplete
|
||||||
|
placeId={currentPlaceId}
|
||||||
|
onSelected={(address, placeId) => {
|
||||||
|
api.joinEvent(placeId);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
border: '2px solid ' + (currentPlaceId ? '#30ff30' : 'gray'),
|
||||||
|
marginRight: '0.5rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!myCarpoolHasOtherMembers && (
|
||||||
|
<div>
|
||||||
|
Can I drive?
|
||||||
|
<UIButton
|
||||||
|
onClick={() => {
|
||||||
|
if (canDrive) {
|
||||||
|
setInvitedSignupIds({});
|
||||||
|
api.setDriving(false);
|
||||||
|
} else {
|
||||||
|
api.setDriving(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ border: '2px solid #30ff00' }}
|
||||||
|
>
|
||||||
|
{canDrive ? 'Yes' : 'No'}
|
||||||
|
</UIButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{myCarpoolHasOtherMembers && (
|
||||||
|
<>
|
||||||
|
<h2>My Carpool</h2>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
border: '1px solid black',
|
||||||
|
width: '30rem',
|
||||||
|
padding: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<b>Driver: </b>
|
||||||
|
{myCarpool?.driverName}
|
||||||
|
</span>
|
||||||
|
<b>Members: </b>
|
||||||
|
{myCarpoolExtraInfo!.members.length > 1 ? (
|
||||||
|
<ul>
|
||||||
|
{myCarpoolExtraInfo!.members.map((member) => {
|
||||||
|
if (member.signupId === event.me.signupId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const signup = event.signups[member.signupId];
|
||||||
|
const name = member.name;
|
||||||
|
return (
|
||||||
|
<li key={signup.id}>
|
||||||
|
<b>{name}</b>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<>(no members)</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{canDrive && (
|
||||||
|
<>
|
||||||
|
<h2>People who need a ride</h2>
|
||||||
|
<span>
|
||||||
|
{invitedSignups.length === 1
|
||||||
|
? '1 person'
|
||||||
|
: `${invitedSignups.length} people`}{' '}
|
||||||
|
in temporary carpool. Estimated distance (linear):{' '}
|
||||||
|
{optimalInvitedSignupPath?.distance.toFixed(1)} miles
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: '20rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<div key={signup.id} style={{ marginBottom: '0.5rem' }}>
|
||||||
|
<b>{name}</b> has no carpool.{' '}
|
||||||
|
{distanceAdded !== null && (
|
||||||
|
<>+{distanceAdded.toFixed(1)} miles</>
|
||||||
|
)}{' '}
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
<UIPressable
|
||||||
|
onClick={() => focus(signup.id)}
|
||||||
|
style={{ marginRight: '0.5rem' }}
|
||||||
|
>
|
||||||
|
View on map
|
||||||
|
</UIPressable>
|
||||||
|
{!invited ? (
|
||||||
|
<UIPressable
|
||||||
|
onClick={() =>
|
||||||
|
setInvitedSignupIds((ids) => ({
|
||||||
|
...ids,
|
||||||
|
[signup.id]: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</UIPressable>
|
||||||
|
) : (
|
||||||
|
<UIPressable
|
||||||
|
onClick={() => {
|
||||||
|
setInvitedSignupIds((ids) => {
|
||||||
|
const newIds = { ...ids };
|
||||||
|
delete newIds[signup.id];
|
||||||
|
return newIds;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</UIPressable>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<h2>Carpools</h2>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
border: '1px solid black',
|
||||||
|
width: '30rem',
|
||||||
|
padding: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<span key={carpool.groupId}>
|
||||||
|
{driverDisplayName}: Available to drive
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={carpool.groupId}>
|
||||||
|
{driverDisplayName}: Driving
|
||||||
|
<br />
|
||||||
|
{passengerCount} member
|
||||||
|
{passengerCount !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
70
src/mvp/PlanEvent.tsx
Normal file
|
@ -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 | Date>(null);
|
||||||
|
const [endTime, setEndTime] = useState<null | Date>(null);
|
||||||
|
const [placeId, setPlaceId] = useState<null | string>(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 (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', width: '30rem' }}>
|
||||||
|
<h2>Plan an event</h2>
|
||||||
|
<b>Event Name</b>
|
||||||
|
<UITextInput
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
style={{ border: '1px solid gray' }}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<b>Location</b>
|
||||||
|
<UIPlacesAutocomplete
|
||||||
|
style={{ border: '1px solid gray' }}
|
||||||
|
onSelected={(address, placeId) => setPlaceId(placeId)}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<b>Start time</b>
|
||||||
|
<UIDatetimeInput
|
||||||
|
onChangedDate={setStartTime}
|
||||||
|
style={{ border: '1px solid gray' }}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<b>End time</b>
|
||||||
|
<UIDatetimeInput
|
||||||
|
onChangedDate={setEndTime}
|
||||||
|
style={{ border: '1px solid gray' }}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<b>Admin code</b> (used to modify details about the event)
|
||||||
|
<UITextInput
|
||||||
|
value={moderatorCode}
|
||||||
|
onChangeText={setModeratorCode}
|
||||||
|
style={{ border: '1px solid gray' }}
|
||||||
|
/>
|
||||||
|
<UIButton onClick={create}>Create</UIButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
46
src/mvp/SignIn.tsx
Normal file
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<h2>Sign In</h2>
|
||||||
|
Email
|
||||||
|
<UITextInput
|
||||||
|
style={{ border: '1px solid #c0c0c0' }}
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
Password
|
||||||
|
<UITextInput
|
||||||
|
style={{ border: '1px solid #c0c0c0' }}
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
password
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<UIButton
|
||||||
|
style={{ border: '1px solid #c0c0c0' }}
|
||||||
|
onClick={() => {
|
||||||
|
api.signin(email, password).catch((error) => {
|
||||||
|
setError('error');
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</UIButton>
|
||||||
|
<br />
|
||||||
|
{error === 'error' && 'Password was incorrect or signup was not found'}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
53
src/mvp/SignUp.tsx
Normal file
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<h2>Sign Up</h2>
|
||||||
|
Name
|
||||||
|
<UITextInput
|
||||||
|
style={{ border: '1px solid #c0c0c0' }}
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
Email
|
||||||
|
<UITextInput
|
||||||
|
style={{ border: '1px solid #c0c0c0' }}
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
Password
|
||||||
|
<UITextInput
|
||||||
|
style={{ border: '1px solid #c0c0c0' }}
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
password
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<UIButton style={{ border: '1px solid #c0c0c0' }} onClick={signup}>
|
||||||
|
Sign Up
|
||||||
|
</UIButton>
|
||||||
|
<br />
|
||||||
|
{error}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
34
src/mvp/WheelShareContext.ts
Normal file
|
@ -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<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
},
|
||||||
|
/** Returns token */
|
||||||
|
async signin(email: string, password: string): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
},
|
||||||
|
async setDriving(driving: boolean): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
},
|
||||||
|
async joinEvent(placeId: string | null): Promise<void> {
|
||||||
|
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;
|
202
src/mvp/WheelShareProvider.tsx
Normal file
|
@ -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<Event | null>(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 (
|
||||||
|
<WheelShareContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</WheelShareContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
86
src/mvp/routeOptimization.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import getDistance from '../lib/getdistance';
|
||||||
|
|
||||||
|
export type Location = {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Path<M extends Location, D extends Location> = {
|
||||||
|
from: M;
|
||||||
|
waypoints: M[];
|
||||||
|
to: D;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function addWaypointOptimally<M extends Location, D extends Location>(
|
||||||
|
path: Path<M, D>,
|
||||||
|
waypoint: M
|
||||||
|
): {
|
||||||
|
path: Path<M, D>;
|
||||||
|
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<M, D>) {
|
||||||
|
let newPath: Path<M, D> = {
|
||||||
|
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<M extends Location, D extends Location>(
|
||||||
|
path: Path<M, D>,
|
||||||
|
waypoint: M
|
||||||
|
): number {
|
||||||
|
const originalDistance = getDistance(path.from, ...path.waypoints, path.to);
|
||||||
|
const { distance: newDistance } = addWaypointOptimally(path, waypoint);
|
||||||
|
|
||||||
|
return newDistance - originalDistance;
|
||||||
|
}
|
37
src/mvp/types.ts
Normal file
|
@ -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<string, Signup>;
|
||||||
|
carpools: {
|
||||||
|
groupId: string;
|
||||||
|
driverName: string;
|
||||||
|
signupIds: string[];
|
||||||
|
}[];
|
||||||
|
};
|
12
yarn.lock
|
@ -2600,6 +2600,13 @@
|
||||||
"@types/minimatch" "*"
|
"@types/minimatch" "*"
|
||||||
"@types/node" "*"
|
"@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@*":
|
"@types/googlemaps@*":
|
||||||
version "3.43.3"
|
version "3.43.3"
|
||||||
resolved "https://registry.npmjs.org/@types/googlemaps/-/googlemaps-3.43.3.tgz"
|
resolved "https://registry.npmjs.org/@types/googlemaps/-/googlemaps-3.43.3.tgz"
|
||||||
|
@ -6339,6 +6346,11 @@ globby@^6.1.0:
|
||||||
pify "^2.0.0"
|
pify "^2.0.0"
|
||||||
pinkie-promise "^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:
|
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"
|
version "4.2.6"
|
||||||
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz"
|
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz"
|
||||||
|
|