add pickLatLong, other hooks

This commit is contained in:
Michael Fatemi 2021-07-15 10:29:41 -04:00
parent ac6717d527
commit a37323fc07
12 changed files with 159 additions and 242 deletions

View File

@ -1,10 +1,9 @@
import CancelIcon from '@material-ui/icons/Cancel'; import CancelIcon from '@material-ui/icons/Cancel';
import PersonAddIcon from '@material-ui/icons/PersonAdd'; import PersonAddIcon from '@material-ui/icons/PersonAdd';
import { useMemo } from 'react'; import { useContext, useEffect, useMemo } from 'react';
import { useContext, useEffect, useState } from 'react';
import { getEventSignups } from '../api'; import { getEventSignups } from '../api';
import { useMe } from '../hooks';
import { IEventSignup } from '../types'; import { IEventSignup } from '../types';
import useImmutable from '../useImmutable';
import { CarpoolContext } from './Carpool'; import { CarpoolContext } from './Carpool';
function InvitationRow({ function InvitationRow({
@ -43,18 +42,15 @@ function InvitationRow({
export default function InvitationList() { export default function InvitationList() {
const { carpool } = useContext(CarpoolContext); const { carpool } = useContext(CarpoolContext);
const me = useMe()!;
const eventId = carpool.event.id; const eventId = carpool.event.id;
const [availableSignups, setAvailableSignups] = const [availableSignups, setAvailableSignups] =
useState<IEventSignup[] | null>(null); useImmutable<IEventSignup[] | null>(null);
useEffect(() => { useEffect(() => {
getEventSignups(eventId).then((signups) => getEventSignups(eventId).then(setAvailableSignups);
setAvailableSignups(signups.filter((signup) => signup.user.id !== me.id)) }, [eventId, setAvailableSignups]);
);
}, [eventId, me.id]);
const invitedUserIDs = useMemo( const invitedUserIDs = useMemo(
() => () =>

View File

@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect } from 'react';
import { green, lightgrey } from '../../lib/colors'; import { green, lightgrey } from '../../lib/colors';
import getPlaceDetails from '../../lib/getPlaceDetails';
import { import {
addOrUpdateEventSignup, addOrUpdateEventSignup,
getEvent, getEvent,
@ -14,7 +15,6 @@ import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete';
import UISecondaryBox from '../UI/UISecondaryBox'; import UISecondaryBox from '../UI/UISecondaryBox';
import UISecondaryHeader from '../UI/UISecondaryHeader'; import UISecondaryHeader from '../UI/UISecondaryHeader';
import useImmutable from '../useImmutable'; import useImmutable from '../useImmutable';
import useThrottle from '../useThrottle';
import EventCarpools from './EventCarpools'; import EventCarpools from './EventCarpools';
import EventContext from './EventContext'; import EventContext from './EventContext';
import EventDetails from './EventDetails'; import EventDetails from './EventDetails';
@ -51,20 +51,10 @@ export default function Event({
duration: 0, duration: 0,
...(initial || {}), ...(initial || {}),
}); });
const [myPlaceId, setPlaceId] = useState<string | null>(null);
const [interested, setInterested] = useState(false);
const [updating, setUpdating] = useState(false);
const [signups, setSignups] = const [signups, setSignups] =
useState<Record<string, IEventSignup>>(NOT_LOADED); useImmutable<Record<string, IEventSignup>>(NOT_LOADED);
const [hasCarpool, setHasCarpool] = useState(false);
const toggleInterested = useCallback(() => setInterested((i) => !i), []); const me = useMe()!;
const toggleInterestedThrottled = useThrottle(toggleInterested, 500);
const existingSignup = useRef({
interested: false,
placeId: null as string | null,
eventId: null as number | null,
});
const me = useMe();
const [tentativeInvites] = useImmutable<Record<number, boolean>>({}); const [tentativeInvites] = useImmutable<Record<number, boolean>>({});
@ -74,60 +64,44 @@ export default function Event({
useEffect(refresh, [refresh]); useEffect(refresh, [refresh]);
useEffect(() => { const updateSignup = useCallback(
if (signups === NOT_LOADED) { async (placeId: string | null) => {
return; await addOrUpdateEventSignup(id, placeId);
}
const removeSignup = () => { if (placeId) {
if (prev.interested) { const details = await getPlaceDetails(placeId);
removeEventSignup(id)
.then(() => { signups[me.id] = {
prev.interested = false; user: { id: me.id, name: me.name },
}) placeId,
.finally(() => setUpdating(false)); ...details,
};
} else {
signups[me.id] = {
user: { id: me.id, name: me.name },
placeId: null,
latitude: null,
longitude: null,
formattedAddress: null,
};
} }
}; },
[id, me.id, me.name, signups]
);
const addOrUpdateSignup = () => { const removeSignup = useCallback(async () => {
if (!prev.interested || prev.placeId !== myPlaceId) { await removeEventSignup(id);
console.log('Adding or updating signup.', prev, {
interested,
placeId: myPlaceId,
eventId: id,
signups,
});
addOrUpdateEventSignup(id, myPlaceId)
.then(() => {
prev.placeId = myPlaceId;
prev.eventId = id;
prev.interested = true;
})
.finally(() => setUpdating(false));
}
};
const prev = existingSignup.current; if (signups[me.id]) {
delete signups[me.id];
if (!interested) {
removeSignup();
} else {
addOrUpdateSignup();
} }
}, [id, interested, myPlaceId, signups, updating]); }, [id, me.id, signups]);
const interested = !!signups[me.id];
useEffect(() => { useEffect(() => {
getEventSignups(id) getEventSignups(id)
.then((signups) => { .then((signups) => {
for (let signup of signups) {
if (signup.user.id === me?.id) {
setInterested(true);
setPlaceId(signup.placeId);
existingSignup.current.eventId = id;
existingSignup.current.placeId = signup.placeId;
existingSignup.current.interested = true;
}
}
const signupMap: Record<string, IEventSignup> = {}; const signupMap: Record<string, IEventSignup> = {};
for (let signup of signups) { for (let signup of signups) {
signupMap[signup.user.id] = signup; signupMap[signup.user.id] = signup;
@ -135,7 +109,7 @@ export default function Event({
setSignups(signupMap); setSignups(signupMap);
}) })
.catch(console.error); .catch(console.error);
}, [id, me?.id]); }, [id, setSignups]);
if (!event) { if (!event) {
return <UISecondaryBox>Loading...</UISecondaryBox>; return <UISecondaryBox>Loading...</UISecondaryBox>;
@ -151,9 +125,6 @@ export default function Event({
default: false, default: false,
tentativeInvites, tentativeInvites,
signups, signups,
hasCarpool,
setHasCarpool,
myPlaceId,
}} }}
> >
<UISecondaryBox> <UISecondaryBox>
@ -163,7 +134,7 @@ export default function Event({
</div> </div>
<EventDetails {...{ startTime, endTime, formattedAddress }} /> <EventDetails {...{ startTime, endTime, formattedAddress }} />
<UIButton <UIButton
onClick={toggleInterestedThrottled} onClick={interested ? () => removeSignup() : () => updateSignup(null)}
style={{ style={{
backgroundColor: interested ? green : lightgrey, backgroundColor: interested ? green : lightgrey,
color: interested ? 'white' : 'black', color: interested ? 'white' : 'black',
@ -176,15 +147,19 @@ export default function Event({
<> <>
<UIPlacesAutocomplete <UIPlacesAutocomplete
placeholder="Pickup and dropoff location" placeholder="Pickup and dropoff location"
onSelected={(_address, placeID) => { onSelected={(_address, placeId) => {
setPlaceId(placeID); updateSignup(placeId);
}} }}
style={myPlaceId != null ? { border: '2px solid ' + green } : {}} style={
placeId={myPlaceId} signups[me.id]?.placeId != null
? { border: '2px solid ' + green }
: {}
}
placeId={signups[me.id]?.placeId}
/> />
<br /> <br />
<EventCarpools /> <EventCarpools />
{signups !== null && <EventSignups myPlaceId={myPlaceId} />} {signups !== null && <EventSignups />}
</> </>
)} )}
</UISecondaryBox> </UISecondaryBox>

View File

@ -1,4 +1,4 @@
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useCallback, useContext, useMemo, useState } from 'react';
import { lightgrey } from '../../lib/colors'; import { lightgrey } from '../../lib/colors';
import { createCarpool } from '../api'; import { createCarpool } from '../api';
import { useMe } from '../hooks'; import { useMe } from '../hooks';
@ -9,8 +9,7 @@ import EventContext from './EventContext';
type CreationStatus = null | 'pending' | 'completed' | 'errored'; type CreationStatus = null | 'pending' | 'completed' | 'errored';
export default function EventCarpoolCreateButton() { export default function EventCarpoolCreateButton() {
const { event, setHasCarpool, tentativeInvites, signups } = const { event, tentativeInvites } = useContext(EventContext);
useContext(EventContext);
const [creationStatus, setCreationStatus] = useState<CreationStatus>(null); const [creationStatus, setCreationStatus] = useState<CreationStatus>(null);
const [createdCarpoolId, setCreatedCarpoolId] = useState<null | number>(null); const [createdCarpoolId, setCreatedCarpoolId] = useState<null | number>(null);
@ -23,83 +22,29 @@ export default function EventCarpoolCreateButton() {
), ),
[event.carpools, me.id] [event.carpools, me.id]
); );
const alreadyInCarpool =
myCarpool !== undefined || creationStatus === 'completed';
useEffect(() => { const createCarpoolCallback = useCallback(async () => {
setHasCarpool(alreadyInCarpool);
}, [alreadyInCarpool, setHasCarpool]);
const createCarpoolCallback = useCallback(() => {
setCreationStatus('pending'); setCreationStatus('pending');
createCarpool({ const { id } = await createCarpool({
name: me.name + "'s Carpool", name: me.name + "'s Carpool",
eventId: event.id, eventId: event.id,
invitedUserIds: Object.keys(tentativeInvites).map(Number), invitedUserIds: Object.keys(tentativeInvites).map(Number),
}) });
.then(({ id }) => { try {
setCreatedCarpoolId(id); event.carpools.push({
event.carpools.push({ id,
id, name: me.name + "'s Carpool",
name: me.name + "'s Carpool", members: [{ id: me.id, name: me.name }],
members: [{ id: me.id, name: me.name }],
});
setCreationStatus('completed');
})
.catch(() => {
setCreationStatus('errored');
}); });
setCreatedCarpoolId(id);
setCreationStatus('completed');
} catch (e) {
setCreationStatus('errored');
}
}, [event.carpools, event.id, me.id, me.name, tentativeInvites]); }, [event.carpools, event.id, me.id, me.name, tentativeInvites]);
const tentativeInviteNames = useMemo(() => { const inviteCount = Object.keys(tentativeInvites).length;
if (!signups) return [];
const names = Object.keys(tentativeInvites).map((id) => {
const signup = signups[id];
return signup?.user.name;
});
return names.filter((n) => n != null);
}, [tentativeInvites, signups]);
let createCarpoolSection;
if (tentativeInviteNames.length > 0) {
const inviteeCount = tentativeInviteNames.length;
const peoplePlural = inviteeCount > 1 ? 'People' : 'Person';
createCarpoolSection = (
<>
<br />
<b>List:</b>
<br />
{tentativeInviteNames.join(',')}
<UIButton
onClick={createCarpoolCallback}
style={{ backgroundColor: lightgrey }}
>
{creationStatus === null
? `Create Carpool With ${inviteeCount} ${peoplePlural}`
: creationStatus === 'pending'
? 'Creating...'
: 'Errored'}
</UIButton>
</>
);
} else
createCarpoolSection = (
<>
<span>Available to drive?</span>
<UIButton
onClick={createCarpoolCallback}
style={{ backgroundColor: lightgrey }}
>
{creationStatus === null
? 'Create Empty Carpool'
: creationStatus === 'pending'
? 'Creating...'
: 'Errored'}
</UIButton>
</>
);
return ( return (
<div> <div>
@ -114,7 +59,21 @@ export default function EventCarpoolCreateButton() {
<UILink href={`/carpools/${myCarpool.id}`}>{myCarpool.name}</UILink> <UILink href={`/carpools/${myCarpool.id}`}>{myCarpool.name}</UILink>
</span> </span>
) : ( ) : (
createCarpoolSection <>
<span>Available to drive?</span>
<UIButton
onClick={createCarpoolCallback}
style={{ backgroundColor: lightgrey }}
>
{creationStatus === null
? inviteCount === 0
? 'Create Empty Carpool'
: 'Create With ' + inviteCount
: creationStatus === 'pending'
? 'Creating...'
: 'Errored'}
</UIButton>
</>
)} )}
</div> </div>
); );

View File

@ -11,8 +11,8 @@ import {
import { useMe } from '../hooks'; import { useMe } from '../hooks';
import { IEvent } from '../types'; import { IEvent } from '../types';
import useOptimalPath from '../useOptimalPath'; import useOptimalPath from '../useOptimalPath';
import usePlace from '../usePlace';
import EventContext from './EventContext'; import EventContext from './EventContext';
import useMySignup from './useMySignup';
function useMemberLocations(members: IEvent['carpools'][0]['members']) { function useMemberLocations(members: IEvent['carpools'][0]['members']) {
const { signups } = useContext(EventContext); const { signups } = useContext(EventContext);
@ -61,10 +61,17 @@ function CarpoolRow({
const { const {
event: { latitude, longitude }, event: { latitude, longitude },
myPlaceId,
} = useContext(EventContext); } = useContext(EventContext);
const myLocation = usePlace(myPlaceId); const mySignup = useMySignup();
const myLocation =
mySignup && mySignup.latitude !== null
? {
latitude: mySignup.latitude,
longitude: mySignup.longitude,
}
: null;
const memberLocations = useMemberLocations(carpool.members); const memberLocations = useMemberLocations(carpool.members);

View File

@ -9,11 +9,6 @@ const EventContext = createContext({
default: true, default: true,
signups: {} as Record<string, IEventSignup>, signups: {} as Record<string, IEventSignup>,
tentativeInvites: {} as Record<number, boolean>, tentativeInvites: {} as Record<number, boolean>,
hasCarpool: false,
setHasCarpool: (has: boolean) => {
console.error('not implemented: setHasCarpool');
},
myPlaceId: null as string | null,
}); });
export default EventContext; export default EventContext;

View File

@ -1,68 +1,42 @@
import CancelIcon from '@material-ui/icons/Cancel'; import CancelIcon from '@material-ui/icons/Cancel';
import PersonAddIcon from '@material-ui/icons/PersonAdd'; import PersonAddIcon from '@material-ui/icons/PersonAdd';
import { useContext, useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { PlaceDetails } from '../../lib/getPlaceDetails';
import latlongdist, { R_miles } from '../../lib/latlongdist'; import latlongdist, { R_miles } from '../../lib/latlongdist';
import { useMe } from '../hooks'; import { useMe } from '../hooks';
import { IEventSignup } from '../types'; import { IEventSignup } from '../types';
import usePlace from '../usePlace';
import EventCarpoolCreateButton from './EventCarpoolCreateButton'; import EventCarpoolCreateButton from './EventCarpoolCreateButton';
import EventContext from './EventContext'; import EventContext from './EventContext';
import pickLatLong from './pickLatLong';
import useMySignup from './useMySignup';
function EventSignup({ function EventSignup({ signup }: { signup: IEventSignup }) {
signup, const { user } = signup;
locationLatitude,
locationLongitude,
myPlaceDetails,
}: {
signup: IEventSignup;
locationLatitude: number;
locationLongitude: number;
myPlaceDetails: PlaceDetails | null;
}) {
const { user, latitude, longitude } = signup;
const me = useMe(); const me = useMe();
const { tentativeInvites, hasCarpool } = useContext(EventContext); const { tentativeInvites, event } = useContext(EventContext);
const mySignup = useMySignup();
const myLocation = pickLatLong(mySignup);
const theirLocation = pickLatLong(signup);
const eventLocation = pickLatLong(event)!;
const extraDistance = useMemo(() => { const extraDistance = useMemo(() => {
if (myPlaceDetails != null && !(latitude === null || longitude === null)) { if (myLocation != null && theirLocation != null) {
const myLatitude = myPlaceDetails.latitude; const meToThem = latlongdist(myLocation, theirLocation, R_miles);
const myLongitude = myPlaceDetails.longitude; const themToLocation = latlongdist(theirLocation, eventLocation, R_miles);
const meToThem = latlongdist( const meToLocation = latlongdist(myLocation, eventLocation, R_miles);
latitude, return meToThem + themToLocation - meToLocation;
longitude,
locationLongitude,
locationLatitude,
R_miles
);
const themToLocation = latlongdist(
latitude,
longitude,
myLatitude,
myLongitude,
R_miles
);
const totalWithThem = meToThem + themToLocation;
const totalWithoutThem = latlongdist(
locationLongitude,
locationLatitude,
myLatitude,
myLongitude,
R_miles
);
return totalWithThem - totalWithoutThem;
} else { } else {
return null; return null;
} }
}, [ }, [eventLocation, myLocation, theirLocation]);
latitude,
longitude,
locationLatitude,
locationLongitude,
myPlaceDetails,
]);
const isTentativelyInvited = signup.user.id in tentativeInvites; const isTentativelyInvited = signup.user.id in tentativeInvites;
const hasCarpool = useMemo(
() =>
event.carpools.some((carpool) =>
carpool.members.some((member) => member.id === me?.id)
),
[event.carpools, me?.id]
);
if (user.id === me?.id) { if (user.id === me?.id) {
return null; return null;
@ -104,14 +78,9 @@ function EventSignup({
); );
} }
export default function EventSignups({ export default function EventSignups() {
myPlaceId,
}: {
myPlaceId: string | null;
}) {
const { event, signups } = useContext(EventContext); const { event, signups } = useContext(EventContext);
const carpools = event.carpools; const carpools = event.carpools;
const myPlaceDetails = usePlace(myPlaceId);
const signupsWithoutCarpool = useMemo(() => { const signupsWithoutCarpool = useMemo(() => {
// A list of users not in any carpool // A list of users not in any carpool
@ -128,13 +97,7 @@ export default function EventSignups({
<h3 style={{ marginBlockEnd: '0' }}>People without a carpool</h3> <h3 style={{ marginBlockEnd: '0' }}>People without a carpool</h3>
<EventCarpoolCreateButton /> <EventCarpoolCreateButton />
{signupsWithoutCarpool.map((signup) => ( {signupsWithoutCarpool.map((signup) => (
<EventSignup <EventSignup key={signup.user.id} signup={signup} />
key={signup.user.id}
signup={signup}
myPlaceDetails={myPlaceDetails}
locationLatitude={event.latitude}
locationLongitude={event.longitude}
/>
))} ))}
</div> </div>
); );

View File

@ -0,0 +1,11 @@
export default function pickLatLong<
T extends { latitude: number | null; longitude: number | null } | null
>(e: T): { latitude: number; longitude: number } | null {
if (e === null) {
return null;
}
if (e.latitude === null || e.longitude === null) {
return null;
}
return { latitude: e.latitude, longitude: e.longitude };
}

View File

@ -0,0 +1,10 @@
import { useContext, useMemo } from 'react';
import { useMe } from '../hooks';
import EventContext from './EventContext';
export default function useMySignup() {
const { signups } = useContext(EventContext);
const me = useMe()!;
return useMemo(() => signups[me.id] ?? null, [signups, me.id]);
}

View File

@ -1,14 +1,19 @@
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import { getEvents } from './api'; import { getEvents } from './api';
import { IEvent } from './types'; import { IEvent } from './types';
import EventStream from './EventStream'; import EventStream from './EventStream';
import useImmutable from './useImmutable';
export default function Events() { export default function Events() {
const [events, setEvents] = useState<IEvent[]>([]); const [events, setEvents] = useImmutable<IEvent[]>([]);
const hasEvents = events.length > 0;
useEffect(() => { useEffect(() => {
getEvents().then(setEvents); if (!hasEvents) {
}, []); getEvents().then(setEvents);
}
}, [hasEvents, setEvents]);
return ( return (
<> <>

View File

@ -86,17 +86,19 @@ export type IEvent = {
*/ */
export type IEventSignup = { export type IEventSignup = {
eventId: number;
// userId: number;
user: { user: {
id: number; id: number;
name: string; name: string;
}; };
placeId: string | null; } & (
formattedAddress: string | null; | { placeId: null; formattedAddress: null; latitude: null; longitude: null }
latitude: number | null; | {
longitude: number | null; placeId: string;
}; formattedAddress: string;
latitude: number;
longitude: number;
}
);
export type IInvitation = { export type IInvitation = {
user: { user: {

View File

@ -6,13 +6,7 @@ export default function getDistance(...locations: Location[]): number {
for (let i = 0; i < locations.length - 1; i++) { for (let i = 0; i < locations.length - 1; i++) {
const from = locations[i]; const from = locations[i];
const to = locations[i + 1]; const to = locations[i + 1];
distance += latlongdist( distance += latlongdist(from, to, R_miles);
from.latitude,
from.longitude,
to.latitude,
to.longitude,
R_miles
);
} }
return distance; return distance;
} }

View File

@ -13,12 +13,12 @@ export const R_miles = 3958.8;
* @returns The distance in meters between point 1 and point 2 * @returns The distance in meters between point 1 and point 2
*/ */
export default function latlongdist( export default function latlongdist(
lat1: number, firstLocation: { latitude: number; longitude: number },
lon1: number, secondLocation: { latitude: number; longitude: number },
lat2: number,
lon2: number,
R = R_meters R = R_meters
) { ) {
const { latitude: lat1, longitude: lon1 } = firstLocation;
const { latitude: lat2, longitude: lon2 } = secondLocation;
const φ1 = (lat1 * Math.PI) / 180; // φ, λ in radians const φ1 = (lat1 * Math.PI) / 180; // φ, λ in radians
const φ2 = (lat2 * Math.PI) / 180; const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180; const Δφ = ((lat2 - lat1) * Math.PI) / 180;