feature: creating carpools with invitees, pt.1

This commit is contained in:
Michael Fatemi 2021-07-14 12:11:34 -04:00
parent 7de92f4773
commit 8b8ef46b3f
4 changed files with 217 additions and 91 deletions

View File

@ -1,3 +1,4 @@
import * as immutable from 'immutable';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { green, lightgrey } from '../../lib/colors'; import { green, lightgrey } from '../../lib/colors';
import { import {
@ -35,6 +36,7 @@ export default function Event({
const [interested, setInterested] = useState(false); const [interested, setInterested] = useState(false);
const [updating, setUpdating] = useState(false); const [updating, setUpdating] = useState(false);
const [signups, setSignups] = useState<IEventSignup[] | null>(null); const [signups, setSignups] = useState<IEventSignup[] | null>(null);
const [hasCarpool, setHasCarpool] = useState(false);
const toggleInterested = useCallback(() => setInterested((i) => !i), []); const toggleInterested = useCallback(() => setInterested((i) => !i), []);
const toggleInterestedThrottled = useThrottle(toggleInterested, 500); const toggleInterestedThrottled = useThrottle(toggleInterested, 500);
const existingSignup = useRef({ const existingSignup = useRef({
@ -44,6 +46,18 @@ export default function Event({
}); });
const me = useMe(); const me = useMe();
const [tentativeInvites, setTentativeInvites] = useState(
immutable.Set<number>()
);
const addTentativeInvite = useCallback((userId: number) => {
setTentativeInvites((t) => t.add(userId));
}, []);
const removeTentativeInvite = useCallback((userId: number) => {
setTentativeInvites((t) => t.delete(userId));
}, []);
const refresh = useCallback(() => { const refresh = useCallback(() => {
getEvent(id).then(setEvent); getEvent(id).then(setEvent);
}, [id]); }, [id]);
@ -116,7 +130,19 @@ export default function Event({
const { name, group, formattedAddress, startTime, endTime } = event; const { name, group, formattedAddress, startTime, endTime } = event;
return ( return (
<EventContext.Provider value={{ event, refresh, default: false }}> <EventContext.Provider
value={{
event,
refresh,
default: false,
addTentativeInvite,
removeTentativeInvite,
tentativeInvites,
signups,
hasCarpool,
setHasCarpool,
}}
>
<UISecondaryBox> <UISecondaryBox>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<UISecondaryHeader>{name}</UISecondaryHeader> <UISecondaryHeader>{name}</UISecondaryHeader>
@ -146,11 +172,7 @@ export default function Event({
<br /> <br />
<EventCarpools /> <EventCarpools />
{signups !== null && ( {signups !== null && (
<EventSignups <EventSignups myPlaceId={placeId} signups={signups} />
event={event}
myPlaceId={placeId}
signups={signups}
/>
)} )}
</> </>
)} )}

View File

@ -1,6 +1,7 @@
import CancelIcon from '@material-ui/icons/Cancel'; import CancelIcon from '@material-ui/icons/Cancel';
import CheckIcon from '@material-ui/icons/Check'; import CheckIcon from '@material-ui/icons/Check';
import EmojiPeopleIcon from '@material-ui/icons/EmojiPeople'; import EmojiPeopleIcon from '@material-ui/icons/EmojiPeople';
import { useEffect } from 'react';
import { useCallback, useContext, useMemo, useState } from 'react'; import { useCallback, useContext, useMemo, useState } from 'react';
import { lightgrey } from '../../lib/colors'; import { lightgrey } from '../../lib/colors';
import { import {
@ -91,7 +92,8 @@ function CarpoolRow({
type CreationStatus = null | 'pending' | 'completed' | 'errored'; type CreationStatus = null | 'pending' | 'completed' | 'errored';
export default function Carpools() { export default function Carpools() {
const { event } = useContext(EventContext); const { event, tentativeInvites, signups, setHasCarpool } =
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);
@ -106,6 +108,10 @@ export default function Carpools() {
const alreadyInCarpool = const alreadyInCarpool =
myCarpool !== undefined || creationStatus === 'completed'; myCarpool !== undefined || creationStatus === 'completed';
useEffect(() => {
setHasCarpool(alreadyInCarpool);
}, [alreadyInCarpool, setHasCarpool]);
const createEmptyCarpool = useCallback(() => { const createEmptyCarpool = useCallback(() => {
setCreationStatus('pending'); setCreationStatus('pending');
@ -119,6 +125,55 @@ export default function Carpools() {
}); });
}, [event.id, me.name]); }, [event.id, me.name]);
const tentativeInviteNames = useMemo(() => {
if (!signups) return [];
const names = tentativeInvites.map((id) => {
const signup = signups.find((s) => s.user.id === id);
return signup?.user.name;
});
const nonNull = names.filter((n) => n != null);
return nonNull.toArray() as string[];
}, [tentativeInvites, signups]);
let createCarpoolSection;
if (tentativeInviteNames.length > 0) {
const inviteeCount = tentativeInviteNames.length;
const peoplePlural = inviteeCount > 1 ? 'People' : 'Person';
createCarpoolSection = (
<>
<br />
<b>You have invited these people to carpool with you:</b>
{tentativeInviteNames.join(',')}
<UIButton
onClick={createEmptyCarpool}
style={{ backgroundColor: lightgrey }}
>
{creationStatus === null
? `Create Carpool With ${inviteeCount} ${peoplePlural}`
: creationStatus === 'pending'
? 'Creating...'
: 'Errored'}
</UIButton>
</>
);
} else
createCarpoolSection = (
<>
<span>Available to drive?</span>
<UIButton
onClick={createEmptyCarpool}
style={{ backgroundColor: lightgrey }}
>
{creationStatus === null
? 'Create Empty Carpool'
: creationStatus === 'pending'
? 'Creating...'
: 'Errored'}
</UIButton>
</>
);
return ( return (
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBottom: '0' }}>Carpools</h3> <h3 style={{ marginBottom: '0' }}>Carpools</h3>
@ -133,19 +188,7 @@ export default function Carpools() {
<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={createEmptyCarpool}
style={{ backgroundColor: lightgrey }}
>
{creationStatus === null
? 'Create Empty Carpool'
: creationStatus === 'pending'
? 'Creating...'
: 'Errored'}
</UIButton>
</>
)} )}
{event.carpools.map((carpool) => ( {event.carpools.map((carpool) => (
<CarpoolRow <CarpoolRow

View File

@ -1,5 +1,6 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { IEvent } from '../types'; import { IEvent, IEventSignup } from '../types';
import * as immutable from 'immutable';
const EventContext = createContext({ const EventContext = createContext({
refresh: () => { refresh: () => {
@ -7,6 +8,18 @@ const EventContext = createContext({
}, },
event: null! as IEvent, event: null! as IEvent,
default: true, default: true,
signups: null as IEventSignup[] | null,
addTentativeInvite: (id: number) => {
console.error('not implemented: addTentativeInvite');
},
removeTentativeInvite: (id: number) => {
console.error('not implemented: removeTentativeInvite');
},
tentativeInvites: immutable.Set<number>(),
hasCarpool: false,
setHasCarpool: (has: boolean) => {
console.error('not implemented: setHasCarpool');
},
}); });
export default EventContext; export default EventContext;

View File

@ -1,24 +1,124 @@
import CancelIcon from '@material-ui/icons/Cancel';
import PersonAddIcon from '@material-ui/icons/PersonAdd'; import PersonAddIcon from '@material-ui/icons/PersonAdd';
import { useMe } from '../hooks'; 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 { IEventSignup, IEvent } from '../types'; import { useMe } from '../hooks';
import { IEventSignup } from '../types';
import usePlace from '../usePlace'; import usePlace from '../usePlace';
import { useMemo } from 'react'; import EventContext from './EventContext';
function EventSignup({
signup,
locationLatitude,
locationLongitude,
myPlaceDetails,
}: {
signup: IEventSignup;
locationLatitude: number;
locationLongitude: number;
myPlaceDetails: PlaceDetails | null;
}) {
const { user, latitude, longitude } = signup;
const me = useMe();
const {
addTentativeInvite,
removeTentativeInvite,
tentativeInvites,
hasCarpool,
} = useContext(EventContext);
let 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;
} else {
return null;
}
}, [
latitude,
longitude,
locationLatitude,
locationLongitude,
myPlaceDetails,
]);
const isTentativelyInvited = useMemo(
() => tentativeInvites.has(signup.user.id),
[signup.user.id, tentativeInvites]
);
if (user.id === me?.id) {
return null;
}
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
position: 'relative',
padding: '1rem',
borderRadius: '0.5rem',
border: '1px solid #e0e0e0',
marginTop: '0.5rem',
marginBottom: '0.5rem',
}}
key={user.id}
>
<b>{user.name}</b>
{extraDistance ? `: +${extraDistance.toFixed(1)} miles` : ''}
{!hasCarpool &&
(isTentativelyInvited ? (
<CancelIcon
onClick={() => removeTentativeInvite(user.id)}
style={{ cursor: 'pointer' }}
/>
) : (
<PersonAddIcon
onClick={() => addTentativeInvite(user.id)}
style={{ cursor: 'pointer' }}
/>
))}
</div>
);
}
export default function EventSignups({ export default function EventSignups({
event,
signups, signups,
myPlaceId, myPlaceId,
}: { }: {
event: IEvent;
signups: IEventSignup[]; signups: IEventSignup[];
myPlaceId: string | null; myPlaceId: string | null;
}) { }) {
const { event } = useContext(EventContext);
const carpools = event.carpools; const carpools = event.carpools;
const placeDetails = usePlace(myPlaceId); const myPlaceDetails = usePlace(myPlaceId);
const locationLongitude = event.latitude;
const locationLatitude = event.longitude;
const me = useMe();
const signupsWithoutCarpool = useMemo(() => { const signupsWithoutCarpool = useMemo(() => {
// A list of users not in any carpool // A list of users not in any carpool
@ -31,67 +131,15 @@ export default function EventSignups({
return ( return (
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBlockEnd: '0' }}>People without a carpool</h3> <h3 style={{ marginBlockEnd: '0' }}>People without a carpool</h3>
{signupsWithoutCarpool.map(({ latitude, longitude, user }) => { {signupsWithoutCarpool.map((signup) => (
if (user.id === me?.id) { <EventSignup
return null; key={signup.user.id}
} signup={signup}
let extraDistance = null; myPlaceDetails={myPlaceDetails}
if ( locationLatitude={event.latitude}
placeDetails != null && locationLongitude={event.longitude}
!(latitude === null || longitude === null) />
) { ))}
const myLatitude = placeDetails.latitude;
const myLongitude = placeDetails.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
);
extraDistance = totalWithThem - totalWithoutThem;
}
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
position: 'relative',
padding: '1rem',
borderRadius: '0.5rem',
border: '1px solid #e0e0e0',
marginTop: '0.5rem',
marginBottom: '0.5rem',
}}
key={user.id}
>
<b>{user.name}</b>
{extraDistance ? `: +${extraDistance.toFixed(1)} miles` : ''}
<PersonAddIcon
onClick={() => {
// Invite to carpool and create carpool
}}
/>
</div>
);
})}
</div> </div>
); );
} }