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> + ); +}