diff --git a/src/components/Carpool/InvitationList.tsx b/src/components/Carpool/InvitationList.tsx index 95d9fa2..a7ea5ce 100644 --- a/src/components/Carpool/InvitationList.tsx +++ b/src/components/Carpool/InvitationList.tsx @@ -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(null); + useImmutable(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( () => diff --git a/src/components/Event/Event.tsx b/src/components/Event/Event.tsx index ee0d744..a2d2c23 100644 --- a/src/components/Event/Event.tsx +++ b/src/components/Event/Event.tsx @@ -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(null); - const [interested, setInterested] = useState(false); - const [updating, setUpdating] = useState(false); const [signups, setSignups] = - useState>(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>(NOT_LOADED); + + const me = useMe()!; const [tentativeInvites] = useImmutable>({}); @@ -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, + }; + } 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 = () => { - 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 removeSignup = useCallback(async () => { + await removeEventSignup(id); - const prev = existingSignup.current; - - if (!interested) { - removeSignup(); - } else { - addOrUpdateSignup(); + if (signups[me.id]) { + delete signups[me.id]; } - }, [id, interested, myPlaceId, signups, updating]); + }, [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 = {}; 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 Loading...; @@ -151,9 +125,6 @@ export default function Event({ default: false, tentativeInvites, signups, - hasCarpool, - setHasCarpool, - myPlaceId, }} > @@ -163,7 +134,7 @@ export default function Event({ removeSignup() : () => updateSignup(null)} style={{ backgroundColor: interested ? green : lightgrey, color: interested ? 'white' : 'black', @@ -176,15 +147,19 @@ export default function Event({ <> { - 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} />
- {signups !== null && } + {signups !== null && } )}
diff --git a/src/components/Event/EventCarpoolCreateButton.tsx b/src/components/Event/EventCarpoolCreateButton.tsx index f945561..e0931dc 100644 --- a/src/components/Event/EventCarpoolCreateButton.tsx +++ b/src/components/Event/EventCarpoolCreateButton.tsx @@ -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(null); const [createdCarpoolId, setCreatedCarpoolId] = useState(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); - event.carpools.push({ - id, - name: me.name + "'s Carpool", - members: [{ id: me.id, name: me.name }], - }); - setCreationStatus('completed'); - }) - .catch(() => { - setCreationStatus('errored'); + }); + try { + event.carpools.push({ + id, + name: me.name + "'s Carpool", + members: [{ id: me.id, name: me.name }], }); + setCreatedCarpoolId(id); + setCreationStatus('completed'); + } 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 = ( - <> -
- List: -
- {tentativeInviteNames.join(',')} - - {creationStatus === null - ? `Create Carpool With ${inviteeCount} ${peoplePlural}` - : creationStatus === 'pending' - ? 'Creating...' - : 'Errored'} - - - ); - } else - createCarpoolSection = ( - <> - Available to drive? - - {creationStatus === null - ? 'Create Empty Carpool' - : creationStatus === 'pending' - ? 'Creating...' - : 'Errored'} - - - ); + const inviteCount = Object.keys(tentativeInvites).length; return (
@@ -114,7 +59,21 @@ export default function EventCarpoolCreateButton() { {myCarpool.name} ) : ( - createCarpoolSection + <> + Available to drive? + + {creationStatus === null + ? inviteCount === 0 + ? 'Create Empty Carpool' + : 'Create With ' + inviteCount + : creationStatus === 'pending' + ? 'Creating...' + : 'Errored'} + + )}
); diff --git a/src/components/Event/EventCarpools.tsx b/src/components/Event/EventCarpools.tsx index 12fd5fe..d036a33 100644 --- a/src/components/Event/EventCarpools.tsx +++ b/src/components/Event/EventCarpools.tsx @@ -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); diff --git a/src/components/Event/EventContext.tsx b/src/components/Event/EventContext.tsx index 363956c..20aaac1 100644 --- a/src/components/Event/EventContext.tsx +++ b/src/components/Event/EventContext.tsx @@ -9,11 +9,6 @@ const EventContext = createContext({ default: true, signups: {} as Record, tentativeInvites: {} as Record, - hasCarpool: false, - setHasCarpool: (has: boolean) => { - console.error('not implemented: setHasCarpool'); - }, - myPlaceId: null as string | null, }); export default EventContext; diff --git a/src/components/Event/EventSignups.tsx b/src/components/Event/EventSignups.tsx index 4a79bc6..cc361e9 100644 --- a/src/components/Event/EventSignups.tsx +++ b/src/components/Event/EventSignups.tsx @@ -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({

People without a carpool

{signupsWithoutCarpool.map((signup) => ( - + ))} ); diff --git a/src/components/Event/pickLatLong.ts b/src/components/Event/pickLatLong.ts new file mode 100644 index 0000000..6a7f743 --- /dev/null +++ b/src/components/Event/pickLatLong.ts @@ -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 }; +} diff --git a/src/components/Event/useMySignup.tsx b/src/components/Event/useMySignup.tsx new file mode 100644 index 0000000..d44c73a --- /dev/null +++ b/src/components/Event/useMySignup.tsx @@ -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]); +} diff --git a/src/components/Events.tsx b/src/components/Events.tsx index 14d027c..5a33712 100644 --- a/src/components/Events.tsx +++ b/src/components/Events.tsx @@ -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([]); + const [events, setEvents] = useImmutable([]); + + const hasEvents = events.length > 0; useEffect(() => { - getEvents().then(setEvents); - }, []); + if (!hasEvents) { + getEvents().then(setEvents); + } + }, [hasEvents, setEvents]); return ( <> diff --git a/src/components/types.ts b/src/components/types.ts index d4b9e86..1c67526 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -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: { diff --git a/src/lib/getdistance.ts b/src/lib/getdistance.ts index b06515c..697c979 100644 --- a/src/lib/getdistance.ts +++ b/src/lib/getdistance.ts @@ -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; } diff --git a/src/lib/latlongdist.ts b/src/lib/latlongdist.ts index 3face9a..ba34d3f 100644 --- a/src/lib/latlongdist.ts +++ b/src/lib/latlongdist.ts @@ -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;