add event signups with notes

This commit is contained in:
Michael Fatemi 2021-08-16 22:38:02 -04:00
parent 69342ffe8e
commit 449308b2d6
8 changed files with 112 additions and 62 deletions

View File

@ -1,7 +1,7 @@
import { useContext, useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { Location } from '../../lib/estimateoptimalpath'; import { Location } from '../../lib/estimateoptimalpath';
import getDistance from '../../lib/getdistance'; import getDistance from '../../lib/getdistance';
import { IEventSignupComplete } from '../types'; import { IEventSignupWithLocation } from '../types';
import useOptimalPath from '../useOptimalPath'; import useOptimalPath from '../useOptimalPath';
import { CarpoolContext } from './Carpool'; import { CarpoolContext } from './Carpool';
import useSignups from './useSignups'; import useSignups from './useSignups';
@ -21,7 +21,7 @@ export default function CarpoolRouteEstimator() {
() => () =>
signups.filter( signups.filter(
(signup) => signup.latitude !== null (signup) => signup.latitude !== null
) as IEventSignupComplete[], ) as IEventSignupWithLocation[],
[signups] [signups]
); );

View File

@ -8,7 +8,7 @@ import {
useSendCarpoolRequest, useSendCarpoolRequest,
} from '../../state/Notifications/NotificationsHooks'; } from '../../state/Notifications/NotificationsHooks';
import { useMe } from '../hooks'; import { useMe } from '../hooks';
import { IEvent, IEventSignupComplete } from '../types'; import { IEvent, IEventSignupWithLocation } from '../types';
import useOptimalPath from '../useOptimalPath'; import useOptimalPath from '../useOptimalPath';
import EventContext from './EventContext'; import EventContext from './EventContext';
import { useCurrentEventSignup } from './EventHooks'; import { useCurrentEventSignup } from './EventHooks';
@ -33,7 +33,7 @@ function useMemberLocations(members: IEvent['carpools'][0]['members']) {
longitude: signup.longitude, longitude: signup.longitude,
}; };
}) })
.filter(Boolean) as IEventSignupComplete[], .filter(Boolean) as IEventSignupWithLocation[],
[members, signups] [members, signups]
); );
} }

View File

@ -1,10 +1,11 @@
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { green, lightgrey } from '../../lib/colors'; import { green, lightgrey } from '../../lib/colors';
import getPlaceDetails from '../../lib/getPlaceDetails'; import getPlaceDetails from '../../lib/getPlaceDetails';
import { addOrUpdateEventSignup, removeEventSignup } from '../api'; import { addOrUpdateEventSignup, removeEventSignup } from '../api';
import { useMe } from '../hooks'; import { useMe } from '../hooks';
import UIButton from '../UI/UIButton'; import UIButton from '../UI/UIButton';
import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete'; import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete';
import UITextInput from '../UI/UITextInput';
import EventCarpools from './EventCarpools'; import EventCarpools from './EventCarpools';
import { useMutableEvent } from './EventHooks'; import { useMutableEvent } from './EventHooks';
import EventSignups from './EventSignups'; import EventSignups from './EventSignups';
@ -14,6 +15,9 @@ export default function EventInterestForm() {
const me = useMe() || { id: 0, name: '' }; const me = useMe() || { id: 0, name: '' };
const placeIdRef = useRef<string | null>(null); const placeIdRef = useRef<string | null>(null);
const canDriveRef = useRef(false); const canDriveRef = useRef(false);
const [note, setNote] = useState('');
const [noteSaved, setNoteSaved] = useState(true);
const noteUpdateTimerRef = useRef<NodeJS.Timeout | null>(null);
{ {
const signup = event.signups[me.id]; const signup = event.signups[me.id];
@ -22,34 +26,48 @@ export default function EventInterestForm() {
placeIdRef.current = signup?.placeId ?? null; placeIdRef.current = signup?.placeId ?? null;
canDriveRef.current = signup?.canDrive ?? false; canDriveRef.current = signup?.canDrive ?? false;
}, [signup?.canDrive, signup?.placeId]); }, [signup?.canDrive, signup?.placeId]);
useEffect(() => {
setNote(signup?.note || '');
}, [signup?.note]);
} }
const updateSignup = useCallback(async () => { const updateSignup = useCallback(
const placeId = placeIdRef.current; async (note: string) => {
const canDrive = canDriveRef.current; const placeId = placeIdRef.current;
const canDrive = canDriveRef.current;
await addOrUpdateEventSignup(event.id, placeIdRef.current, canDrive); await addOrUpdateEventSignup(
event.id,
if (placeId) { placeIdRef.current,
const details = await getPlaceDetails(placeId);
event.signups[me.id] = {
user: { id: me.id, name: me.name },
placeId,
...details,
canDrive, canDrive,
}; note
} else { );
event.signups[me.id] = {
user: { id: me.id, name: me.name }, if (placeId) {
placeId: null, const details = await getPlaceDetails(placeId);
latitude: null,
longitude: null, event.signups[me.id] = {
formattedAddress: null, user: { id: me.id, name: me.name },
canDrive, placeId,
}; ...details,
} canDrive,
}, [event.id, event.signups, me.id, me.name]); note,
};
} else {
event.signups[me.id] = {
user: { id: me.id, name: me.name },
placeId: null,
latitude: null,
longitude: null,
formattedAddress: null,
canDrive,
note,
};
}
},
[event.id, event.signups, me.id, me.name]
);
const removeSignup = useCallback(async () => { const removeSignup = useCallback(async () => {
await removeEventSignup(event.id); await removeEventSignup(event.id);
@ -59,6 +77,20 @@ export default function EventInterestForm() {
} }
}, [event.id, event.signups, me.id]); }, [event.id, event.signups, me.id]);
const updateNote = useCallback(
(newNote: string) => {
setNote(newNote);
setNoteSaved(false);
if (noteUpdateTimerRef.current) {
clearTimeout(noteUpdateTimerRef.current);
}
noteUpdateTimerRef.current = setTimeout(() => {
updateSignup(newNote).then(() => setNoteSaved(true));
}, 1000);
},
[updateSignup]
);
const interested = !!event.signups[me.id]; const interested = !!event.signups[me.id];
const canDrive = !!event.signups[me.id]?.canDrive; const canDrive = !!event.signups[me.id]?.canDrive;
@ -72,7 +104,7 @@ export default function EventInterestForm() {
? () => removeSignup() ? () => removeSignup()
: () => { : () => {
placeIdRef.current = null; placeIdRef.current = null;
updateSignup(); updateSignup(note);
} }
} }
style={{ style={{
@ -91,7 +123,7 @@ export default function EventInterestForm() {
<UIButton <UIButton
onClick={() => { onClick={() => {
canDriveRef.current = !canDriveRef.current; canDriveRef.current = !canDriveRef.current;
updateSignup(); updateSignup(note);
}} }}
style={{ style={{
backgroundColor: canDrive ? green : lightgrey, backgroundColor: canDrive ? green : lightgrey,
@ -105,7 +137,7 @@ export default function EventInterestForm() {
placeholder="Pickup and dropoff location" placeholder="Pickup and dropoff location"
onSelected={(_address, placeId) => { onSelected={(_address, placeId) => {
placeIdRef.current = placeId; placeIdRef.current = placeId;
updateSignup(); updateSignup(note);
}} }}
style={ style={
event.signups[me.id]?.placeId != null event.signups[me.id]?.placeId != null
@ -115,6 +147,15 @@ export default function EventInterestForm() {
placeId={event.signups[me.id]?.placeId} placeId={event.signups[me.id]?.placeId}
/> />
<br /> <br />
<span style={{ fontSize: '0.875em' }}>
Note (e.g. "Monday, Tuesday, Wednesday")
</span>
<UITextInput
value={note}
onChangeText={updateNote}
style={noteSaved ? { border: '2px solid ' + green } : {}}
/>
<br />
<EventCarpools /> <EventCarpools />
{event.signups !== null && <EventSignups />} {event.signups !== null && <EventSignups />}
</> </>

View File

@ -1,4 +1,5 @@
import { CSSProperties, useCallback } from 'react'; import { forwardRef } from 'react';
import { CSSProperties, ForwardedRef, useCallback } from 'react';
const baseStyle = { const baseStyle = {
marginTop: '0.5em', marginTop: '0.5em',
@ -9,23 +10,27 @@ const baseStyle = {
border: '0px', border: '0px',
}; };
export default function UITextInput({ function UITextInput(
value, {
disabled = false, value,
onChangeText, disabled = false,
style, onChangeText,
}: { style,
value: string; }: {
disabled?: boolean; value?: string;
onChangeText: (text: string) => void; disabled?: boolean;
style?: CSSProperties; onChangeText?: (text: string) => void;
}) { style?: CSSProperties;
},
ref: ForwardedRef<HTMLInputElement>
) {
const onChange = useCallback( const onChange = useCallback(
(e) => onChangeText(e.target.value), (e) => onChangeText?.(e.target.value),
[onChangeText] [onChangeText]
); );
return ( return (
<input <input
ref={ref}
style={style ? { ...baseStyle, ...style } : baseStyle} style={style ? { ...baseStyle, ...style } : baseStyle}
value={value} value={value}
disabled={disabled} disabled={disabled}
@ -33,3 +38,5 @@ export default function UITextInput({
/> />
); );
} }
export default forwardRef(UITextInput);

View File

@ -75,9 +75,10 @@ export async function getEventSignups(
export async function addOrUpdateEventSignup( export async function addOrUpdateEventSignup(
eventId: number, eventId: number,
placeId: string | null, placeId: string | null,
canDrive: boolean canDrive: boolean,
note: string
) { ) {
await post(`/events/${eventId}/signup`, { placeId, canDrive }); await post(`/events/${eventId}/signup`, { placeId, canDrive, note });
} }
export async function removeEventSignup(eventId: number) { export async function removeEventSignup(eventId: number) {

View File

@ -92,24 +92,23 @@ export type IEvent = {
longitude: number; longitude: number;
}; };
export type IEventSignupComplete = { export type IEventSignupBase = {
user: { user: {
id: number; id: number;
name: string; name: string;
}; };
canDrive: boolean; canDrive: boolean;
note: string;
};
export type IEventSignupWithLocation = IEventSignupBase & {
placeId: string; placeId: string;
formattedAddress: string; formattedAddress: string;
latitude: number; latitude: number;
longitude: number; longitude: number;
}; };
export type IEventSignupIncomplete = { export type IEventSignupWithoutLocation = IEventSignupBase & {
user: {
id: number;
name: string;
};
canDrive: boolean;
placeId: null; placeId: null;
formattedAddress: null; formattedAddress: null;
latitude: null; latitude: null;
@ -120,7 +119,9 @@ export type IEventSignupIncomplete = {
* Model EventSignup * Model EventSignup
*/ */
export type IEventSignup = IEventSignupComplete | IEventSignupIncomplete; export type IEventSignup =
| IEventSignupWithLocation
| IEventSignupWithoutLocation;
export type IInvitation = { export type IInvitation = {
user: { user: {

View File

@ -1,9 +1,9 @@
import { useDebugValue, useMemo } from 'react'; import { useDebugValue, useMemo } from 'react';
import estimateOptimalPath, { Path } from '../lib/estimateoptimalpath'; import estimateOptimalPath, { Path } from '../lib/estimateoptimalpath';
import { ICarpool, IEventSignupComplete } from './types'; import { ICarpool, IEventSignupWithLocation } from './types';
export default function useOptimalPath( export default function useOptimalPath(
members: IEventSignupComplete[], members: IEventSignupWithLocation[],
destination: ICarpool['event'] destination: ICarpool['event']
) { ) {
const path = useMemo(() => { const path = useMemo(() => {
@ -38,7 +38,7 @@ export default function useOptimalPath(
} }
return prev; return prev;
}, null! as { path: Path<IEventSignupComplete, ICarpool['event']>; distance: number }); }, null! as { path: Path<IEventSignupWithLocation, ICarpool['event']>; distance: number });
return path; return path;
}, [destination, members]); }, [destination, members]);

View File

@ -1,4 +1,4 @@
import { ICarpool, IEventSignupComplete } from '../components/types'; import { ICarpool, IEventSignupWithLocation } from '../components/types';
import getDistance from './getdistance'; import getDistance from './getdistance';
export type Location = { export type Location = {
@ -13,9 +13,9 @@ export type Path<M extends Location, D extends Location> = {
}; };
export default function estimateOptimalPath( export default function estimateOptimalPath(
path: Path<IEventSignupComplete, ICarpool['event']> path: Path<IEventSignupWithLocation, ICarpool['event']>
): { ): {
path: Path<IEventSignupComplete, ICarpool['event']>; path: Path<IEventSignupWithLocation, ICarpool['event']>;
distance: number; distance: number;
} { } {
const { from, to, waypoints } = path; const { from, to, waypoints } = path;
@ -49,7 +49,7 @@ export default function estimateOptimalPath(
path: { path: {
from, from,
to, to,
waypoints: newWaypoints as IEventSignupComplete[], waypoints: newWaypoints as IEventSignupWithLocation[],
}, },
distance: getDistance(from, ...sequence, to), distance: getDistance(from, ...sequence, to),
}; };