mirror of
https://github.com/myfatemi04/wheelshare-frontend.git
synced 2025-04-21 11:20:17 -04:00
feature: creating carpools with invitees, pt.1
This commit is contained in:
parent
7de92f4773
commit
8b8ef46b3f
|
@ -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}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user