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 PersonAddIcon from '@material-ui/icons/PersonAdd';
import { useMemo } from 'react';
import { useContext, useEffect, useState } from 'react';
import { useContext, useEffect, useMemo } from 'react';
import { getEventSignups } from '../api';
import { useMe } from '../hooks';
import { IEventSignup } from '../types';
import useImmutable from '../useImmutable';
import { CarpoolContext } from './Carpool';
function InvitationRow({
@ -43,18 +42,15 @@ function InvitationRow({
export default function InvitationList() {
const { carpool } = useContext(CarpoolContext);
const me = useMe()!;
const eventId = carpool.event.id;
const [availableSignups, setAvailableSignups] =
useState<IEventSignup[] | null>(null);
useImmutable<IEventSignup[] | null>(null);
useEffect(() => {
getEventSignups(eventId).then((signups) =>
setAvailableSignups(signups.filter((signup) => signup.user.id !== me.id))
);
}, [eventId, me.id]);
getEventSignups(eventId).then(setAvailableSignups);
}, [eventId, setAvailableSignups]);
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 getPlaceDetails from '../../lib/getPlaceDetails';
import {
addOrUpdateEventSignup,
getEvent,
@ -14,7 +15,6 @@ import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete';
import UISecondaryBox from '../UI/UISecondaryBox';
import UISecondaryHeader from '../UI/UISecondaryHeader';
import useImmutable from '../useImmutable';
import useThrottle from '../useThrottle';
import EventCarpools from './EventCarpools';
import EventContext from './EventContext';
import EventDetails from './EventDetails';
@ -51,20 +51,10 @@ export default function Event({
duration: 0,
...(initial || {}),
});
const [myPlaceId, setPlaceId] = useState<string | null>(null);
const [interested, setInterested] = useState(false);
const [updating, setUpdating] = useState(false);
const [signups, setSignups] =
useState<Record<string, IEventSignup>>(NOT_LOADED);
const [hasCarpool, setHasCarpool] = useState(false);
const toggleInterested = useCallback(() => setInterested((i) => !i), []);
const toggleInterestedThrottled = useThrottle(toggleInterested, 500);
const existingSignup = useRef({
interested: false,
placeId: null as string | null,
eventId: null as number | null,
});
const me = useMe();
useImmutable<Record<string, IEventSignup>>(NOT_LOADED);
const me = useMe()!;
const [tentativeInvites] = useImmutable<Record<number, boolean>>({});
@ -74,60 +64,44 @@ export default function Event({
useEffect(refresh, [refresh]);
useEffect(() => {
if (signups === NOT_LOADED) {
return;
}
const updateSignup = useCallback(
async (placeId: string | null) => {
await addOrUpdateEventSignup(id, placeId);
const removeSignup = () => {
if (prev.interested) {
removeEventSignup(id)
.then(() => {
prev.interested = false;
})
.finally(() => setUpdating(false));
}
if (placeId) {
const details = await getPlaceDetails(placeId);
signups[me.id] = {
user: { id: me.id, name: me.name },
placeId,
...details,
};
const addOrUpdateSignup = () => {
if (!prev.interested || prev.placeId !== myPlaceId) {
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 (!interested) {
removeSignup();
} else {
addOrUpdateSignup();
signups[me.id] = {
user: { id: me.id, name: me.name },
placeId: null,
latitude: null,
longitude: null,
formattedAddress: null,
};
}
}, [id, interested, myPlaceId, signups, updating]);
},
[id, me.id, me.name, signups]
);
const removeSignup = useCallback(async () => {
await removeEventSignup(id);
if (signups[me.id]) {
delete signups[me.id];
}
}, [id, me.id, signups]);
const interested = !!signups[me.id];
useEffect(() => {
getEventSignups(id)
.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> = {};
for (let signup of signups) {
signupMap[signup.user.id] = signup;
@ -135,7 +109,7 @@ export default function Event({
setSignups(signupMap);
})
.catch(console.error);
}, [id, me?.id]);
}, [id, setSignups]);
if (!event) {
return <UISecondaryBox>Loading...</UISecondaryBox>;
@ -151,9 +125,6 @@ export default function Event({
default: false,
tentativeInvites,
signups,
hasCarpool,
setHasCarpool,
myPlaceId,
}}
>
<UISecondaryBox>
@ -163,7 +134,7 @@ export default function Event({
</div>
<EventDetails {...{ startTime, endTime, formattedAddress }} />
<UIButton
onClick={toggleInterestedThrottled}
onClick={interested ? () => removeSignup() : () => updateSignup(null)}
style={{
backgroundColor: interested ? green : lightgrey,
color: interested ? 'white' : 'black',
@ -176,15 +147,19 @@ export default function Event({
<>
<UIPlacesAutocomplete
placeholder="Pickup and dropoff location"
onSelected={(_address, placeID) => {
setPlaceId(placeID);
onSelected={(_address, placeId) => {
updateSignup(placeId);
}}
style={myPlaceId != null ? { border: '2px solid ' + green } : {}}
placeId={myPlaceId}
style={
signups[me.id]?.placeId != null
? { border: '2px solid ' + green }
: {}
}
placeId={signups[me.id]?.placeId}
/>
<br />
<EventCarpools />
{signups !== null && <EventSignups myPlaceId={myPlaceId} />}
{signups !== null && <EventSignups />}
</>
)}
</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 { createCarpool } from '../api';
import { useMe } from '../hooks';
@ -9,8 +9,7 @@ import EventContext from './EventContext';
type CreationStatus = null | 'pending' | 'completed' | 'errored';
export default function EventCarpoolCreateButton() {
const { event, setHasCarpool, tentativeInvites, signups } =
useContext(EventContext);
const { event, tentativeInvites } = useContext(EventContext);
const [creationStatus, setCreationStatus] = useState<CreationStatus>(null);
const [createdCarpoolId, setCreatedCarpoolId] = useState<null | number>(null);
@ -23,83 +22,29 @@ export default function EventCarpoolCreateButton() {
),
[event.carpools, me.id]
);
const alreadyInCarpool =
myCarpool !== undefined || creationStatus === 'completed';
useEffect(() => {
setHasCarpool(alreadyInCarpool);
}, [alreadyInCarpool, setHasCarpool]);
const createCarpoolCallback = useCallback(() => {
const createCarpoolCallback = useCallback(async () => {
setCreationStatus('pending');
createCarpool({
const { id } = await createCarpool({
name: me.name + "'s Carpool",
eventId: event.id,
invitedUserIds: Object.keys(tentativeInvites).map(Number),
})
.then(({ id }) => {
setCreatedCarpoolId(id);
});
try {
event.carpools.push({
id,
name: me.name + "'s Carpool",
members: [{ id: me.id, name: me.name }],
});
setCreatedCarpoolId(id);
setCreationStatus('completed');
})
.catch(() => {
} catch (e) {
setCreationStatus('errored');
});
}
}, [event.carpools, event.id, me.id, me.name, tentativeInvites]);
const tentativeInviteNames = useMemo(() => {
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>
</>
);
const inviteCount = Object.keys(tentativeInvites).length;
return (
<div>
@ -114,7 +59,21 @@ export default function EventCarpoolCreateButton() {
<UILink href={`/carpools/${myCarpool.id}`}>{myCarpool.name}</UILink>
</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>
);

View File

@ -11,8 +11,8 @@ import {
import { useMe } from '../hooks';
import { IEvent } from '../types';
import useOptimalPath from '../useOptimalPath';
import usePlace from '../usePlace';
import EventContext from './EventContext';
import useMySignup from './useMySignup';
function useMemberLocations(members: IEvent['carpools'][0]['members']) {
const { signups } = useContext(EventContext);
@ -61,10 +61,17 @@ function CarpoolRow({
const {
event: { latitude, longitude },
myPlaceId,
} = 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);

View File

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

View File

@ -1,68 +1,42 @@
import CancelIcon from '@material-ui/icons/Cancel';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
import { useContext, useMemo } from 'react';
import { PlaceDetails } from '../../lib/getPlaceDetails';
import latlongdist, { R_miles } from '../../lib/latlongdist';
import { useMe } from '../hooks';
import { IEventSignup } from '../types';
import usePlace from '../usePlace';
import EventCarpoolCreateButton from './EventCarpoolCreateButton';
import EventContext from './EventContext';
import pickLatLong from './pickLatLong';
import useMySignup from './useMySignup';
function EventSignup({
signup,
locationLatitude,
locationLongitude,
myPlaceDetails,
}: {
signup: IEventSignup;
locationLatitude: number;
locationLongitude: number;
myPlaceDetails: PlaceDetails | null;
}) {
const { user, latitude, longitude } = signup;
function EventSignup({ signup }: { signup: IEventSignup }) {
const { user } = signup;
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(() => {
if (myPlaceDetails != null && !(latitude === null || longitude === null)) {
const myLatitude = myPlaceDetails.latitude;
const myLongitude = myPlaceDetails.longitude;
const meToThem = latlongdist(
latitude,
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;
if (myLocation != null && theirLocation != null) {
const meToThem = latlongdist(myLocation, theirLocation, R_miles);
const themToLocation = latlongdist(theirLocation, eventLocation, R_miles);
const meToLocation = latlongdist(myLocation, eventLocation, R_miles);
return meToThem + themToLocation - meToLocation;
} else {
return null;
}
}, [
latitude,
longitude,
locationLatitude,
locationLongitude,
myPlaceDetails,
]);
}, [eventLocation, myLocation, theirLocation]);
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) {
return null;
@ -104,14 +78,9 @@ function EventSignup({
);
}
export default function EventSignups({
myPlaceId,
}: {
myPlaceId: string | null;
}) {
export default function EventSignups() {
const { event, signups } = useContext(EventContext);
const carpools = event.carpools;
const myPlaceDetails = usePlace(myPlaceId);
const signupsWithoutCarpool = useMemo(() => {
// 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>
<EventCarpoolCreateButton />
{signupsWithoutCarpool.map((signup) => (
<EventSignup
key={signup.user.id}
signup={signup}
myPlaceDetails={myPlaceDetails}
locationLatitude={event.latitude}
locationLongitude={event.longitude}
/>
<EventSignup key={signup.user.id} signup={signup} />
))}
</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 { IEvent } from './types';
import EventStream from './EventStream';
import useImmutable from './useImmutable';
export default function Events() {
const [events, setEvents] = useState<IEvent[]>([]);
const [events, setEvents] = useImmutable<IEvent[]>([]);
const hasEvents = events.length > 0;
useEffect(() => {
if (!hasEvents) {
getEvents().then(setEvents);
}, []);
}
}, [hasEvents, setEvents]);
return (
<>

View File

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

View File

@ -6,13 +6,7 @@ export default function getDistance(...locations: Location[]): number {
for (let i = 0; i < locations.length - 1; i++) {
const from = locations[i];
const to = locations[i + 1];
distance += latlongdist(
from.latitude,
from.longitude,
to.latitude,
to.longitude,
R_miles
);
distance += latlongdist(from, to, R_miles);
}
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
*/
export default function latlongdist(
lat1: number,
lon1: number,
lat2: number,
lon2: number,
firstLocation: { latitude: number; longitude: number },
secondLocation: { latitude: number; longitude: number },
R = R_meters
) {
const { latitude: lat1, longitude: lon1 } = firstLocation;
const { latitude: lat2, longitude: lon2 } = secondLocation;
const φ1 = (lat1 * Math.PI) / 180; // φ, λ in radians
const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180;