From 98a8e51b7bff725cb7f1e4f8cd0edff5ccbb659c Mon Sep 17 00:00:00 2001 From: Michael Fatemi <myfatemi04@gmail.com> Date: Thu, 8 Jul 2021 13:51:24 -0400 Subject: [PATCH] Organize --- src/components/App.tsx | 2 +- src/components/Availability.tsx | 151 ------- src/components/Carpool.tsx | 2 +- src/components/Event.tsx | 409 ------------------ src/components/Event/Event.tsx | 162 +++++++ src/components/Event/EventCarpools.tsx | 89 ++++ src/components/Event/EventDetails.tsx | 39 ++ src/components/Event/EventSignups.tsx | 91 ++++ .../EventCreator/DaysOfWeekSelector.tsx | 86 ++++ .../{ => EventCreator}/EventCreator.tsx | 107 +---- .../{ => EventCreator}/EventCreatorLink.tsx | 4 +- src/components/EventStream.tsx | 2 +- src/components/Events.tsx | 2 +- src/components/Group.tsx | 8 +- .../{ => GroupCreator}/GroupCreator.tsx | 10 +- .../{ => GroupCreator}/GroupCreatorLink.tsx | 2 +- src/components/GroupJoinerLink.tsx | 8 +- .../GroupSettings.tsx} | 39 +- .../GroupSettings/GroupSettingsLink.tsx | 27 ++ src/components/{ => Groups}/GroupList.tsx | 4 +- src/components/{ => Groups}/Groups.tsx | 8 +- .../{ => Notifications}/Notification.tsx | 6 +- .../{ => Notifications}/Notifications.tsx | 2 +- src/components/{ => UI}/UIButton.tsx | 0 src/components/{ => UI}/UIDateInput.tsx | 0 src/components/{ => UI}/UIDatetimeInput.tsx | 0 src/components/{ => UI}/UILink.tsx | 0 .../{ => UI}/UIPlacesAutocomplete.tsx | 0 src/components/{ => UI}/UIPressable.tsx | 0 src/components/{ => UI}/UIPrimaryTitle.tsx | 0 src/components/{ => UI}/UISecondaryBox.tsx | 0 src/components/{ => UI}/UISecondaryHeader.tsx | 0 src/components/{ => UI}/UITextInput.tsx | 0 src/components/{ => UI}/UITimeInput.tsx | 0 src/components/WheelShare.tsx | 6 +- src/components/WheelShareLoggedOut.tsx | 4 +- src/components/api.ts | 2 +- src/components/dates.ts | 27 ++ 38 files changed, 576 insertions(+), 723 deletions(-) delete mode 100644 src/components/Availability.tsx delete mode 100644 src/components/Event.tsx create mode 100644 src/components/Event/Event.tsx create mode 100644 src/components/Event/EventCarpools.tsx create mode 100644 src/components/Event/EventDetails.tsx create mode 100644 src/components/Event/EventSignups.tsx create mode 100644 src/components/EventCreator/DaysOfWeekSelector.tsx rename src/components/{ => EventCreator}/EventCreator.tsx (61%) rename src/components/{ => EventCreator}/EventCreatorLink.tsx (84%) rename src/components/{ => GroupCreator}/GroupCreator.tsx (86%) rename src/components/{ => GroupCreator}/GroupCreatorLink.tsx (91%) rename src/components/{GroupSettingsLink.tsx => GroupSettings/GroupSettings.tsx} (53%) create mode 100644 src/components/GroupSettings/GroupSettingsLink.tsx rename src/components/{ => Groups}/GroupList.tsx (86%) rename src/components/{ => Groups}/Groups.tsx (77%) rename src/components/{ => Notifications}/Notification.tsx (94%) rename src/components/{ => Notifications}/Notifications.tsx (92%) rename src/components/{ => UI}/UIButton.tsx (100%) rename src/components/{ => UI}/UIDateInput.tsx (100%) rename src/components/{ => UI}/UIDatetimeInput.tsx (100%) rename src/components/{ => UI}/UILink.tsx (100%) rename src/components/{ => UI}/UIPlacesAutocomplete.tsx (100%) rename src/components/{ => UI}/UIPressable.tsx (100%) rename src/components/{ => UI}/UIPrimaryTitle.tsx (100%) rename src/components/{ => UI}/UISecondaryBox.tsx (100%) rename src/components/{ => UI}/UISecondaryHeader.tsx (100%) rename src/components/{ => UI}/UITextInput.tsx (100%) rename src/components/{ => UI}/UITimeInput.tsx (100%) create mode 100644 src/components/dates.ts diff --git a/src/components/App.tsx b/src/components/App.tsx index 4345575..b8f85bf 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2,7 +2,7 @@ import { CSSProperties, lazy, Suspense, useEffect, useState } from 'react'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { getReceivedInvitationsAndRequests } from './api'; import { useMe } from './hooks'; -import Notifications from './Notifications'; +import Notifications from './Notifications/Notifications'; import { IInvitation } from './types'; import WheelShare from './WheelShare'; import WheelShareLoggedOut from './WheelShareLoggedOut'; diff --git a/src/components/Availability.tsx b/src/components/Availability.tsx deleted file mode 100644 index 91474d1..0000000 --- a/src/components/Availability.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { CSSProperties } from 'react'; -import { - MouseEventHandler, - ReactEventHandler, - useCallback, - useState, -} from 'react'; - -export type AvailabilityKind = 'going' | 'interested' | 'not-interested'; - -const availabilityNames: Record<AvailabilityKind, string> = { - going: 'Going', - interested: 'Interested', - 'not-interested': 'Not interested', -}; - -const optionStyle: CSSProperties = { - height: '3rem', - backgroundColor: '#e0e0e0', - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - cursor: 'pointer', - transition: 'background-color 100ms cubic-bezier', - userSelect: 'none', - position: 'relative', - fontWeight: 'normal', - textTransform: 'uppercase', -}; - -const selectedOptionStyle = { - ...optionStyle, - fontWeight: 600, -}; - -function Option({ - bind, - current, - onSelected, -}: { - bind: AvailabilityKind; - current: AvailabilityKind; - onSelected: (kind: AvailabilityKind) => void; -}) { - const selected = current === bind; - - const select: MouseEventHandler<HTMLDivElement> = useCallback( - (event) => { - onSelected(bind); - }, - [onSelected, bind] - ); - - return ( - <div style={selected ? selectedOptionStyle : optionStyle} onClick={select}> - {availabilityNames[bind]} - </div> - ); -} - -// eslint-disable-next-line -function Availability__old({ - selected, - onSelected: onSelectedInner, -}: { - selected: AvailabilityKind; - onSelected: (kind: AvailabilityKind) => void; -}) { - const [focused, setFocused] = useState(false); - const onSelected = useCallback( - (kind: AvailabilityKind) => { - setFocused(false); - onSelectedInner(kind); - }, - [onSelectedInner] - ); - return ( - <div - style={{ - display: 'flex', - flexDirection: 'column', - borderRadius: '0.5rem', - overflow: 'hidden', - marginTop: '1rem', - marginBottom: '1rem', - }} - tabIndex={0} - onBlur={() => setFocused(false)} - > - {focused ? ( - <> - <Option bind="going" current={selected} onSelected={onSelected} /> - <Option - bind="interested" - current={selected} - onSelected={onSelected} - /> - <Option - bind="not-interested" - current={selected} - onSelected={onSelected} - /> - </> - ) : ( - <Option - bind={selected} - current={selected} - onSelected={() => setFocused(true)} - /> - )} - </div> - ); -} - -export default function Availability({ - selected, - onSelected: onSelectedInner, -}: { - selected: AvailabilityKind; - onSelected: (kind: AvailabilityKind) => void; -}) { - const onSelected: ReactEventHandler<HTMLSelectElement> = useCallback( - (event) => { - onSelectedInner( - // @ts-ignore - event.target.value - ); - event.preventDefault(); - }, - [onSelectedInner] - ); - return ( - <select - value={selected} - onChange={onSelected} - style={{ - fontFamily: 'Inter', - fontSize: '1rem', - border: '0px solid black', - borderRadius: '0.5rem', - padding: '0.5rem', - marginTop: '1rem', - }} - > - <option value="going">Going</option> - <option value="interested">Interested</option> - <option value="not-interested">Not interested</option> - </select> - ); -} diff --git a/src/components/Carpool.tsx b/src/components/Carpool.tsx index a852c04..66e848e 100644 --- a/src/components/Carpool.tsx +++ b/src/components/Carpool.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { getCarpool } from './api'; import { ICarpool } from './types'; -import UISecondaryBox from './UISecondaryBox'; +import UISecondaryBox from './UI/UISecondaryBox'; function MemberList({ members }: { members: ICarpool['members'] }) { return ( diff --git a/src/components/Event.tsx b/src/components/Event.tsx deleted file mode 100644 index 7d6d5bb..0000000 --- a/src/components/Event.tsx +++ /dev/null @@ -1,409 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { - addOrUpdateEventSignup, - getEventSignups, - removeEventSignup, -} from './api'; -import { green, lightgrey } from './colors'; -import { useMe } from './hooks'; -import latlongdist, { R_miles } from './latlongdist'; -import UIButton from './UIButton'; -import UIPlacesAutocomplete from './UIPlacesAutocomplete'; -import UISecondaryBox from './UISecondaryBox'; -import UISecondaryHeader from './UISecondaryHeader'; -import usePlace from './usePlace'; -import useThrottle from './useThrottle'; -import useToggle from './useToggle'; - -export type IEvent = { - id: number; - name: string; - group: string; - formattedAddress: string; - startTime: string; - endTime: string; - latitude: number; - longitude: number; -}; - -function formatStartAndEndTime( - startDatetimeString: string, - endDatetimeString: string -) { - const startDatetime = new Date(startDatetimeString); - const endDatetime = new Date(endDatetimeString); - - if (isNaN(startDatetime.valueOf())) { - console.error('Invalid datetime:', startDatetimeString); - return '(invalid)'; - } - if (isNaN(endDatetime.valueOf())) { - console.error('Invalid datetime:', startDatetimeString); - return '(invalid)'; - } - - const startDateString = startDatetime.toLocaleDateString(); - const endDateString = endDatetime.toLocaleDateString(); - - if (startDateString === endDateString) { - const startTimeString = startDatetime.toLocaleTimeString(); - const endTimeString = endDatetime.toLocaleTimeString(); - return `${startDateString}, ${startTimeString} - ${endTimeString}`; - } else { - return `${startDatetime.toLocaleString()} - ${endDatetime.toLocaleString()}`; - } -} - -function GroupName({ name }: { name: string }) { - return ( - <span - style={{ - color: '#303030', - textAlign: 'center', - }} - > - {name} - </span> - ); -} - -function Details({ - startTime, - endTime, - formattedAddress, -}: { - startTime: string; - endTime: string; - formattedAddress: string; -}) { - return ( - <div - style={{ - marginTop: '0.5rem', - textAlign: 'left', - }} - > - <span - style={{ - color: '#303030', - }} - > - <b>When: </b> - {formatStartAndEndTime(startTime, endTime)} - </span> - <br /> - <br /> - <span - style={{ - color: '#303030', - }} - > - <b>Where: </b> - {formattedAddress} - </span> - </div> - ); -} - -export type ICarpool = { - driver: { - id: number; - name: string; - }; - startTime: string; - endTime: string; - extraDistance: number; -}; - -function CarpoolRow({ carpool }: { carpool: ICarpool }) { - const PADDING = '1rem'; - return ( - <div - style={{ - display: 'flex', - alignItems: 'center', - position: 'relative', - padding: PADDING, - borderRadius: '0.5rem', - border: '1px solid #e0e0e0', - marginTop: '0.5rem', - marginBottom: '0.5rem', - }} - > - <div> - <span style={{ fontWeight: 500 }}>{carpool.driver.name}</span> - <br /> - Time:{' '} - <b> - {carpool.startTime} - {carpool.endTime} - </b> - <br /> - Offset from route: <b>{carpool.extraDistance} miles</b> - </div> - <div - style={{ - borderRadius: '0.5em', - cursor: 'pointer', - padding: '0.5em', - position: 'absolute', - right: PADDING, - userSelect: 'none', - backgroundColor: '#e0e0e0', - }} - > - Request to join - </div> - </div> - ); -} - -const dummyCarpoolData: ICarpool[] = [ - { - driver: { - id: 0, - name: 'Michael Fatemi', - }, - startTime: '10:00', - endTime: '10:10', - extraDistance: 6.9, - }, - { - driver: { - id: 1, - name: 'Joshua Hsueh', - }, - startTime: '10:05', - endTime: '10:10', - extraDistance: 420, - }, -]; -function Carpools({ event }: { event: IEvent }) { - // eslint-disable-next-line - const [carpools, _setCarpools] = useState(dummyCarpoolData); - - return ( - <div style={{ display: 'flex', flexDirection: 'column' }}> - <h3 style={{ marginBlockEnd: '0' }}>Carpools</h3> - {carpools.map((carpool) => ( - <CarpoolRow carpool={carpool} key={carpool.driver.id} /> - ))} - </div> - ); -} - -export type IEventSignup = { - user: { - id: number; - name: number; - }; - placeId: string; - formattedAddress: string; - latitude: number; - longitude: number; -}; - -function Signups({ - event, - signups, - myPlaceId, -}: { - event: IEvent; - signups: IEventSignup[]; - myPlaceId: string | null; -}) { - const PADDING = '1rem'; - const placeDetails = usePlace(myPlaceId); - const locationLongitude = event.latitude; - const locationLatitude = event.longitude; - const me = useMe(); - - return ( - <div style={{ display: 'flex', flexDirection: 'column' }}> - <h3 style={{ marginBlockEnd: '0' }}>People</h3> - {signups.map(({ latitude, longitude, user }) => { - if (user.id === me?.id) { - return null; - } - let extraDistance = null; - if (placeDetails != null) { - const myLatitude = placeDetails.latitude; - const myLongitude = placeDetails.longitude; - const meToThem = latlongdist( - latitude, - longitude, - locationLongitude, - locationLatitude, - R_miles - ); - const themToLocation = latlongdist( - latitude, - longitude, - myLatitude, - myLongitude, - R_miles - ); - const totalWithThem = meToThem + themToLocation; - const totalWithoutThem = latlongdist( - locationLongitude, - locationLatitude, - myLatitude, - myLongitude, - R_miles - ); - extraDistance = totalWithThem - totalWithoutThem; - } - - return ( - <div - style={{ - display: 'flex', - alignItems: 'center', - position: 'relative', - padding: '1rem', - borderRadius: '0.5rem', - border: '1px solid #e0e0e0', - marginTop: '0.5rem', - marginBottom: '0.5rem', - }} - key={user.id} - > - <b>{user.name}</b> - {extraDistance ? `: +${extraDistance.toFixed(1)} miles` : ''} - <div - style={{ - borderRadius: '0.5em', - cursor: 'pointer', - padding: '0.5em', - position: 'absolute', - right: PADDING, - userSelect: 'none', - backgroundColor: '#e0e0e0', - }} - > - Invite to carpool - </div> - </div> - ); - })} - </div> - ); -} - -export default function Event({ event }: { event: IEvent }) { - const { name, group, formattedAddress, startTime, endTime } = event; - const [haveRide, toggleHaveRide] = useToggle(false); - const [placeId, setPlaceId] = useState<string | null>(null); - const [interested, setInterested] = useState(false); - const [updating, setUpdating] = useState(false); - const [signups, setSignups] = useState<IEventSignup[]>([]); - const toggleInterested = useCallback(() => setInterested((i) => !i), []); - const toggleInterestedThrottled = useThrottle(toggleInterested, 500); - const existingSignup = useRef({ - interested: false, - placeId: null as string | null, - eventId: null as number | null, - }); - const me = useMe(); - - useEffect(() => { - const removeSignup = () => { - if (prev.interested) { - removeEventSignup(event.id) - .then(() => { - prev.interested = false; - }) - .finally(() => setUpdating(false)); - } - }; - - const addOrUpdateSignup = () => { - if (!prev.interested) { - addOrUpdateEventSignup(event.id, placeId) - .then(() => { - prev.placeId = placeId; - prev.eventId = event.id; - prev.interested = true; - }) - .finally(() => setUpdating(false)); - } - }; - - const prev = existingSignup.current; - - if (!interested) { - removeSignup(); - } else { - addOrUpdateSignup(); - } - }, [event.id, interested, placeId, updating]); - - useEffect(() => { - getEventSignups(event.id) - .then((signups) => { - setSignups(signups); - for (let signup of signups) { - if (signup.user.id === me?.id) { - setInterested(true); - existingSignup.current.eventId = event.id; - existingSignup.current.placeId = signup.placeId; - existingSignup.current.interested = true; - } - } - }) - .catch(console.error); - }, [event.id, me?.id]); - - return ( - <UISecondaryBox> - <UISecondaryHeader>{name}</UISecondaryHeader> - <GroupName name={group} /> - <Details {...{ startTime, endTime, formattedAddress }} /> - <UIButton - onClick={toggleInterestedThrottled} - style={{ - backgroundColor: interested ? green : lightgrey, - color: interested ? 'white' : 'black', - transition: 'color 0.2s, background-color 0.2s', - }} - > - {interested ? 'Interested' : 'Not interested'} - </UIButton> - {interested && ( - <> - <UIPlacesAutocomplete - placeholder="Pickup and dropoff location" - onSelected={(_address, placeID) => { - setPlaceId(placeID); - }} - style={placeId != null ? { border: '2px solid ' + green } : {}} - /> - {false && ( - <div - style={{ - display: 'flex', - alignItems: 'center', - cursor: 'pointer', - userSelect: 'none', - }} - onClick={toggleHaveRide} - > - <input - type="checkbox" - style={{ - borderRadius: '0.5em', - width: '2em', - height: '2em', - margin: '1em', - }} - checked={haveRide} - /> - I don't have any way to get there yet - </div> - )} - <Carpools event={event} /> - <Signups event={event} myPlaceId={placeId} signups={signups} /> - </> - )} - </UISecondaryBox> - ); -} diff --git a/src/components/Event/Event.tsx b/src/components/Event/Event.tsx new file mode 100644 index 0000000..14a388a --- /dev/null +++ b/src/components/Event/Event.tsx @@ -0,0 +1,162 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { + addOrUpdateEventSignup, + getEventSignups, + removeEventSignup, +} from '../api'; +import { green, lightgrey } from '../colors'; +import { useMe } from '../hooks'; +import UIButton from '../UI/UIButton'; +import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete'; +import UISecondaryBox from '../UI/UISecondaryBox'; +import UISecondaryHeader from '../UI/UISecondaryHeader'; +import useThrottle from '../useThrottle'; +import useToggle from '../useToggle'; +import EventCarpools from './EventCarpools'; +import EventDetails from './EventDetails'; +import EventSignups from './EventSignups'; + +export type IEvent = { + id: number; + name: string; + group: string; + formattedAddress: string; + startTime: string; + endTime: string; + latitude: number; + longitude: number; +}; + +function GroupName({ name }: { name: string }) { + return <span style={{ color: '#303030', textAlign: 'center' }}>{name}</span>; +} + +export type IEventSignup = { + user: { + id: number; + name: number; + }; + placeId: string; + formattedAddress: string; + latitude: number; + longitude: number; +}; + +export default function Event({ event }: { event: IEvent }) { + const { name, group, formattedAddress, startTime, endTime } = event; + const [haveRide, toggleHaveRide] = useToggle(false); + const [placeId, setPlaceId] = useState<string | null>(null); + const [interested, setInterested] = useState(false); + const [updating, setUpdating] = useState(false); + const [signups, setSignups] = useState<IEventSignup[]>([]); + const toggleInterested = useCallback(() => setInterested((i) => !i), []); + const toggleInterestedThrottled = useThrottle(toggleInterested, 500); + const existingSignup = useRef({ + interested: false, + placeId: null as string | null, + eventId: null as number | null, + }); + const me = useMe(); + + useEffect(() => { + const removeSignup = () => { + if (prev.interested) { + removeEventSignup(event.id) + .then(() => { + prev.interested = false; + }) + .finally(() => setUpdating(false)); + } + }; + + const addOrUpdateSignup = () => { + if (!prev.interested) { + addOrUpdateEventSignup(event.id, placeId) + .then(() => { + prev.placeId = placeId; + prev.eventId = event.id; + prev.interested = true; + }) + .finally(() => setUpdating(false)); + } + }; + + const prev = existingSignup.current; + + if (!interested) { + removeSignup(); + } else { + addOrUpdateSignup(); + } + }, [event.id, interested, placeId, updating]); + + useEffect(() => { + getEventSignups(event.id) + .then((signups) => { + setSignups(signups); + for (let signup of signups) { + if (signup.user.id === me?.id) { + setInterested(true); + existingSignup.current.eventId = event.id; + existingSignup.current.placeId = signup.placeId; + existingSignup.current.interested = true; + } + } + }) + .catch(console.error); + }, [event.id, me?.id]); + + return ( + <UISecondaryBox> + <UISecondaryHeader>{name}</UISecondaryHeader> + <GroupName name={group} /> + <EventDetails {...{ startTime, endTime, formattedAddress }} /> + <UIButton + onClick={toggleInterestedThrottled} + style={{ + backgroundColor: interested ? green : lightgrey, + color: interested ? 'white' : 'black', + transition: 'color 0.2s, background-color 0.2s', + }} + > + {interested ? 'Interested' : 'Not interested'} + </UIButton> + {interested && ( + <> + <UIPlacesAutocomplete + placeholder="Pickup and dropoff location" + onSelected={(_address, placeID) => { + setPlaceId(placeID); + }} + style={placeId != null ? { border: '2px solid ' + green } : {}} + /> + {false && ( + <div + style={{ + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + userSelect: 'none', + }} + onClick={toggleHaveRide} + > + <input + type="checkbox" + style={{ + borderRadius: '0.5em', + width: '2em', + height: '2em', + margin: '1em', + }} + checked={haveRide} + /> + I don't have any way to get there yet + </div> + )} + <EventCarpools event={event} /> + <EventSignups event={event} myPlaceId={placeId} signups={signups} /> + </> + )} + </UISecondaryBox> + ); +} diff --git a/src/components/Event/EventCarpools.tsx b/src/components/Event/EventCarpools.tsx new file mode 100644 index 0000000..f102024 --- /dev/null +++ b/src/components/Event/EventCarpools.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react'; +import { IEvent } from './Event'; + +export type ICarpool = { + driver: { + id: number; + name: string; + }; + startTime: string; + endTime: string; + extraDistance: number; +}; + +function CarpoolRow({ carpool }: { carpool: ICarpool }) { + const PADDING = '1rem'; + return ( + <div + style={{ + display: 'flex', + alignItems: 'center', + position: 'relative', + padding: PADDING, + borderRadius: '0.5rem', + border: '1px solid #e0e0e0', + marginTop: '0.5rem', + marginBottom: '0.5rem', + }} + > + <div> + <span style={{ fontWeight: 500 }}>{carpool.driver.name}</span> + <br /> + Time:{' '} + <b> + {carpool.startTime} - {carpool.endTime} + </b> + <br /> + Offset from route: <b>{carpool.extraDistance} miles</b> + </div> + <div + style={{ + borderRadius: '0.5em', + cursor: 'pointer', + padding: '0.5em', + position: 'absolute', + right: PADDING, + userSelect: 'none', + backgroundColor: '#e0e0e0', + }} + > + Request to join + </div> + </div> + ); +} + +const dummyCarpoolData: ICarpool[] = [ + { + driver: { + id: 0, + name: 'Michael Fatemi', + }, + startTime: '10:00', + endTime: '10:10', + extraDistance: 6.9, + }, + { + driver: { + id: 1, + name: 'Joshua Hsueh', + }, + startTime: '10:05', + endTime: '10:10', + extraDistance: 420, + }, +]; + +export default function Carpools({ event }: { event: IEvent }) { + // eslint-disable-next-line + const [carpools, _setCarpools] = useState(dummyCarpoolData); + + return ( + <div style={{ display: 'flex', flexDirection: 'column' }}> + <h3 style={{ marginBlockEnd: '0' }}>Carpools</h3> + {carpools.map((carpool) => ( + <CarpoolRow carpool={carpool} key={carpool.driver.id} /> + ))} + </div> + ); +} diff --git a/src/components/Event/EventDetails.tsx b/src/components/Event/EventDetails.tsx new file mode 100644 index 0000000..4d7e4cc --- /dev/null +++ b/src/components/Event/EventDetails.tsx @@ -0,0 +1,39 @@ +import formatStartAndEndTime from '../dates'; + +export default function Details({ + startTime, + endTime, + formattedAddress, +}: { + startTime: string; + endTime: string; + formattedAddress: string; +}) { + return ( + <div + style={{ + marginTop: '0.5rem', + textAlign: 'left', + }} + > + <span + style={{ + color: '#303030', + }} + > + <b>When: </b> + {formatStartAndEndTime(startTime, endTime)} + </span> + <br /> + <br /> + <span + style={{ + color: '#303030', + }} + > + <b>Where: </b> + {formattedAddress} + </span> + </div> + ); +} diff --git a/src/components/Event/EventSignups.tsx b/src/components/Event/EventSignups.tsx new file mode 100644 index 0000000..4b230bd --- /dev/null +++ b/src/components/Event/EventSignups.tsx @@ -0,0 +1,91 @@ +import { useMe } from '../hooks'; +import latlongdist, { R_miles } from '../latlongdist'; +import usePlace from '../usePlace'; +import { IEvent, IEventSignup } from './Event'; + +export default function Signups({ + event, + signups, + myPlaceId, +}: { + event: IEvent; + signups: IEventSignup[]; + myPlaceId: string | null; +}) { + const PADDING = '1rem'; + const placeDetails = usePlace(myPlaceId); + const locationLongitude = event.latitude; + const locationLatitude = event.longitude; + const me = useMe(); + + return ( + <div style={{ display: 'flex', flexDirection: 'column' }}> + <h3 style={{ marginBlockEnd: '0' }}>People</h3> + {signups.map(({ latitude, longitude, user }) => { + if (user.id === me?.id) { + return null; + } + let extraDistance = null; + if (placeDetails != null) { + const myLatitude = placeDetails.latitude; + const myLongitude = placeDetails.longitude; + const meToThem = latlongdist( + latitude, + longitude, + locationLongitude, + locationLatitude, + R_miles + ); + const themToLocation = latlongdist( + latitude, + longitude, + myLatitude, + myLongitude, + R_miles + ); + const totalWithThem = meToThem + themToLocation; + const totalWithoutThem = latlongdist( + locationLongitude, + locationLatitude, + myLatitude, + myLongitude, + R_miles + ); + extraDistance = totalWithThem - totalWithoutThem; + } + + return ( + <div + style={{ + display: 'flex', + alignItems: 'center', + position: 'relative', + padding: '1rem', + borderRadius: '0.5rem', + border: '1px solid #e0e0e0', + marginTop: '0.5rem', + marginBottom: '0.5rem', + }} + key={user.id} + > + <b>{user.name}</b> + {extraDistance ? `: +${extraDistance.toFixed(1)} miles` : ''} + <div + style={{ + borderRadius: '0.5em', + cursor: 'pointer', + padding: '0.5em', + position: 'absolute', + right: PADDING, + userSelect: 'none', + backgroundColor: '#e0e0e0', + }} + > + Invite to carpool + </div> + </div> + ); + })} + </div> + ); +} diff --git a/src/components/EventCreator/DaysOfWeekSelector.tsx b/src/components/EventCreator/DaysOfWeekSelector.tsx new file mode 100644 index 0000000..f78ecbc --- /dev/null +++ b/src/components/EventCreator/DaysOfWeekSelector.tsx @@ -0,0 +1,86 @@ +import { Dispatch, SetStateAction, useCallback } from 'react'; +import { toggleBit } from '../bits'; +import { green, lightgrey } from '../colors'; + +const DAY_NAMES = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', +]; + +export default 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> + ); +} diff --git a/src/components/EventCreator.tsx b/src/components/EventCreator/EventCreator.tsx similarity index 61% rename from src/components/EventCreator.tsx rename to src/components/EventCreator/EventCreator.tsx index 547f5ca..9c4a95b 100644 --- a/src/components/EventCreator.tsx +++ b/src/components/EventCreator/EventCreator.tsx @@ -1,101 +1,18 @@ -import { Dispatch, SetStateAction, useCallback, useState } from 'react'; -import { createEvent } from './api'; -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'; -import UITextInput from './UITextInput'; -import useToggle from './useToggle'; +import { useCallback, useState } from 'react'; +import { createEvent } from '../api'; +import { green, lightgrey } from '../colors'; +import { IGroup } from '../Group'; +import UIButton from '../UI/UIButton'; +import UIDateInput from '../UI/UIDateInput'; +import UIDatetimeInput from '../UI/UIDatetimeInput'; +import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete'; +import UISecondaryBox from '../UI/UISecondaryBox'; +import UITextInput from '../UI/UITextInput'; +import useToggle from '../useToggle'; +import DaysOfWeekSelector from './DaysOfWeekSelector'; 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 }) { const [name, setName] = useState(''); const [startTime, setStartTime] = useState<Date | null>(null); diff --git a/src/components/EventCreatorLink.tsx b/src/components/EventCreator/EventCreatorLink.tsx similarity index 84% rename from src/components/EventCreatorLink.tsx rename to src/components/EventCreator/EventCreatorLink.tsx index f88bbe4..6e47db0 100644 --- a/src/components/EventCreatorLink.tsx +++ b/src/components/EventCreator/EventCreatorLink.tsx @@ -1,6 +1,6 @@ import EventCreator from './EventCreator'; -import { IGroup } from './Group'; -import useToggle from './useToggle'; +import { IGroup } from '../Group'; +import useToggle from '../useToggle'; export default function EventCreatorLink({ group }: { group: IGroup }) { const [open, toggle] = useToggle(false); diff --git a/src/components/EventStream.tsx b/src/components/EventStream.tsx index 16b8f09..9fc94ae 100644 --- a/src/components/EventStream.tsx +++ b/src/components/EventStream.tsx @@ -1,4 +1,4 @@ -import Event, { IEvent } from './Event'; +import Event, { IEvent } from './Event/Event'; export default function EventStream({ events }: { events: IEvent[] }) { return ( diff --git a/src/components/Events.tsx b/src/components/Events.tsx index 136656a..3fe11da 100644 --- a/src/components/Events.tsx +++ b/src/components/Events.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { getEvents } from './api'; -import { IEvent } from './Event'; +import { IEvent } from './Event/Event'; import EventStream from './EventStream'; export default function Events() { diff --git a/src/components/Group.tsx b/src/components/Group.tsx index 008e7eb..779369e 100644 --- a/src/components/Group.tsx +++ b/src/components/Group.tsx @@ -2,11 +2,11 @@ import { useEffect, useState } from 'react'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; import { getGroup, getGroupEvents } from './api'; -import { IEvent } from './Event'; -import EventCreatorLink from './EventCreatorLink'; +import { IEvent } from './Event/Event'; +import EventCreatorLink from './EventCreator/EventCreatorLink'; import EventStream from './EventStream'; -import GroupSettingsLink from './GroupSettingsLink'; -import UILink from './UILink'; +import GroupSettingsLink from './GroupSettings/GroupSettingsLink'; +import UILink from './UI/UILink'; export type IGroup = { id: number; diff --git a/src/components/GroupCreator.tsx b/src/components/GroupCreator/GroupCreator.tsx similarity index 86% rename from src/components/GroupCreator.tsx rename to src/components/GroupCreator/GroupCreator.tsx index 660935f..51a2566 100644 --- a/src/components/GroupCreator.tsx +++ b/src/components/GroupCreator/GroupCreator.tsx @@ -1,9 +1,9 @@ import { useCallback, useState } from 'react'; -import { createGroup } from './api'; -import UIButton from './UIButton'; -import UILink from './UILink'; -import UISecondaryBox from './UISecondaryBox'; -import UITextInput from './UITextInput'; +import { createGroup } from '../api'; +import UIButton from '../UI/UIButton'; +import UILink from '../UI/UILink'; +import UISecondaryBox from '../UI/UISecondaryBox'; +import UITextInput from '../UI/UITextInput'; export default function GroupCreator() { const [name, setName] = useState(''); diff --git a/src/components/GroupCreatorLink.tsx b/src/components/GroupCreator/GroupCreatorLink.tsx similarity index 91% rename from src/components/GroupCreatorLink.tsx rename to src/components/GroupCreator/GroupCreatorLink.tsx index e9f1ae5..313e787 100644 --- a/src/components/GroupCreatorLink.tsx +++ b/src/components/GroupCreator/GroupCreatorLink.tsx @@ -1,5 +1,5 @@ import GroupCreator from './GroupCreator'; -import useToggle from './useToggle'; +import useToggle from '../useToggle'; export default function GroupCreatorLink() { const [open, toggle] = useToggle(false); diff --git a/src/components/GroupJoinerLink.tsx b/src/components/GroupJoinerLink.tsx index 65fbe35..c122e38 100644 --- a/src/components/GroupJoinerLink.tsx +++ b/src/components/GroupJoinerLink.tsx @@ -1,9 +1,9 @@ import { useCallback, useEffect, useState } from 'react'; import { joinGroup, resolveCode } from './api'; -import UIButton from './UIButton'; -import UIPressable from './UIPressable'; -import UISecondaryBox from './UISecondaryBox'; -import UITextInput from './UITextInput'; +import UIButton from './UI/UIButton'; +import UIPressable from './UI/UIPressable'; +import UISecondaryBox from './UI/UISecondaryBox'; +import UITextInput from './UI/UITextInput'; import useToggle from './useToggle'; export type GroupPreview = { diff --git a/src/components/GroupSettingsLink.tsx b/src/components/GroupSettings/GroupSettings.tsx similarity index 53% rename from src/components/GroupSettingsLink.tsx rename to src/components/GroupSettings/GroupSettings.tsx index 79cf264..fb1d9ff 100644 --- a/src/components/GroupSettingsLink.tsx +++ b/src/components/GroupSettings/GroupSettings.tsx @@ -1,12 +1,11 @@ -import { useCallback, useState } from 'react'; -import { deleteGroup } from './api'; -import { IGroup } from './Group'; -import UILink from './UILink'; -import UIPressable from './UIPressable'; -import UISecondaryBox from './UISecondaryBox'; -import useToggle from './useToggle'; +import { useState, useCallback } from 'react'; +import { deleteGroup } from '../api'; +import { IGroup } from '../types'; +import UILink from '../UI/UILink'; +import UIPressable from '../UI/UIPressable'; +import UISecondaryBox from '../UI/UISecondaryBox'; -function GroupSettings({ group }: { group: IGroup }) { +export default function GroupSettings({ group }: { group: IGroup }) { const [deletionSuccessful, setDeletionSuccessful] = useState<boolean | null>(null); @@ -41,27 +40,3 @@ function GroupSettings({ group }: { group: IGroup }) { </UISecondaryBox> ); } - -export default function GroupSettingsLink({ group }: { group: IGroup }) { - const [open, toggle] = useToggle(false); - - return ( - <div> - <div - style={{ - cursor: 'pointer', - userSelect: 'none', - }} - onClick={toggle} - > - Settings - </div> - {open && ( - <> - <br /> - <GroupSettings group={group} /> - </> - )} - </div> - ); -} diff --git a/src/components/GroupSettings/GroupSettingsLink.tsx b/src/components/GroupSettings/GroupSettingsLink.tsx new file mode 100644 index 0000000..0c992ea --- /dev/null +++ b/src/components/GroupSettings/GroupSettingsLink.tsx @@ -0,0 +1,27 @@ +import { IGroup } from '../Group'; +import useToggle from '../useToggle'; +import GroupSettings from './GroupSettings'; + +export default function GroupSettingsLink({ group }: { group: IGroup }) { + const [open, toggle] = useToggle(false); + + return ( + <div> + <div + style={{ + cursor: 'pointer', + userSelect: 'none', + }} + onClick={toggle} + > + Settings + </div> + {open && ( + <> + <br /> + <GroupSettings group={group} /> + </> + )} + </div> + ); +} diff --git a/src/components/GroupList.tsx b/src/components/Groups/GroupList.tsx similarity index 86% rename from src/components/GroupList.tsx rename to src/components/Groups/GroupList.tsx index 0e97ae8..ac031a9 100644 --- a/src/components/GroupList.tsx +++ b/src/components/Groups/GroupList.tsx @@ -1,5 +1,5 @@ -import { IGroup } from './Group'; -import UISecondaryBox from './UISecondaryBox'; +import { IGroup } from '../Group'; +import UISecondaryBox from '../UI/UISecondaryBox'; function GroupListItem({ group }: { group: IGroup }) { return ( diff --git a/src/components/Groups.tsx b/src/components/Groups/Groups.tsx similarity index 77% rename from src/components/Groups.tsx rename to src/components/Groups/Groups.tsx index 3b4bee6..13531c6 100644 --- a/src/components/Groups.tsx +++ b/src/components/Groups/Groups.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; -import { getGroups } from './api'; -import { IGroup } from './Group'; -import GroupCreatorLink from './GroupCreatorLink'; -import GroupJoinerLink from './GroupJoinerLink'; +import { getGroups } from '../api'; +import { IGroup } from '../Group'; +import GroupCreatorLink from '../GroupCreator/GroupCreatorLink'; +import GroupJoinerLink from '../GroupJoinerLink'; import GroupList from './GroupList'; export default function Groups() { diff --git a/src/components/Notification.tsx b/src/components/Notifications/Notification.tsx similarity index 94% rename from src/components/Notification.tsx rename to src/components/Notifications/Notification.tsx index 4d4c878..3893ab1 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notifications/Notification.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; -import { acceptInvite, acceptRequest, denyInvite, denyRequest } from './api'; -import { IInvitation } from './types'; -import UIButton from './UIButton'; +import { acceptInvite, acceptRequest, denyInvite, denyRequest } from '../api'; +import { IInvitation } from '../types'; +import UIButton from '../UI/UIButton'; export default function Notification({ notification, diff --git a/src/components/Notifications.tsx b/src/components/Notifications/Notifications.tsx similarity index 92% rename from src/components/Notifications.tsx rename to src/components/Notifications/Notifications.tsx index 5338312..773b58c 100644 --- a/src/components/Notifications.tsx +++ b/src/components/Notifications/Notifications.tsx @@ -1,5 +1,5 @@ import Notification from './Notification'; -import { IInvitation } from './types'; +import { IInvitation } from '../types'; export default function Notifications({ notifications, diff --git a/src/components/UIButton.tsx b/src/components/UI/UIButton.tsx similarity index 100% rename from src/components/UIButton.tsx rename to src/components/UI/UIButton.tsx diff --git a/src/components/UIDateInput.tsx b/src/components/UI/UIDateInput.tsx similarity index 100% rename from src/components/UIDateInput.tsx rename to src/components/UI/UIDateInput.tsx diff --git a/src/components/UIDatetimeInput.tsx b/src/components/UI/UIDatetimeInput.tsx similarity index 100% rename from src/components/UIDatetimeInput.tsx rename to src/components/UI/UIDatetimeInput.tsx diff --git a/src/components/UILink.tsx b/src/components/UI/UILink.tsx similarity index 100% rename from src/components/UILink.tsx rename to src/components/UI/UILink.tsx diff --git a/src/components/UIPlacesAutocomplete.tsx b/src/components/UI/UIPlacesAutocomplete.tsx similarity index 100% rename from src/components/UIPlacesAutocomplete.tsx rename to src/components/UI/UIPlacesAutocomplete.tsx diff --git a/src/components/UIPressable.tsx b/src/components/UI/UIPressable.tsx similarity index 100% rename from src/components/UIPressable.tsx rename to src/components/UI/UIPressable.tsx diff --git a/src/components/UIPrimaryTitle.tsx b/src/components/UI/UIPrimaryTitle.tsx similarity index 100% rename from src/components/UIPrimaryTitle.tsx rename to src/components/UI/UIPrimaryTitle.tsx diff --git a/src/components/UISecondaryBox.tsx b/src/components/UI/UISecondaryBox.tsx similarity index 100% rename from src/components/UISecondaryBox.tsx rename to src/components/UI/UISecondaryBox.tsx diff --git a/src/components/UISecondaryHeader.tsx b/src/components/UI/UISecondaryHeader.tsx similarity index 100% rename from src/components/UISecondaryHeader.tsx rename to src/components/UI/UISecondaryHeader.tsx diff --git a/src/components/UITextInput.tsx b/src/components/UI/UITextInput.tsx similarity index 100% rename from src/components/UITextInput.tsx rename to src/components/UI/UITextInput.tsx diff --git a/src/components/UITimeInput.tsx b/src/components/UI/UITimeInput.tsx similarity index 100% rename from src/components/UITimeInput.tsx rename to src/components/UI/UITimeInput.tsx diff --git a/src/components/WheelShare.tsx b/src/components/WheelShare.tsx index ffdada2..d96e999 100644 --- a/src/components/WheelShare.tsx +++ b/src/components/WheelShare.tsx @@ -1,9 +1,9 @@ import logout from './Authentication/logout'; import Events from './Events'; -import Groups from './Groups'; +import Groups from './Groups/Groups'; import { useMe } from './hooks'; -import UIPressable from './UIPressable'; -import UIPrimaryTitle from './UIPrimaryTitle'; +import UIPressable from './UI/UIPressable'; +import UIPrimaryTitle from './UI/UIPrimaryTitle'; export default function WheelShare() { const { name } = useMe()!; diff --git a/src/components/WheelShareLoggedOut.tsx b/src/components/WheelShareLoggedOut.tsx index a5e0213..ad70b17 100644 --- a/src/components/WheelShareLoggedOut.tsx +++ b/src/components/WheelShareLoggedOut.tsx @@ -1,6 +1,6 @@ import authorizationEndpoint from './Authentication/authorizationEndpoint'; -import UILink from './UILink'; -import UIPrimaryTitle from './UIPrimaryTitle'; +import UILink from './UI/UILink'; +import UIPrimaryTitle from './UI/UIPrimaryTitle'; export default function WheelShareLoggedOut() { return ( diff --git a/src/components/api.ts b/src/components/api.ts index 98c8d86..7043d0e 100644 --- a/src/components/api.ts +++ b/src/components/api.ts @@ -1,4 +1,4 @@ -import { IEventSignup } from './Event'; +import { IEventSignup } from './Event/Event'; import { GroupPreview } from './GroupJoinerLink'; import { IInvitation } from './types'; diff --git a/src/components/dates.ts b/src/components/dates.ts new file mode 100644 index 0000000..8c191b3 --- /dev/null +++ b/src/components/dates.ts @@ -0,0 +1,27 @@ +export default function formatStartAndEndTime( + startDatetimeString: string, + endDatetimeString: string +) { + const startDatetime = new Date(startDatetimeString); + const endDatetime = new Date(endDatetimeString); + + if (isNaN(startDatetime.valueOf())) { + console.error('Invalid datetime:', startDatetimeString); + return '(invalid)'; + } + if (isNaN(endDatetime.valueOf())) { + console.error('Invalid datetime:', startDatetimeString); + return '(invalid)'; + } + + const startDateString = startDatetime.toLocaleDateString(); + const endDateString = endDatetime.toLocaleDateString(); + + if (startDateString === endDateString) { + const startTimeString = startDatetime.toLocaleTimeString(); + const endTimeString = endDatetime.toLocaleTimeString(); + return `${startDateString}, ${startTimeString} - ${endTimeString}`; + } else { + return `${startDatetime.toLocaleString()} - ${endDatetime.toLocaleString()}`; + } +}