map based mvp

This commit is contained in:
Michael Fatemi 2021-07-24 18:00:29 -04:00
parent 6f2d53270d
commit f6642a94db
28 changed files with 1116 additions and 26 deletions

1
.env
View File

@ -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/

View File

@ -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",

BIN
public/markers/blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/markers/green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/markers/orange.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/markers/pink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/markers/purple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/markers/red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/markers/yellow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { CSSProperties, useCallback } from 'react';
const baseStyle = {
marginTop: '0.5em',
@ -12,9 +12,11 @@ const baseStyle = {
export default function UIDatetimeInput({
onChangedDate,
disabled = false,
style = {},
}: {
onChangedDate: (date: Date | null) => void;
disabled?: boolean;
style?: CSSProperties;
}) {
const onChange = useCallback(
(e) => {
@ -28,7 +30,7 @@ export default function UIDatetimeInput({
);
return (
<input
style={baseStyle}
style={{ ...baseStyle, ...style }}
type="datetime-local"
disabled={disabled}
onChange={onChange}

View File

@ -93,7 +93,7 @@ export default function UIPlacesAutocomplete({
return (
<div
style={{
width: '100%',
// width: '100%',
maxWidth: '100%',
display: 'flex',
flexDirection: 'column',

View File

@ -1,3 +1,4 @@
import { CSSProperties } from 'react';
import { useCallback } from 'react';
const baseStyle = {
@ -13,10 +14,14 @@ export default function UITextInput({
value,
disabled = false,
onChangeText,
password = false,
style = {},
}: {
value: string;
disabled?: boolean;
onChangeText: (text: string) => void;
password?: boolean;
style?: CSSProperties;
}) {
const onChange = useCallback(
(e) => onChangeText(e.target.value),
@ -24,10 +29,12 @@ export default function UITextInput({
);
return (
<input
style={baseStyle}
style={{ ...baseStyle, ...style }}
value={value}
disabled={disabled}
onChange={onChange}
type={password ? 'password' : 'text'}
autoCapitalize="new-password"
/>
);
}

View 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;
}

View File

@ -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;
}
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -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(
<React.StrictMode>
<AuthenticationWrapper>
{/* <AuthenticationWrapper>
<App />
</AuthenticationWrapper>
</AuthenticationWrapper> */}
<WheelShareProvider>
<App />
</WheelShareProvider>
</React.StrictMode>,
document.getElementById('root')
);

View File

@ -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);

33
src/mvp/App.tsx Normal file
View 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 />;
}
}

View 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
View 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
View 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
View 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
View 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}
</>
);
}

View 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;

View 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>
);
}

View 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
View 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[];
}[];
};

View File

@ -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"