From 3fc96ca3d46f84734e94781dd566397cf36fc687 Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Wed, 30 Jun 2021 10:18:27 -0400 Subject: [PATCH 1/8] Add days of week picker and binary encoding --- src/components/NewUI/Event.tsx | 26 ++++---- src/components/NewUI/EventCreator.tsx | 85 ++++++++++++++++++++++++++- src/components/NewUI/bits.ts | 23 ++++++++ src/components/NewUI/colors.ts | 2 + 4 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 src/components/NewUI/bits.ts create mode 100644 src/components/NewUI/colors.ts diff --git a/src/components/NewUI/Event.tsx b/src/components/NewUI/Event.tsx index b6804cc..e5c632a 100644 --- a/src/components/NewUI/Event.tsx +++ b/src/components/NewUI/Event.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { green, lightgrey } from './colors'; import latlongdist, { R_miles } from './latlongdist'; import UIButton from './UIButton'; import UIPlacesAutocomplete from './UIPlacesAutocomplete'; @@ -8,9 +9,6 @@ import usePlace from './usePlace'; import useThrottle from './useThrottle'; import useToggle from './useToggle'; -const green = '#60f760'; -const lightgrey = '#e0e0e0'; - export type IEvent = { id: number; name: string; @@ -18,6 +16,8 @@ export type IEvent = { formattedAddress: string; startTime: string; endTime: string; + latitude: number; + longitude: number; }; function formatStartAndEndTime( @@ -211,8 +211,8 @@ function People({ event, placeId }: { event: IEvent; placeId: string }) { // eslint-disable-next-line const [people, setPeople] = useState(dummyPeopleData); const placeDetails = usePlace(placeId); - const myLatitude = 10; - const myLongitude = 10; + const locationLongitude = event.latitude; + const locationLatitude = event.longitude; return (
@@ -220,28 +220,28 @@ function People({ event, placeId }: { event: IEvent; placeId: string }) { {people.map(({ name, latitude, longitude, id }) => { let extraDistance = null; if (placeDetails != null) { - const locationLatitude = placeDetails.latitude; - const locationLongitude = placeDetails.longitude; + const myLatitude = placeDetails.latitude; + const myLongitude = placeDetails.longitude; const meToThem = latlongdist( latitude, longitude, - myLatitude, - myLongitude, + locationLongitude, + locationLatitude, R_miles ); const themToLocation = latlongdist( latitude, longitude, - locationLatitude, - locationLongitude, + myLatitude, + myLongitude, R_miles ); const totalWithThem = meToThem + themToLocation; const totalWithoutThem = latlongdist( + locationLongitude, + locationLatitude, myLatitude, myLongitude, - locationLatitude, - locationLongitude, R_miles ); extraDistance = totalWithThem - totalWithoutThem; diff --git a/src/components/NewUI/EventCreator.tsx b/src/components/NewUI/EventCreator.tsx index 6a2f6af..080c6eb 100644 --- a/src/components/NewUI/EventCreator.tsx +++ b/src/components/NewUI/EventCreator.tsx @@ -1,14 +1,81 @@ -import { useCallback, useState } from 'react'; +import { Dispatch, SetStateAction, useCallback, useState } from 'react'; import { post } from './api'; +import { toggleBit } from './bits'; +import { green, lightgrey } from './colors'; import { IGroup } from './Group'; import UIButton from './UIButton'; import UIDatetimeInput from './UIDatetimeInput'; import UIPlacesAutocomplete from './UIPlacesAutocomplete'; import UISecondaryBox from './UISecondaryBox'; import UITextInput from './UITextInput'; +import useToggle from './useToggle'; const noop = () => {}; +const DAY_NAMES = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', +]; + +function DaysOfWeekSelector({ + daysOfWeek, + update, +}: { + daysOfWeek: number; + update: Dispatch>; +}) { + const toggleDayOfWeek = useCallback( + function (idx: 1 | 2 | 3 | 4 | 5 | 6 | 7) { + update((daysOfWeek) => toggleBit(daysOfWeek, idx)); + }, + [update] + ); + + return ( +
+ {DAY_NAMES.map((name, idx) => { + const mask = 0b1000_0000 >> (idx + 1); + const active = (daysOfWeek & mask) !== 0; + return ( +
+ // @ts-ignore + toggleDayOfWeek(idx + 1) + } + > + {name.charAt(0)} +
+ ); + })} +
+ ); +} + export default function EventCreator({ group }: { group: IGroup }) { const [name, setName] = useState(''); const [startTime, setStartTime] = useState(null); @@ -17,6 +84,9 @@ export default function EventCreator({ group }: { group: IGroup }) { const [creating, setCreating] = useState(false); const [createdEventId, setCreatedEventId] = useState(-1); + const [recurring, toggleRecurring] = useToggle(false); + const [daysOfWeek, setDaysOfWeek] = useState(0); + const buttonEnabled = name.length > 0 && startTime != null && @@ -63,6 +133,19 @@ export default function EventCreator({ group }: { group: IGroup }) { setPlaceId(placeId); }} /> + + Recurring event + + {recurring && ( + + )} {createdEventId === -1 ? ( 7 || !isFinite(idx)) { + throw new Error('invalid idx. idx must be from 1 - 7.'); + } + + const mask = 0b1000_0000 >> idx; + + if (active) { + return n | mask; + } else { + return n & ~mask; + } +} + +export function toggleBit(n: number, idx: number) { + if (idx < 1 || idx > 7 || !isFinite(idx)) { + throw new Error('invalid idx. idx must be from 1 - 7.'); + } + + const mask = 0b1000_0000 >> idx; + + return n ^ mask; +} diff --git a/src/components/NewUI/colors.ts b/src/components/NewUI/colors.ts new file mode 100644 index 0000000..c69ddb8 --- /dev/null +++ b/src/components/NewUI/colors.ts @@ -0,0 +1,2 @@ +export const green = '#60f760'; +export const lightgrey = '#e0e0e0'; From c0a00945ef60acf3ac193a29eb6770f4abdb58de Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Wed, 30 Jun 2021 20:52:24 -0400 Subject: [PATCH 2/8] event creator includes recurring events! --- src/components/NewUI/EventCreator.tsx | 11 ++++++- src/components/NewUI/UIDateInput.tsx | 44 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/components/NewUI/UIDateInput.tsx diff --git a/src/components/NewUI/EventCreator.tsx b/src/components/NewUI/EventCreator.tsx index 080c6eb..6b8d0c3 100644 --- a/src/components/NewUI/EventCreator.tsx +++ b/src/components/NewUI/EventCreator.tsx @@ -4,6 +4,7 @@ import { toggleBit } from './bits'; import { green, lightgrey } from './colors'; import { IGroup } from './Group'; import UIButton from './UIButton'; +import UIDateInput from './UIDateInput'; import UIDatetimeInput from './UIDatetimeInput'; import UIPlacesAutocomplete from './UIPlacesAutocomplete'; import UISecondaryBox from './UISecondaryBox'; @@ -86,12 +87,14 @@ export default function EventCreator({ group }: { group: IGroup }) { const [recurring, toggleRecurring] = useToggle(false); const [daysOfWeek, setDaysOfWeek] = useState(0); + const [endDate, setEndDate] = useState(null); const buttonEnabled = name.length > 0 && startTime != null && endTime != null && placeId != null && + (!recurring || daysOfWeek || endDate !== null) && !creating; const createEvent = useCallback(() => { @@ -139,12 +142,18 @@ export default function EventCreator({ group }: { group: IGroup }) { backgroundColor: recurring ? green : lightgrey, color: recurring ? 'white' : 'black', transition: 'color 0.2s, background-color 0.2s', + marginBottom: '1.5rem', }} > Recurring event {recurring && ( - + <> + Days of week + + Date of last occurence + + )} {createdEventId === -1 ? ( void; + disabled?: boolean; +}) { + const onChange = useCallback( + (e) => { + // YYYY-MM-DD or "" (empty string) + const dateAsString = e.target.value as string; + if (typeof dateAsString !== 'string' || dateAsString.length === 0) { + onChangedDate(null); + } + const year = dateAsString.slice(0, 4); + const month = dateAsString.slice(5, 7); + const day = dateAsString.slice(8, 10); + + // Midnight in the timezone of the user + const date = new Date(`${year}-${month}-${day}T00:00:00`); + onChangedDate(date); + }, + [onChangedDate] + ); + return ( + + ); +} From 66b5c8aff692e4a4497f0ed7aee435f30af495ff Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Wed, 30 Jun 2021 21:29:05 -0400 Subject: [PATCH 3/8] update api call for creating an event --- src/components/NewUI/EventCreator.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/NewUI/EventCreator.tsx b/src/components/NewUI/EventCreator.tsx index 6b8d0c3..ca17144 100644 --- a/src/components/NewUI/EventCreator.tsx +++ b/src/components/NewUI/EventCreator.tsx @@ -106,6 +106,9 @@ export default function EventCreator({ group }: { group: IGroup }) { endTime, groupId: group.id, placeId, + recurring, + daysOfWeek, + endDate, }) .then((response) => response.json()) .then(({ id }) => { @@ -113,7 +116,17 @@ export default function EventCreator({ group }: { group: IGroup }) { }) .finally(() => setCreating(false)); } - }, [creating, name, startTime, endTime, group.id, placeId]); + }, [ + creating, + name, + startTime, + endTime, + group.id, + placeId, + recurring, + daysOfWeek, + endDate, + ]); return ( From 7dc5e92c46aea2c573bea0563b41d7d149790918 Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Thu, 1 Jul 2021 00:25:48 -0400 Subject: [PATCH 4/8] refactor event creation api --- src/components/NewUI/EventCreator.tsx | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/NewUI/EventCreator.tsx b/src/components/NewUI/EventCreator.tsx index ca17144..bf7ef17 100644 --- a/src/components/NewUI/EventCreator.tsx +++ b/src/components/NewUI/EventCreator.tsx @@ -26,9 +26,11 @@ const DAY_NAMES = [ function DaysOfWeekSelector({ daysOfWeek, update, + disabled = false, }: { daysOfWeek: number; update: Dispatch>; + disabled?: boolean; }) { const toggleDayOfWeek = useCallback( function (idx: 1 | 2 | 3 | 4 | 5 | 6 | 7) { @@ -53,7 +55,11 @@ function DaysOfWeekSelector({ style={{ borderRadius: '100%', cursor: 'pointer', - backgroundColor: active ? green : lightgrey, + backgroundColor: active + ? disabled + ? green + '77' + : green + : lightgrey, color: active ? 'white' : 'black', userSelect: 'none', width: '2em', @@ -68,6 +74,7 @@ function DaysOfWeekSelector({ // @ts-ignore toggleDayOfWeek(idx + 1) } + key={name} > {name.charAt(0)}
@@ -99,16 +106,26 @@ export default function EventCreator({ group }: { group: IGroup }) { const createEvent = useCallback(() => { if (!creating) { + if (startTime === null) { + console.warn( + 'Tried to create an event where the start time was unspecified.' + ); + return; + } + + const duration = + endTime !== null ? (endTime.getTime() - startTime.getTime()) / 60 : 0; + setCreating(true); + post('/events', { name, startTime, - endTime, + duration, + endDate, groupId: group.id, placeId, - recurring, daysOfWeek, - endDate, }) .then((response) => response.json()) .then(({ id }) => { @@ -123,7 +140,6 @@ export default function EventCreator({ group }: { group: IGroup }) { endTime, group.id, placeId, - recurring, daysOfWeek, endDate, ]); From f17feac49747fb6e4a17ffa303f215f509366efb Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Thu, 1 Jul 2021 00:33:18 -0400 Subject: [PATCH 5/8] disable day of week selector when event is being created --- src/components/NewUI/EventCreator.tsx | 30 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/NewUI/EventCreator.tsx b/src/components/NewUI/EventCreator.tsx index bf7ef17..500ecbf 100644 --- a/src/components/NewUI/EventCreator.tsx +++ b/src/components/NewUI/EventCreator.tsx @@ -57,10 +57,18 @@ function DaysOfWeekSelector({ cursor: 'pointer', backgroundColor: active ? disabled - ? green + '77' + ? // lighter version of green + 'rgba(96, 247, 96, 0.5)' : green + : disabled + ? // lighter version of lightgrey + 'rgba(224, 224, 224, 0.5)' : lightgrey, - color: active ? 'white' : 'black', + color: active + ? 'white' + : disabled + ? 'rgba(0, 0, 0, 0.5)' + : 'black', userSelect: 'none', width: '2em', height: '2em', @@ -70,10 +78,14 @@ function DaysOfWeekSelector({ alignItems: 'center', justifyContent: 'center', }} - onClick={() => - // @ts-ignore - toggleDayOfWeek(idx + 1) - } + onClick={() => { + if (!disabled) { + toggleDayOfWeek( + // @ts-ignore + idx + 1 + ); + } + }} key={name} > {name.charAt(0)} @@ -179,7 +191,11 @@ export default function EventCreator({ group }: { group: IGroup }) { {recurring && ( <> Days of week - + Date of last occurence From 305c738e500bd0ca7c149d42bc43e9d4421e4591 Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Thu, 1 Jul 2021 00:34:18 -0400 Subject: [PATCH 6/8] disable end date selector when creating event --- src/components/NewUI/EventCreator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NewUI/EventCreator.tsx b/src/components/NewUI/EventCreator.tsx index 500ecbf..21244d4 100644 --- a/src/components/NewUI/EventCreator.tsx +++ b/src/components/NewUI/EventCreator.tsx @@ -197,7 +197,7 @@ export default function EventCreator({ group }: { group: IGroup }) { disabled={creating} /> Date of last occurence - + )} {createdEventId === -1 ? ( From cf3a84d1c540f9a7cbe4d5aa81a2a8a7db50ede2 Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Thu, 1 Jul 2021 00:39:06 -0400 Subject: [PATCH 7/8] add start time and end time client-side validation --- src/components/NewUI/EventCreator.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/NewUI/EventCreator.tsx b/src/components/NewUI/EventCreator.tsx index 21244d4..2b1a2f0 100644 --- a/src/components/NewUI/EventCreator.tsx +++ b/src/components/NewUI/EventCreator.tsx @@ -108,12 +108,16 @@ export default function EventCreator({ group }: { group: IGroup }) { const [daysOfWeek, setDaysOfWeek] = useState(0); const [endDate, setEndDate] = useState(null); + const durationIsNegative = + endTime && startTime && endTime.getTime() < startTime.getTime(); + const buttonEnabled = name.length > 0 && startTime != null && endTime != null && placeId != null && (!recurring || daysOfWeek || endDate !== null) && + !durationIsNegative && !creating; const createEvent = useCallback(() => { @@ -200,6 +204,11 @@ export default function EventCreator({ group }: { group: IGroup }) { )} + {durationIsNegative && ( + + The start time can't be after the end time. + + )} {createdEventId === -1 ? ( Date: Thu, 1 Jul 2021 13:06:33 -0400 Subject: [PATCH 8/8] make frontend update signup in backend --- src/components/NewUI/Event.tsx | 45 +++++++++++++++++++++++++++++--- src/components/NewUI/api.ts | 9 +++++++ src/components/NewUI/usePlace.ts | 2 +- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/components/NewUI/Event.tsx b/src/components/NewUI/Event.tsx index e5c632a..3caef51 100644 --- a/src/components/NewUI/Event.tsx +++ b/src/components/NewUI/Event.tsx @@ -1,4 +1,5 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { post } from './api'; import { green, lightgrey } from './colors'; import latlongdist, { R_miles } from './latlongdist'; import UIButton from './UIButton'; @@ -206,7 +207,7 @@ const dummyPeopleData: IPerson[] = [ longitude: 10.12, }, ]; -function People({ event, placeId }: { event: IEvent; placeId: string }) { +function People({ event, placeId }: { event: IEvent; placeId: string | null }) { const PADDING = '1rem'; // eslint-disable-next-line const [people, setPeople] = useState(dummyPeopleData); @@ -285,9 +286,47 @@ function People({ event, placeId }: { event: IEvent; placeId: string }) { export default function Event({ event }: { event: IEvent }) { const { name, group, formattedAddress, startTime, endTime } = event; const [haveRide, toggleHaveRide] = useToggle(false); - const [placeId, setPlaceId] = useState(null!); + const [placeId, setPlaceId] = useState(null); const [interested, toggleInterested] = useToggle(false); + const [updating, setUpdating] = useState(false); const toggleInterestedThrottled = useThrottle(toggleInterested, 500); + const existingSignup = useRef({ + interested: false, + placeId: null as string | null, + eventId: null as number | null, + }); + + useEffect(() => { + const prev = existingSignup.current; + if (prev.interested === false && interested === false) { + return; + } + + if ( + (prev.interested === true && interested === false) || + (interested === true && prev.placeId !== null && placeId === null) + ) { + fetch(`http://localhost:5000/api/events/${event.id}/signup`, { + method: 'delete', + }).finally(() => setUpdating(false)); + prev.interested = false; + return; + } + + if ( + interested === true && + (prev.placeId !== placeId || prev.eventId !== event.id) + ) { + prev.placeId = placeId; + prev.eventId = event.id; + prev.interested = true; + + post(`/events/${event.id}/signup`, { + placeId, + }).finally(() => setUpdating(false)); + return; + } + }, [event.id, interested, placeId, updating]); return ( diff --git a/src/components/NewUI/api.ts b/src/components/NewUI/api.ts index 81168bf..10f5123 100644 --- a/src/components/NewUI/api.ts +++ b/src/components/NewUI/api.ts @@ -7,3 +7,12 @@ export function post(path: string, data: any) { }, }); } + +export function delete$(path: string, data: any) { + return fetch('http://localhost:5000/api' + path, { + method: 'delete', + headers: { + 'Content-Type': 'application/json', + }, + }); +} diff --git a/src/components/NewUI/usePlace.ts b/src/components/NewUI/usePlace.ts index ffbc0fb..9d17cff 100644 --- a/src/components/NewUI/usePlace.ts +++ b/src/components/NewUI/usePlace.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { getPlaceDetails, PlaceDetails } from '../../api/google'; import useThrottle from './useThrottle'; -export default function usePlace(placeId: string) { +export default function usePlace(placeId: string | null) { const [placeDetails, setPlaceDetails] = useState(null); const updatePlaceDetails = useCallback(() => {