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 />