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

View File

@ -8,7 +8,7 @@ import {
useSendCarpoolRequest,
} from '../../state/Notifications/NotificationsHooks';
import { useMe } from '../hooks';
import { IEvent, IEventSignupComplete } from '../types';
import { IEvent, IEventSignupWithLocation } from '../types';
import useOptimalPath from '../useOptimalPath';
import EventContext from './EventContext';
import { useCurrentEventSignup } from './EventHooks';
@ -33,7 +33,7 @@ function useMemberLocations(members: IEvent['carpools'][0]['members']) {
longitude: signup.longitude,
};
})
.filter(Boolean) as IEventSignupComplete[],
.filter(Boolean) as IEventSignupWithLocation[],
[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 getPlaceDetails from '../../lib/getPlaceDetails';
import { addOrUpdateEventSignup, removeEventSignup } from '../api';
import { useMe } from '../hooks';
import UIButton from '../UI/UIButton';
import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete';
import UITextInput from '../UI/UITextInput';
import EventCarpools from './EventCarpools';
import { useMutableEvent } from './EventHooks';
import EventSignups from './EventSignups';
@ -14,6 +15,9 @@ export default function EventInterestForm() {
const me = useMe() || { id: 0, name: '' };
const placeIdRef = useRef<string | null>(null);
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];
@ -22,34 +26,48 @@ export default function EventInterestForm() {
placeIdRef.current = signup?.placeId ?? null;
canDriveRef.current = signup?.canDrive ?? false;
}, [signup?.canDrive, signup?.placeId]);
useEffect(() => {
setNote(signup?.note || '');
}, [signup?.note]);
}
const updateSignup = useCallback(async () => {
const placeId = placeIdRef.current;
const canDrive = canDriveRef.current;
const updateSignup = useCallback(
async (note: string) => {
const placeId = placeIdRef.current;
const canDrive = canDriveRef.current;
await addOrUpdateEventSignup(event.id, placeIdRef.current, canDrive);
if (placeId) {
const details = await getPlaceDetails(placeId);
event.signups[me.id] = {
user: { id: me.id, name: me.name },
placeId,
...details,
await addOrUpdateEventSignup(
event.id,
placeIdRef.current,
canDrive,
};
} else {
event.signups[me.id] = {
user: { id: me.id, name: me.name },
placeId: null,
latitude: null,
longitude: null,
formattedAddress: null,
canDrive,
};
}
}, [event.id, event.signups, me.id, me.name]);
note
);
if (placeId) {
const details = await getPlaceDetails(placeId);
event.signups[me.id] = {
user: { id: me.id, name: me.name },
placeId,
...details,
canDrive,
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 () => {
await removeEventSignup(event.id);
@ -59,6 +77,20 @@ export default function EventInterestForm() {
}
}, [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 canDrive = !!event.signups[me.id]?.canDrive;
@ -72,7 +104,7 @@ export default function EventInterestForm() {
? () => removeSignup()
: () => {
placeIdRef.current = null;
updateSignup();
updateSignup(note);
}
}
style={{
@ -91,7 +123,7 @@ export default function EventInterestForm() {
<UIButton
onClick={() => {
canDriveRef.current = !canDriveRef.current;
updateSignup();
updateSignup(note);
}}
style={{
backgroundColor: canDrive ? green : lightgrey,
@ -105,7 +137,7 @@ export default function EventInterestForm() {
placeholder="Pickup and dropoff location"
onSelected={(_address, placeId) => {
placeIdRef.current = placeId;
updateSignup();
updateSignup(note);
}}
style={
event.signups[me.id]?.placeId != null
@ -115,6 +147,15 @@ export default function EventInterestForm() {
placeId={event.signups[me.id]?.placeId}
/>
<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 />
{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 = {
marginTop: '0.5em',
@ -9,23 +10,27 @@ const baseStyle = {
border: '0px',
};
export default function UITextInput({
value,
disabled = false,
onChangeText,
style,
}: {
value: string;
disabled?: boolean;
onChangeText: (text: string) => void;
style?: CSSProperties;
}) {
function UITextInput(
{
value,
disabled = false,
onChangeText,
style,
}: {
value?: string;
disabled?: boolean;
onChangeText?: (text: string) => void;
style?: CSSProperties;
},
ref: ForwardedRef<HTMLInputElement>
) {
const onChange = useCallback(
(e) => onChangeText(e.target.value),
(e) => onChangeText?.(e.target.value),
[onChangeText]
);
return (
<input
ref={ref}
style={style ? { ...baseStyle, ...style } : baseStyle}
value={value}
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(
eventId: number,
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) {

View File

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

View File

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

View File

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