From 6529d1c39fb246858918a3089095d1be48b9aa07 Mon Sep 17 00:00:00 2001 From: Michael Fatemi <myfatemi04@gmail.com> Date: Thu, 15 Jul 2021 18:40:26 -0400 Subject: [PATCH] UI improvements --- src/components/App.tsx | 6 +- src/components/Carpool/CarpoolPage.tsx | 8 +- src/components/Event/Event.tsx | 130 +++--------------- src/components/Event/EventDetails.tsx | 15 +- src/components/Event/EventHooks.ts | 4 + src/components/Event/EventInterestForm.tsx | 84 +++++++++++ src/components/Event/EventPage.tsx | 8 +- src/components/Event/EventPlaceholder.tsx | 7 + src/components/EventCreator/EventCreator.tsx | 2 +- .../EventCreator/EventCreatorLink.tsx | 15 +- src/components/Group/Group.tsx | 46 ++++--- .../Group/GroupInviteCodeGenerator.tsx | 16 ++- src/components/Group/GroupMembersLink.tsx | 7 +- src/components/Group/GroupSettings.tsx | 2 +- src/components/Group/GroupSettingsLink.tsx | 15 +- src/components/WheelShare.tsx | 3 - 16 files changed, 172 insertions(+), 196 deletions(-) create mode 100644 src/components/Event/EventInterestForm.tsx create mode 100644 src/components/Event/EventPlaceholder.tsx diff --git a/src/components/App.tsx b/src/components/App.tsx index a9f8780..6de316d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,6 +1,7 @@ import { CSSProperties, lazy, Suspense } from 'react'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; import NotificationsProvider from '../state/Notifications/NotificationsProvider'; +import Header from './Header/Header'; import { useMe } from './hooks'; import WheelShare from './WheelShare'; import WheelShareLoggedOut from './WheelShareLoggedOut'; @@ -31,11 +32,8 @@ export default function App() { <Switch> {user ? ( <NotificationsProvider> + <Header /> <Route path="/" exact component={WheelShare} /> - <Route - component={Authenticator} - path="/auth/:provider/callback" - /> <Route path="/carpools/:id" component={CarpoolPage} /> <Route path="/events/:id" component={EventPage} /> <Route path="/groups/:id" component={Group} /> diff --git a/src/components/Carpool/CarpoolPage.tsx b/src/components/Carpool/CarpoolPage.tsx index a7c381c..71cdf65 100644 --- a/src/components/Carpool/CarpoolPage.tsx +++ b/src/components/Carpool/CarpoolPage.tsx @@ -1,14 +1,8 @@ import { useParams } from 'react-router-dom'; -import Header from '../Header/Header'; import Carpool from './Carpool'; export default function CarpoolPage() { const id = +useParams<{ id: string }>().id; - return ( - <> - <Header /> - <Carpool id={id} /> - </> - ); + return <Carpool id={id} />; } diff --git a/src/components/Event/Event.tsx b/src/components/Event/Event.tsx index f84c824..13ebae1 100644 --- a/src/components/Event/Event.tsx +++ b/src/components/Event/Event.tsx @@ -1,19 +1,14 @@ import { useCallback, useEffect, useState } from 'react'; -import { green, lightgrey } from '../../lib/colors'; -import getPlaceDetails from '../../lib/getPlaceDetails'; -import { addOrUpdateEventSignup, getEvent, removeEventSignup } from '../api'; -import { useMe } from '../hooks'; +import { getEvent } from '../api'; import { IEvent } from '../types'; -import UIButton from '../UI/UIButton'; import UILink from '../UI/UILink'; -import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete'; import UISecondaryBox from '../UI/UISecondaryBox'; import UISecondaryHeader from '../UI/UISecondaryHeader'; import useImmutable from '../useImmutable'; -import EventCarpools from './EventCarpools'; import EventContext from './EventContext'; import EventDetails from './EventDetails'; -import EventSignups from './EventSignups'; +import EventInterestForm from './EventInterestForm'; +import EventPlaceholder from './EventPlaceholder'; function GroupName({ group }: { group: IEvent['group'] }) { return <UILink href={`/groups/${group.id}`}>{group.name}</UILink>; @@ -26,89 +21,28 @@ export default function Event({ id: number; initial?: IEvent; }) { - const [event, setEvent] = useImmutable<IEvent>({ - id, - name: '', - group: { - id: 0, - name: '', - }, - signups: {}, - carpools: [], - startTime: '', - endTime: '', - daysOfWeek: 0, - placeId: '', - formattedAddress: '', - latitude: 0, - longitude: 0, - duration: 0, - ...(initial || {}), - }); - - const [found, setFound] = useState(false); - - const me = useMe() || { id: 0, name: '' }; - + const [event, setEvent] = useImmutable<IEvent | null>(initial ?? null); + const [loading, setLoading] = useState(true); const [tentativeInvites] = useImmutable<Record<number, boolean>>({}); const refresh = useCallback(() => { - getEvent(id).then((e) => { - if (e) { - setFound(true); - setEvent(e); - } else { - setFound(false); - } - }); + setLoading(true); + getEvent(id) + .then((e) => e && setEvent(e)) + .finally(() => setLoading(false)); }, [id, setEvent]); useEffect(refresh, [refresh]); - const updateSignup = useCallback( - async (placeId: string | null) => { - await addOrUpdateEventSignup(id, placeId); - - if (placeId) { - const details = await getPlaceDetails(placeId); - - event.signups[me.id] = { - user: { id: me.id, name: me.name }, - placeId, - ...details, - }; - } else { - event.signups[me.id] = { - user: { id: me.id, name: me.name }, - placeId: null, - latitude: null, - longitude: null, - formattedAddress: null, - }; - } - }, - [event.signups, id, me.id, me.name] - ); - - const removeSignup = useCallback(async () => { - await removeEventSignup(id); - - if (event.signups[me.id]) { - delete event.signups[me.id]; - } - }, [id, me.id, event.signups]); - - const interested = !!event.signups[me.id]; - - if (!found) { - return ( - <> - <h1>Event Not Found</h1> - </> - ); + if (loading) { + return <EventPlaceholder />; } - const { name, group, formattedAddress, startTime, endTime } = event; + if (!event) { + return <h1>Event Not Found</h1>; + } + + const { name, group } = event; return ( <EventContext.Provider @@ -124,36 +58,8 @@ export default function Event({ <UISecondaryHeader>{name}</UISecondaryHeader> {group && <GroupName group={group} />} </div> - <EventDetails {...{ startTime, endTime, formattedAddress }} /> - <UIButton - onClick={interested ? () => removeSignup() : () => updateSignup(null)} - 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) => { - updateSignup(placeId); - }} - style={ - event.signups[me.id]?.placeId != null - ? { border: '2px solid ' + green } - : {} - } - placeId={event.signups[me.id]?.placeId} - /> - <br /> - <EventCarpools /> - {event.signups !== null && <EventSignups />} - </> - )} + <EventDetails /> + <EventInterestForm /> </UISecondaryBox> </EventContext.Provider> ); diff --git a/src/components/Event/EventDetails.tsx b/src/components/Event/EventDetails.tsx index e9363e2..2952308 100644 --- a/src/components/Event/EventDetails.tsx +++ b/src/components/Event/EventDetails.tsx @@ -1,16 +1,13 @@ import formatStartAndEndTime from '../../lib/dates'; import EventIcon from '@material-ui/icons/Event'; import LocationOnIcon from '@material-ui/icons/LocationOn'; +import { useContext } from 'react'; +import EventContext from './EventContext'; + +export default function EventDetails() { + const { startTime, endTime, formattedAddress } = + useContext(EventContext).event; -export default function Details({ - startTime, - endTime, - formattedAddress, -}: { - startTime: string; - endTime: string | null; - formattedAddress: string; -}) { return ( <div style={{ diff --git a/src/components/Event/EventHooks.ts b/src/components/Event/EventHooks.ts index 1875960..552ef4f 100644 --- a/src/components/Event/EventHooks.ts +++ b/src/components/Event/EventHooks.ts @@ -37,3 +37,7 @@ export function useMyCarpool() { return carpool; } + +export function useMutableEvent() { + return useContext(EventContext).event; +} diff --git a/src/components/Event/EventInterestForm.tsx b/src/components/Event/EventInterestForm.tsx new file mode 100644 index 0000000..e4d3797 --- /dev/null +++ b/src/components/Event/EventInterestForm.tsx @@ -0,0 +1,84 @@ +import { useCallback } from 'react'; +import { green, lightgrey } from '../../lib/colors'; +import getPlaceDetails from '../../lib/getPlaceDetails'; +import { addOrUpdateEventSignup, removeEventSignup } from '../api'; +import { useMe } from '../hooks'; +import UIButton from '../UI/UIButton'; +import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete'; +import EventCarpools from './EventCarpools'; +import { useMutableEvent } from './EventHooks'; +import EventSignups from './EventSignups'; + +export default function EventInterestForm() { + const event = useMutableEvent(); + const me = useMe() || { id: 0, name: '' }; + + const updateSignup = useCallback( + async (placeId: string | null) => { + await addOrUpdateEventSignup(event.id, placeId); + + if (placeId) { + const details = await getPlaceDetails(placeId); + + event.signups[me.id] = { + user: { id: me.id, name: me.name }, + placeId, + ...details, + }; + } else { + event.signups[me.id] = { + user: { id: me.id, name: me.name }, + placeId: null, + latitude: null, + longitude: null, + formattedAddress: null, + }; + } + }, + [event.id, event.signups, me.id, me.name] + ); + + const removeSignup = useCallback(async () => { + await removeEventSignup(event.id); + + if (event.signups[me.id]) { + delete event.signups[me.id]; + } + }, [event.id, event.signups, me.id]); + + const interested = !!event.signups[me.id]; + + return ( + <> + <UIButton + onClick={interested ? () => removeSignup() : () => updateSignup(null)} + 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) => { + updateSignup(placeId); + }} + style={ + event.signups[me.id]?.placeId != null + ? { border: '2px solid ' + green } + : {} + } + placeId={event.signups[me.id]?.placeId} + /> + <br /> + <EventCarpools /> + {event.signups !== null && <EventSignups />} + </> + )} + </> + ); +} diff --git a/src/components/Event/EventPage.tsx b/src/components/Event/EventPage.tsx index c18acee..e503e7c 100644 --- a/src/components/Event/EventPage.tsx +++ b/src/components/Event/EventPage.tsx @@ -1,14 +1,8 @@ import { useParams } from 'react-router-dom'; -import Header from '../Header/Header'; import Event from './Event'; export default function EventPage() { const id = +useParams<{ id: string }>().id; - return ( - <> - <Header /> - <Event id={id} /> - </> - ); + return <Event id={id} />; } diff --git a/src/components/Event/EventPlaceholder.tsx b/src/components/Event/EventPlaceholder.tsx new file mode 100644 index 0000000..f108f7d --- /dev/null +++ b/src/components/Event/EventPlaceholder.tsx @@ -0,0 +1,7 @@ +import UISecondaryBox from '../UI/UISecondaryBox'; + +export default function EventPlaceholder() { + return ( + <UISecondaryBox style={{ height: '10rem' }}>Loading...</UISecondaryBox> + ); +} diff --git a/src/components/EventCreator/EventCreator.tsx b/src/components/EventCreator/EventCreator.tsx index 8da8878..7133d71 100644 --- a/src/components/EventCreator/EventCreator.tsx +++ b/src/components/EventCreator/EventCreator.tsx @@ -84,7 +84,7 @@ export default function EventCreator({ group }: { group: IGroup }) { ]); return ( - <UISecondaryBox style={{ width: '100%', boxSizing: 'border-box' }}> + <UISecondaryBox style={{ width: '100%', textAlign: 'center' }}> <h1 style={{ textAlign: 'center', marginBottom: '0.5rem' }}> Create Event </h1> diff --git a/src/components/EventCreator/EventCreatorLink.tsx b/src/components/EventCreator/EventCreatorLink.tsx index ce0b10c..2310b14 100644 --- a/src/components/EventCreator/EventCreatorLink.tsx +++ b/src/components/EventCreator/EventCreatorLink.tsx @@ -1,5 +1,6 @@ import { useContext } from 'react'; import { GroupContext } from '../Group/Group'; +import UIPressable from '../UI/UIPressable'; import useToggle from '../useToggle'; import EventCreator from './EventCreator'; @@ -8,22 +9,14 @@ export default function EventCreatorLink() { const { group } = useContext(GroupContext); return ( - <div> - <div - style={{ - cursor: 'pointer', - userSelect: 'none', - }} - onClick={toggle} - > - Create Event - </div> + <> + <UIPressable onClick={toggle}>Create Event</UIPressable> {open && ( <> <br /> <EventCreator group={group} /> </> )} - </div> + </> ); } diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 77a47f5..009d5cd 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -19,48 +19,50 @@ const DEFAULT_GROUP = (): IGroup => ({ export const GroupContext = createContext({ group: DEFAULT_GROUP() }); export default function Group({ id }: { id: number }) { - const [group, setGroup] = useImmutable<IGroup>(DEFAULT_GROUP()); - const [found, setFound] = useState(false); + const [group, setGroup] = useImmutable<IGroup | null>(null); + const [loading, setLoading] = useState(false); useEffect(() => { + setLoading(true); getGroup(id) .then(setGroup) - .catch(() => setFound(false)); + .finally(() => setLoading(false)); }, [id, setGroup]); - return found ? ( + if (loading) { + return <h1>Loading...</h1>; + } + + return group ? ( <GroupContext.Provider value={{ group }}> + <h1>{group.name}</h1> <div style={{ - textAlign: 'center', - maxWidth: '30rem', - marginLeft: 'auto', - marginRight: 'auto', + display: 'flex', + flexDirection: 'column', + width: '100%', + alignItems: 'center', }} > - <h1>{group.name}</h1> <UILink href="/">Home</UILink> <br /> - <br /> <GroupMembersLink /> <br /> <GroupSettingsLink /> <br /> <EventCreatorLink /> - <br /> - - {group.events.length > 0 ? ( - <EventStream events={group.events} /> - ) : ( - <span> - There are no events yet. Click 'create event' above to add one! - </span> - )} </div> + <br /> + + {group.events.length > 0 ? ( + <EventStream events={group.events} /> + ) : ( + <span> + There are no events yet. Click 'create event' above to add one! + </span> + )} </GroupContext.Provider> ) : ( - <> - <h1>Group not found</h1> - </> + <h1>Group not found</h1> ); } diff --git a/src/components/Group/GroupInviteCodeGenerator.tsx b/src/components/Group/GroupInviteCodeGenerator.tsx index caecffe..31a8a40 100644 --- a/src/components/Group/GroupInviteCodeGenerator.tsx +++ b/src/components/Group/GroupInviteCodeGenerator.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext } from 'react'; +import { useCallback, useContext, useState } from 'react'; import { lightgrey } from '../../lib/colors'; import { generateCode, resetCode } from '../api'; import UIButton from '../UI/UIButton'; @@ -7,6 +7,8 @@ import { GroupContext } from './Group'; export default function GroupInviteCodeGenerator() { const { group } = useContext(GroupContext); + const [shown, setShown] = useState(false); + const generateJoinCode = useCallback(() => { generateCode(group.id).then((code) => { group.joinCode = code; @@ -22,11 +24,15 @@ export default function GroupInviteCodeGenerator() { if (group.joinCode) { return ( <> - <span> + <span style={{ userSelect: 'none' }}> Join this group with the code{' '} - <b> - <code>{group.joinCode}</code> - </b> + <code + style={{ userSelect: 'text' }} + onClick={() => setShown((shown) => !shown)} + > + {shown ? group.joinCode : 'XXXXXX'} + </code>{' '} + (click to show/hide) </span> <div style={{ display: 'flex', justifyContent: 'space-between' }}> <UIButton diff --git a/src/components/Group/GroupMembersLink.tsx b/src/components/Group/GroupMembersLink.tsx index 9c030a2..136b7e8 100644 --- a/src/components/Group/GroupMembersLink.tsx +++ b/src/components/Group/GroupMembersLink.tsx @@ -17,14 +17,15 @@ export default function GroupMembersLink() { {open && ( <> <br /> - <UISecondaryBox> + <UISecondaryBox style={{ width: '100%', textAlign: 'center' }}> <h1>Members</h1> - <GroupInviteCodeGenerator /> - {group.users.map(({ name }) => ( <span key={name}>{name}</span> ))} + <br /> + + <GroupInviteCodeGenerator /> </UISecondaryBox> </> )} diff --git a/src/components/Group/GroupSettings.tsx b/src/components/Group/GroupSettings.tsx index fb1d9ff..b7a5f0b 100644 --- a/src/components/Group/GroupSettings.tsx +++ b/src/components/Group/GroupSettings.tsx @@ -20,7 +20,7 @@ export default function GroupSettings({ group }: { group: IGroup }) { }, [group.id]); return ( - <UISecondaryBox> + <UISecondaryBox style={{ width: '100%', textAlign: 'center' }}> <h1>Settings</h1> {deletionSuccessful !== true && ( <UIPressable onClick={onClickedDelete}>Delete Group</UIPressable> diff --git a/src/components/Group/GroupSettingsLink.tsx b/src/components/Group/GroupSettingsLink.tsx index a3ad286..ccdc50e 100644 --- a/src/components/Group/GroupSettingsLink.tsx +++ b/src/components/Group/GroupSettingsLink.tsx @@ -1,4 +1,5 @@ import { useContext } from 'react'; +import UIPressable from '../UI/UIPressable'; import useToggle from '../useToggle'; import { GroupContext } from './Group'; import GroupSettings from './GroupSettings'; @@ -8,22 +9,14 @@ export default function GroupSettingsLink() { const { group } = useContext(GroupContext); return ( - <div> - <div - style={{ - cursor: 'pointer', - userSelect: 'none', - }} - onClick={toggle} - > - Settings - </div> + <> + <UIPressable onClick={toggle}>Settings</UIPressable> {open && ( <> <br /> <GroupSettings group={group} /> </> )} - </div> + </> ); } diff --git a/src/components/WheelShare.tsx b/src/components/WheelShare.tsx index c3b4970..d1670d8 100644 --- a/src/components/WheelShare.tsx +++ b/src/components/WheelShare.tsx @@ -1,13 +1,10 @@ import ActiveCarpools from './ActiveCarpools/ActiveCarpools'; import ActiveEvents from './ActiveEvents/Events'; import Groups from './Groups/Groups'; -import Header from './Header/Header'; export default function WheelShare() { return ( <> - <Header /> - <Groups /> <ActiveCarpools /> <ActiveEvents />