mirror of
https://github.com/myfatemi04/wheelshare-frontend.git
synced 2025-04-21 11:20:17 -04:00
Merge branch 'main' of https://github.com/myfatemi04/wheelshare-frontend
This commit is contained in:
commit
99291fb37b
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
44
src/components/NewUI/UIDateInput.tsx
Normal file
44
src/components/NewUI/UIDateInput.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
23
src/components/NewUI/bits.ts
Normal file
23
src/components/NewUI/bits.ts
Normal 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;
|
||||||
|
}
|
2
src/components/NewUI/colors.ts
Normal file
2
src/components/NewUI/colors.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const green = '#60f760';
|
||||||
|
export const lightgrey = '#e0e0e0';
|
|
@ -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(() => {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user