From 41a8a578548831674bdc6593edba0c3bc5ab25d8 Mon Sep 17 00:00:00 2001
From: Michael Fatemi <myfatemi04@gmail.com>
Date: Tue, 13 Jul 2021 18:50:37 -0400
Subject: [PATCH] add requesting/cancelling request (notifications provider)

---
 src/components/App.tsx                        | 41 +++++++-------
 src/components/Carpool/MemberList.tsx         | 41 ++++++++++++--
 src/components/api.ts                         |  8 +++
 .../Notifications/NotificationsHooks.tsx      | 21 ++++++++
 .../Notifications/NotificationsProvider.tsx   | 54 +++++++++++++++++++
 5 files changed, 142 insertions(+), 23 deletions(-)
 create mode 100644 src/state/Notifications/NotificationsHooks.tsx
 create mode 100644 src/state/Notifications/NotificationsProvider.tsx

diff --git a/src/components/App.tsx b/src/components/App.tsx
index f098788..40b03b2 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -1,5 +1,6 @@
 import { CSSProperties, lazy, Suspense } from 'react';
 import { BrowserRouter, Route, Switch } from 'react-router-dom';
+import NotificationsProvider from '../state/Notifications/NotificationsProvider';
 import { useMe } from './hooks';
 import WheelShare from './WheelShare';
 import WheelShareLoggedOut from './WheelShareLoggedOut';
@@ -22,27 +23,29 @@ const style: CSSProperties = {
 export default function App() {
 	const user = useMe();
 	return (
-		<div style={{ padding: '1rem', maxWidth: '100vw' }}>
-			<div style={style}>
-				<BrowserRouter>
-					<Switch>
-						<Route
-							path="/"
-							exact
-							component={user ? WheelShare : WheelShareLoggedOut}
-						/>
-						<Suspense fallback={null}>
-							<Route path="/groups/:id" component={Group} />
+		<NotificationsProvider>
+			<div style={{ padding: '1rem', maxWidth: '100vw' }}>
+				<div style={style}>
+					<BrowserRouter>
+						<Switch>
 							<Route
-								component={Authenticator}
-								path="/auth/:provider/callback"
+								path="/"
+								exact
+								component={user ? WheelShare : WheelShareLoggedOut}
 							/>
-							<Route path="/carpools/:id" component={CarpoolPage} />
-							<Route path="/events/:id" component={EventPage} />
-						</Suspense>
-					</Switch>
-				</BrowserRouter>
+							<Suspense fallback={null}>
+								<Route path="/groups/:id" component={Group} />
+								<Route
+									component={Authenticator}
+									path="/auth/:provider/callback"
+								/>
+								<Route path="/carpools/:id" component={CarpoolPage} />
+								<Route path="/events/:id" component={EventPage} />
+							</Suspense>
+						</Switch>
+					</BrowserRouter>
+				</div>
 			</div>
-		</div>
+		</NotificationsProvider>
 	);
 }
diff --git a/src/components/Carpool/MemberList.tsx b/src/components/Carpool/MemberList.tsx
index aac57ad..31a2150 100644
--- a/src/components/Carpool/MemberList.tsx
+++ b/src/components/Carpool/MemberList.tsx
@@ -1,6 +1,11 @@
 import AccountCircleIcon from '@material-ui/icons/AccountCircle';
+import { useCallback } from 'react';
+import { useMemo } from 'react';
 import { useContext } from 'react';
+import { useInvitationState } from '../../state/Notifications/NotificationsHooks';
+import { NotificationsContext } from '../../state/Notifications/NotificationsProvider';
 import { lightgrey } from '../colors';
+import { useMe } from '../hooks';
 import UIButton from '../UI/UIButton';
 import { CarpoolContext } from './Carpool';
 
@@ -21,9 +26,26 @@ export default function MemberList({
 		name: string;
 	}[];
 }) {
-	const { leave } = useContext(CarpoolContext);
+	const { leave, carpool } = useContext(CarpoolContext);
 	const membersToShow = members.slice(0, 2);
 	const hiddenMemberCount = members.length - membersToShow.length;
+	const me = useMe()!;
+
+	const isMember = useMemo(() => {
+		return members.some(({ id }) => id === me.id);
+	}, [me.id, members]);
+
+	const { sendCarpoolRequest, cancelCarpoolRequest } =
+		useContext(NotificationsContext);
+	const invitationState = useInvitationState(carpool.id);
+
+	const sendRequest = useCallback(() => {
+		sendCarpoolRequest(carpool.id);
+	}, [carpool.id, sendCarpoolRequest]);
+
+	const cancelRequest = useCallback(() => {
+		cancelCarpoolRequest(carpool.id);
+	}, [carpool.id, cancelCarpoolRequest]);
 
 	return (
 		<div
@@ -48,9 +70,20 @@ export default function MemberList({
 			) : (
 				'This carpool has no members.'
 			)}
-			<UIButton onClick={leave} style={{ backgroundColor: lightgrey }}>
-				Leave
-			</UIButton>
+
+			{isMember ? (
+				<UIButton onClick={leave} style={{ backgroundColor: lightgrey }}>
+					Leave
+				</UIButton>
+			) : invitationState === 'requested' ? (
+				<UIButton onClick={cancelRequest}>Cancel request to join</UIButton>
+			) : invitationState === 'none' ? (
+				<UIButton onClick={sendRequest}>Request to join</UIButton>
+			) : (
+				<span>
+					You've been invited, we need to make it so you can accept the invite
+				</span>
+			)}
 		</div>
 	);
 }
diff --git a/src/components/api.ts b/src/components/api.ts
index 0b26b31..7e01c8c 100644
--- a/src/components/api.ts
+++ b/src/components/api.ts
@@ -193,3 +193,11 @@ export async function cancelCarpoolInvite(carpoolId: number, userId: number) {
 export async function leaveCarpool(carpoolId: number) {
 	return await post(`/carpools/${carpoolId}/leave`, {});
 }
+
+export async function sendCarpoolRequest(carpoolId: number) {
+	return await post('/carpools/' + carpoolId + '/request', {});
+}
+
+export async function cancelCarpoolRequest(carpoolId: number) {
+	return await delete$('/carpools/' + carpoolId + '/request');
+}
diff --git a/src/state/Notifications/NotificationsHooks.tsx b/src/state/Notifications/NotificationsHooks.tsx
new file mode 100644
index 0000000..5959238
--- /dev/null
+++ b/src/state/Notifications/NotificationsHooks.tsx
@@ -0,0 +1,21 @@
+import { useMemo } from 'react';
+import { useContext } from 'react';
+import { NotificationsContext } from './NotificationsProvider';
+
+export function useInvitationState(
+	carpoolId: number
+): 'invited' | 'requested' | 'none' {
+	const notifications = useContext(NotificationsContext);
+
+	const invited = useMemo(
+		() => notifications.invitedCarpoolIds.has(carpoolId),
+		[carpoolId, notifications.invitedCarpoolIds]
+	);
+
+	const requested = useMemo(
+		() => notifications.requestedCarpoolIds.has(carpoolId),
+		[carpoolId, notifications.requestedCarpoolIds]
+	);
+
+	return invited ? 'invited' : requested ? 'requested' : 'none';
+}
diff --git a/src/state/Notifications/NotificationsProvider.tsx b/src/state/Notifications/NotificationsProvider.tsx
new file mode 100644
index 0000000..be3d119
--- /dev/null
+++ b/src/state/Notifications/NotificationsProvider.tsx
@@ -0,0 +1,54 @@
+import { createContext, ReactNode, useCallback, useState } from 'react';
+import * as immutable from 'immutable';
+import * as api from '../../components/api';
+
+export const NotificationsContext = createContext({
+	invitedCarpoolIds: immutable.Set<number>(),
+	requestedCarpoolIds: immutable.Set<number>(),
+
+	sendCarpoolRequest: (carpoolId: number) =>
+		console.error('not implemented: sendCarpoolRequest'),
+
+	cancelCarpoolRequest: (carpoolId: number) =>
+		console.error('not implemented: cancelCarpoolRequest'),
+});
+
+export default function NotificationsProvider({
+	children,
+}: {
+	children: ReactNode;
+}) {
+	// eslint-disable-next-line
+	const [invitedCarpoolIds, _setInvitedCarpoolIds] = useState(
+		immutable.Set<number>()
+	);
+
+	const [requestedCarpoolIds, setRequestedCarpoolIds] = useState(
+		immutable.Set<number>()
+	);
+
+	const sendCarpoolRequest = useCallback((carpoolId: number) => {
+		api
+			.sendCarpoolRequest(carpoolId)
+			.then(() => setRequestedCarpoolIds((ids) => ids.add(carpoolId)));
+	}, []);
+
+	const cancelCarpoolRequest = useCallback((carpoolId: number) => {
+		api
+			.cancelCarpoolRequest(carpoolId)
+			.then(() => setRequestedCarpoolIds((ids) => ids.delete(carpoolId)));
+	}, []);
+
+	return (
+		<NotificationsContext.Provider
+			value={{
+				invitedCarpoolIds,
+				requestedCarpoolIds,
+				sendCarpoolRequest,
+				cancelCarpoolRequest,
+			}}
+		>
+			{children}
+		</NotificationsContext.Provider>
+	);
+}