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

View File

@ -1,6 +1,7 @@
import CancelIcon from '@material-ui/icons/Cancel';
import CheckIcon from '@material-ui/icons/Check';
import EmojiPeopleIcon from '@material-ui/icons/EmojiPeople';
import { useEffect } from 'react';
import { useCallback, useContext, useMemo, useState } from 'react';
import { lightgrey } from '../../lib/colors';
import {
@ -91,7 +92,8 @@ function CarpoolRow({
type CreationStatus = null | 'pending' | 'completed' | 'errored';
export default function Carpools() {
const { event } = useContext(EventContext);
const { event, tentativeInvites, signups, setHasCarpool } =
useContext(EventContext);
const [creationStatus, setCreationStatus] = useState<CreationStatus>(null);
const [createdCarpoolId, setCreatedCarpoolId] = useState<null | number>(null);
@ -106,6 +108,10 @@ export default function Carpools() {
const alreadyInCarpool =
myCarpool !== undefined || creationStatus === 'completed';
useEffect(() => {
setHasCarpool(alreadyInCarpool);
}, [alreadyInCarpool, setHasCarpool]);
const createEmptyCarpool = useCallback(() => {
setCreationStatus('pending');
@ -119,6 +125,55 @@ export default function Carpools() {
});
}, [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 (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBottom: '0' }}>Carpools</h3>
@ -133,19 +188,7 @@ export default function Carpools() {
<UILink href={`/carpools/${myCarpool.id}`}>{myCarpool.name}</UILink>
</span>
) : (
<>
<span>Available to drive?</span>
<UIButton
onClick={createEmptyCarpool}
style={{ backgroundColor: lightgrey }}
>
{creationStatus === null
? 'Create Empty Carpool'
: creationStatus === 'pending'
? 'Creating...'
: 'Errored'}
</UIButton>
</>
createCarpoolSection
)}
{event.carpools.map((carpool) => (
<CarpoolRow

View File

@ -1,5 +1,6 @@
import { createContext } from 'react';
import { IEvent } from '../types';
import { IEvent, IEventSignup } from '../types';
import * as immutable from 'immutable';
const EventContext = createContext({
refresh: () => {
@ -7,6 +8,18 @@ const EventContext = createContext({
},
event: null! as IEvent,
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;

View File

@ -1,47 +1,37 @@
import CancelIcon from '@material-ui/icons/Cancel';
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 { IEventSignup, IEvent } from '../types';
import { useMe } from '../hooks';
import { IEventSignup } from '../types';
import usePlace from '../usePlace';
import { useMemo } from 'react';
import EventContext from './EventContext';
export default function EventSignups({
event,
signups,
myPlaceId,
function EventSignup({
signup,
locationLatitude,
locationLongitude,
myPlaceDetails,
}: {
event: IEvent;
signups: IEventSignup[];
myPlaceId: string | null;
signup: IEventSignup;
locationLatitude: number;
locationLongitude: number;
myPlaceDetails: PlaceDetails | null;
}) {
const carpools = event.carpools;
const placeDetails = usePlace(myPlaceId);
const locationLongitude = event.latitude;
const locationLatitude = event.longitude;
const { user, latitude, longitude } = signup;
const me = useMe();
const {
addTentativeInvite,
removeTentativeInvite,
tentativeInvites,
hasCarpool,
} = useContext(EventContext);
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(({ 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;
let extraDistance = useMemo(() => {
if (myPlaceDetails != null && !(latitude === null || longitude === null)) {
const myLatitude = myPlaceDetails.latitude;
const myLongitude = myPlaceDetails.longitude;
const meToThem = latlongdist(
latitude,
longitude,
@ -64,7 +54,25 @@ export default function EventSignups({
myLongitude,
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 (
@ -84,14 +92,54 @@ export default function EventSignups({
>
<b>{user.name}</b>
{extraDistance ? `: +${extraDistance.toFixed(1)} miles` : ''}
<PersonAddIcon
onClick={() => {
// Invite to carpool and create carpool
}}
{!hasCarpool &&
(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>
);
}