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,47 +1,37 @@
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';
export default function EventSignups({ function EventSignup({
event, signup,
signups, locationLatitude,
myPlaceId, locationLongitude,
myPlaceDetails,
}: { }: {
event: IEvent; signup: IEventSignup;
signups: IEventSignup[]; locationLatitude: number;
myPlaceId: string | null; locationLongitude: number;
myPlaceDetails: PlaceDetails | null;
}) { }) {
const carpools = event.carpools; const { user, latitude, longitude } = signup;
const placeDetails = usePlace(myPlaceId);
const locationLongitude = event.latitude;
const locationLatitude = event.longitude;
const me = useMe(); const me = useMe();
const {
addTentativeInvite,
removeTentativeInvite,
tentativeInvites,
hasCarpool,
} = useContext(EventContext);
const signupsWithoutCarpool = useMemo(() => { let extraDistance = useMemo(() => {
// A list of users not in any carpool if (myPlaceDetails != null && !(latitude === null || longitude === null)) {
const members = carpools.map((c) => c.members); const myLatitude = myPlaceDetails.latitude;
const allMembers = members.reduce((a, b) => a.concat(b), []); const myLongitude = myPlaceDetails.longitude;
const allMembersIds = allMembers.map((m) => m.id);
return signups.filter((s) => !allMembersIds.includes(s.user.id));
}, [signups, carpools]);
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBlockEnd: '0' }}>People without a carpool</h3>
{signupsWithoutCarpool.map(({ latitude, longitude, user }) => {
if (user.id === me?.id) {
return null;
}
let extraDistance = null;
if (
placeDetails != null &&
!(latitude === null || longitude === null)
) {
const myLatitude = placeDetails.latitude;
const myLongitude = placeDetails.longitude;
const meToThem = latlongdist( const meToThem = latlongdist(
latitude, latitude,
longitude, longitude,
@ -64,7 +54,25 @@ export default function EventSignups({
myLongitude, myLongitude,
R_miles R_miles
); );
extraDistance = totalWithThem - totalWithoutThem; 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 ( return (
@ -84,14 +92,54 @@ export default function EventSignups({
> >
<b>{user.name}</b> <b>{user.name}</b>
{extraDistance ? `: +${extraDistance.toFixed(1)} miles` : ''} {extraDistance ? `: +${extraDistance.toFixed(1)} miles` : ''}
<PersonAddIcon
onClick={() => { {!hasCarpool &&
// Invite to carpool and create carpool (isTentativelyInvited ? (
}} <CancelIcon
onClick={() => removeTentativeInvite(user.id)}
style={{ cursor: 'pointer' }}
/> />
</div> ) : (
); <PersonAddIcon
})} onClick={() => addTentativeInvite(user.id)}
style={{ cursor: 'pointer' }}
/>
))}
</div>
);
}
export default function EventSignups({
signups,
myPlaceId,
}: {
signups: IEventSignup[];
myPlaceId: string | null;
}) {
const { event } = useContext(EventContext);
const carpools = event.carpools;
const myPlaceDetails = usePlace(myPlaceId);
const signupsWithoutCarpool = useMemo(() => {
// A list of users not in any carpool
const members = carpools.map((c) => c.members);
const allMembers = members.reduce((a, b) => a.concat(b), []);
const allMembersIds = allMembers.map((m) => m.id);
return signups.filter((s) => !allMembersIds.includes(s.user.id));
}, [signups, carpools]);
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBlockEnd: '0' }}>People without a carpool</h3>
{signupsWithoutCarpool.map((signup) => (
<EventSignup
key={signup.user.id}
signup={signup}
myPlaceDetails={myPlaceDetails}
locationLatitude={event.latitude}
locationLongitude={event.longitude}
/>
))}
</div> </div>
); );
} }