This commit is contained in:
Nitin Kanchinadam 2021-07-02 20:08:14 -04:00
commit 99291fb37b
7 changed files with 283 additions and 20 deletions

View File

@ -1,4 +1,6 @@
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 latlongdist, { R_miles } from './latlongdist';
import UIButton from './UIButton'; import UIButton from './UIButton';
import UIPlacesAutocomplete from './UIPlacesAutocomplete'; import UIPlacesAutocomplete from './UIPlacesAutocomplete';
@ -8,9 +10,6 @@ import usePlace from './usePlace';
import useThrottle from './useThrottle'; import useThrottle from './useThrottle';
import useToggle from './useToggle'; import useToggle from './useToggle';
const green = '#60f760';
const lightgrey = '#e0e0e0';
export type IEvent = { export type IEvent = {
id: number; id: number;
name: string; name: string;
@ -18,6 +17,8 @@ export type IEvent = {
formattedAddress: string; formattedAddress: string;
startTime: string; startTime: string;
endTime: string; endTime: string;
latitude: number;
longitude: number;
}; };
function formatStartAndEndTime( function formatStartAndEndTime(
@ -206,13 +207,13 @@ const dummyPeopleData: IPerson[] = [
longitude: 10.12, longitude: 10.12,
}, },
]; ];
function People({ event, placeId }: { event: IEvent; placeId: string }) { function People({ event, placeId }: { event: IEvent; placeId: string | null }) {
const PADDING = '1rem'; const PADDING = '1rem';
// eslint-disable-next-line // eslint-disable-next-line
const [people, setPeople] = useState(dummyPeopleData); const [people, setPeople] = useState(dummyPeopleData);
const placeDetails = usePlace(placeId); const placeDetails = usePlace(placeId);
const myLatitude = 10; const locationLongitude = event.latitude;
const myLongitude = 10; const locationLatitude = event.longitude;
return ( return (
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
@ -220,28 +221,28 @@ function People({ event, placeId }: { event: IEvent; placeId: string }) {
{people.map(({ name, latitude, longitude, id }) => { {people.map(({ name, latitude, longitude, id }) => {
let extraDistance = null; let extraDistance = null;
if (placeDetails != null) { if (placeDetails != null) {
const locationLatitude = placeDetails.latitude; const myLatitude = placeDetails.latitude;
const locationLongitude = placeDetails.longitude; const myLongitude = placeDetails.longitude;
const meToThem = latlongdist( const meToThem = latlongdist(
latitude, latitude,
longitude, longitude,
myLatitude, locationLongitude,
myLongitude, locationLatitude,
R_miles R_miles
); );
const themToLocation = latlongdist( const themToLocation = latlongdist(
latitude, latitude,
longitude, longitude,
locationLatitude, myLatitude,
locationLongitude, myLongitude,
R_miles R_miles
); );
const totalWithThem = meToThem + themToLocation; const totalWithThem = meToThem + themToLocation;
const totalWithoutThem = latlongdist( const totalWithoutThem = latlongdist(
locationLongitude,
locationLatitude,
myLatitude, myLatitude,
myLongitude, myLongitude,
locationLatitude,
locationLongitude,
R_miles R_miles
); );
extraDistance = totalWithThem - totalWithoutThem; extraDistance = totalWithThem - totalWithoutThem;
@ -285,9 +286,47 @@ function People({ event, placeId }: { event: IEvent; placeId: string }) {
export default function Event({ event }: { event: IEvent }) { export default function Event({ event }: { event: IEvent }) {
const { name, group, formattedAddress, startTime, endTime } = event; const { name, group, formattedAddress, startTime, endTime } = event;
const [haveRide, toggleHaveRide] = useToggle(false); const [haveRide, toggleHaveRide] = useToggle(false);
const [placeId, setPlaceId] = useState<string>(null!); const [placeId, setPlaceId] = useState<string | null>(null);
const [interested, toggleInterested] = useToggle(false); const [interested, toggleInterested] = useToggle(false);
const [updating, setUpdating] = useState(false);
const toggleInterestedThrottled = useThrottle(toggleInterested, 500); 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 ( return (
<UISecondaryBox> <UISecondaryBox>

View File

@ -1,14 +1,101 @@
import { useCallback, useState } from 'react'; import { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { post } from './api'; import { post } from './api';
import { toggleBit } from './bits';
import { green, lightgrey } from './colors';
import { IGroup } from './Group'; import { IGroup } from './Group';
import UIButton from './UIButton'; import UIButton from './UIButton';
import UIDateInput from './UIDateInput';
import UIDatetimeInput from './UIDatetimeInput'; import UIDatetimeInput from './UIDatetimeInput';
import UIPlacesAutocomplete from './UIPlacesAutocomplete'; import UIPlacesAutocomplete from './UIPlacesAutocomplete';
import UISecondaryBox from './UISecondaryBox'; import UISecondaryBox from './UISecondaryBox';
import UITextInput from './UITextInput'; import UITextInput from './UITextInput';
import useToggle from './useToggle';
const noop = () => {}; const noop = () => {};
const DAY_NAMES = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
function DaysOfWeekSelector({
daysOfWeek,
update,
disabled = false,
}: {
daysOfWeek: number;
update: Dispatch<SetStateAction<number>>;
disabled?: boolean;
}) {
const toggleDayOfWeek = useCallback(
function (idx: 1 | 2 | 3 | 4 | 5 | 6 | 7) {
update((daysOfWeek) => toggleBit(daysOfWeek, idx));
},
[update]
);
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
margin: '1rem auto',
}}
>
{DAY_NAMES.map((name, idx) => {
const mask = 0b1000_0000 >> (idx + 1);
const active = (daysOfWeek & mask) !== 0;
return (
<div
style={{
borderRadius: '100%',
cursor: 'pointer',
backgroundColor: active
? disabled
? // 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'
: disabled
? 'rgba(0, 0, 0, 0.5)'
: 'black',
userSelect: 'none',
width: '2em',
height: '2em',
margin: '0.5rem',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={() => {
if (!disabled) {
toggleDayOfWeek(
// @ts-ignore
idx + 1
);
}
}}
key={name}
>
{name.charAt(0)}
</div>
);
})}
</div>
);
}
export default function EventCreator({ group }: { group: IGroup }) { export default function EventCreator({ group }: { group: IGroup }) {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [startTime, setStartTime] = useState<Date | null>(null); const [startTime, setStartTime] = useState<Date | null>(null);
@ -17,22 +104,44 @@ export default function EventCreator({ group }: { group: IGroup }) {
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [createdEventId, setCreatedEventId] = useState(-1); const [createdEventId, setCreatedEventId] = useState(-1);
const [recurring, toggleRecurring] = useToggle(false);
const [daysOfWeek, setDaysOfWeek] = useState(0);
const [endDate, setEndDate] = useState<Date | null>(null);
const durationIsNegative =
endTime && startTime && endTime.getTime() < startTime.getTime();
const buttonEnabled = const buttonEnabled =
name.length > 0 && name.length > 0 &&
startTime != null && startTime != null &&
endTime != null && endTime != null &&
placeId != null && placeId != null &&
(!recurring || daysOfWeek || endDate !== null) &&
!durationIsNegative &&
!creating; !creating;
const createEvent = useCallback(() => { const createEvent = useCallback(() => {
if (!creating) { 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); setCreating(true);
post('/events', { post('/events', {
name, name,
startTime, startTime,
endTime, duration,
endDate,
groupId: group.id, groupId: group.id,
placeId, placeId,
daysOfWeek,
}) })
.then((response) => response.json()) .then((response) => response.json())
.then(({ id }) => { .then(({ id }) => {
@ -40,7 +149,16 @@ export default function EventCreator({ group }: { group: IGroup }) {
}) })
.finally(() => setCreating(false)); .finally(() => setCreating(false));
} }
}, [creating, name, startTime, endTime, group.id, placeId]); }, [
creating,
name,
startTime,
endTime,
group.id,
placeId,
daysOfWeek,
endDate,
]);
return ( return (
<UISecondaryBox style={{ width: '100%', boxSizing: 'border-box' }}> <UISecondaryBox style={{ width: '100%', boxSizing: 'border-box' }}>
@ -63,6 +181,34 @@ export default function EventCreator({ group }: { group: IGroup }) {
setPlaceId(placeId); setPlaceId(placeId);
}} }}
/> />
<UIButton
onClick={toggleRecurring}
style={{
backgroundColor: recurring ? green : lightgrey,
color: recurring ? 'white' : 'black',
transition: 'color 0.2s, background-color 0.2s',
marginBottom: '1.5rem',
}}
>
Recurring event
</UIButton>
{recurring && (
<>
Days of week
<DaysOfWeekSelector
daysOfWeek={daysOfWeek}
update={setDaysOfWeek}
disabled={creating}
/>
Date of last occurence
<UIDateInput onChangedDate={setEndDate} disabled={creating} />
</>
)}
{durationIsNegative && (
<span style={{ marginTop: '1rem' }}>
The start time can't be after the end time.
</span>
)}
{createdEventId === -1 ? ( {createdEventId === -1 ? (
<UIButton <UIButton
onClick={buttonEnabled ? createEvent : noop} onClick={buttonEnabled ? createEvent : noop}

View File

@ -0,0 +1,44 @@
import { useCallback } from 'react';
const baseStyle = {
marginTop: '0.5em',
padding: '0.5em',
fontFamily: 'Inter',
fontSize: '1.25rem',
borderRadius: '0.5em',
border: '0px',
};
export default function UIDateInput({
onChangedDate,
disabled = false,
}: {
onChangedDate: (date: Date | null) => 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 (
<input
style={baseStyle}
type="date"
disabled={disabled}
onChange={onChange}
/>
);
}

View File

@ -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',
},
});
}

View File

@ -0,0 +1,23 @@
export function setBit(n: number, idx: number, active: boolean) {
if (idx < 1 || idx > 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;
}

View File

@ -0,0 +1,2 @@
export const green = '#60f760';
export const lightgrey = '#e0e0e0';

View File

@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
import { getPlaceDetails, PlaceDetails } from '../../api/google'; import { getPlaceDetails, PlaceDetails } from '../../api/google';
import useThrottle from './useThrottle'; import useThrottle from './useThrottle';
export default function usePlace(placeId: string) { export default function usePlace(placeId: string | null) {
const [placeDetails, setPlaceDetails] = useState<PlaceDetails | null>(null); const [placeDetails, setPlaceDetails] = useState<PlaceDetails | null>(null);
const updatePlaceDetails = useCallback(() => { const updatePlaceDetails = useCallback(() => {