diff --git a/src/components/App.tsx b/src/components/App.tsx index e926e39..4345575 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,11 +1,11 @@ -import { lazy, Suspense, useContext, useState, useEffect } from 'react'; +import { CSSProperties, lazy, Suspense, useEffect, useState } from 'react'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; -import AuthenticationContext from './Authentication/AuthenticationContext'; +import { getReceivedInvitationsAndRequests } from './api'; +import { useMe } from './hooks'; +import Notifications from './Notifications'; +import { IInvitation } from './types'; import WheelShare from './WheelShare'; import WheelShareLoggedOut from './WheelShareLoggedOut'; -import Notifications from './Notifications'; -import { getReceivedInvitationsAndRequests } from './api'; -import { IInvitation } from './types'; const Authenticator = lazy(() => import('./Authentication/Authenticator')); const Group = lazy(() => import('./Group')); @@ -37,33 +37,54 @@ const dummyNotificationData: IInvitation[] = [ }, ]; -export default function App() { - const { user } = useContext(AuthenticationContext); - // eslint-disable-next-line +const style: CSSProperties = { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: '30rem', + maxWidth: '30rem', + marginLeft: 'auto', + marginRight: 'auto', +}; + +function useNotifications() { const [notifications, setNotifications] = useState(dummyNotificationData); + useEffect(() => { getReceivedInvitationsAndRequests().then(setNotifications); }, []); + + return notifications; +} + +export default function App() { + const user = useMe(); + const notifications = useNotifications(); return ( <div style={{ padding: '1rem' }}> - {notifications.length > 0 ? ( - <Notifications notifications={notifications} /> - ) : ( - <span>No notifications</span> - )} - <BrowserRouter> - <Switch> - <Route - path="/" - exact - component={user ? WheelShare : WheelShareLoggedOut} - /> - <Suspense fallback={null}> - <Route path="/groups/:id" component={Group} /> - <Route component={Authenticator} path="/auth/:provider/callback" /> - </Suspense> - </Switch> - </BrowserRouter> + <div style={style}> + {notifications.length > 0 ? ( + <Notifications notifications={notifications} /> + ) : ( + <span>No notifications</span> + )} + <BrowserRouter> + <Switch> + <Route + path="/" + exact + component={user ? WheelShare : WheelShareLoggedOut} + /> + <Suspense fallback={null}> + <Route path="/groups/:id" component={Group} /> + <Route + component={Authenticator} + path="/auth/:provider/callback" + /> + </Suspense> + </Switch> + </BrowserRouter> + </div> </div> ); } diff --git a/src/components/Authentication/Authenticator.tsx b/src/components/Authentication/Authenticator.tsx index f5a1cfa..ba95556 100644 --- a/src/components/Authentication/Authenticator.tsx +++ b/src/components/Authentication/Authenticator.tsx @@ -19,22 +19,13 @@ function inferRedirectUrl() { return redirectUrl; } -export default function Authenticator() { - const { provider } = useParams<{ provider: string }>(); - const [code, error] = useCodeAndError(); - const { refresh } = useContext(AuthenticationContext); - - const [pending, setPending] = useState(true); +function useToken( + code: string | null, + provider: string +): [string | null, boolean] { + const [pending, setPending] = useState(false); const [token, setToken] = useState<string | null>(null); - useEffect(() => { - if (token) { - localStorage.setItem('session_token', token); - } else { - localStorage.removeItem('session_token'); - } - }, [token]); - useEffect(() => { if (code) { setPending(true); @@ -46,10 +37,34 @@ export default function Authenticator() { } }, [code, provider]); + return [token, pending]; +} + +function useLocalStorageSync(key: string, value: string | null) { + useEffect(() => { + if (value) { + localStorage.setItem(key, value); + } else { + localStorage.removeItem(key); + } + }, [key, value]); +} + +function useRefresh(refresh: Function, watch: string | null) { useEffect(() => { - // Refresh when the token changes refresh(); - }, [token, refresh]); + }, [watch, refresh]); +} + +export default function Authenticator() { + const { provider } = useParams<{ provider: string }>(); + const [code, error] = useCodeAndError(); + const { refresh } = useContext(AuthenticationContext); + + const [token, pending] = useToken(code, provider); + + useLocalStorageSync('session_token', token); + useRefresh(refresh, token); let children: JSX.Element; @@ -86,15 +101,5 @@ export default function Authenticator() { ); } - return ( - <div - style={{ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - }} - > - {children} - </div> - ); + return children; } diff --git a/src/components/Notifications.tsx b/src/components/Notifications.tsx index 09d3ac1..5338312 100644 --- a/src/components/Notifications.tsx +++ b/src/components/Notifications.tsx @@ -10,9 +10,12 @@ export default function Notifications({ <div className="notifications"> <div style={{ display: 'flex', flexDirection: 'column' }}> <h3 style={{ marginBlockEnd: '0' }}>Notifications</h3> - {notifications.map((notification) => { - return <Notification notification={notification} />; - })} + {notifications.map((notification) => ( + <Notification + notification={notification} + key={`${notification.user.id}:${notification.carpool.id}`} + /> + ))} </div> </div> ); diff --git a/src/components/WheelShare.tsx b/src/components/WheelShare.tsx index 80bd3f8..fe8a1b2 100644 --- a/src/components/WheelShare.tsx +++ b/src/components/WheelShare.tsx @@ -1,4 +1,3 @@ -import { CSSProperties } from 'react'; import logout from './Authentication/logout'; import Carpool from './Carpool'; import Events from './Events'; @@ -7,21 +6,11 @@ import { useMe } from './hooks'; import UIPressable from './UIPressable'; import UIPrimaryTitle from './UIPrimaryTitle'; -const style: CSSProperties = { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - width: '30rem', - maxWidth: '30rem', - marginLeft: 'auto', - marginRight: 'auto', -}; - export default function WheelShare() { const user = useMe()!; return ( - <div style={style}> + <> <UIPrimaryTitle>WheelShare</UIPrimaryTitle> <Carpool @@ -40,6 +29,6 @@ export default function WheelShare() { <Groups /> <Events /> - </div> + </> ); } diff --git a/src/components/WheelShareLoggedOut.tsx b/src/components/WheelShareLoggedOut.tsx index b910e15..b1655d4 100644 --- a/src/components/WheelShareLoggedOut.tsx +++ b/src/components/WheelShareLoggedOut.tsx @@ -1,28 +1,30 @@ -import { CSSProperties } from 'react'; import UILink from './UILink'; import UIPrimaryTitle from './UIPrimaryTitle'; -const dev = true; -const ION_AUTHORIZATION_ENDPOINT = dev - ? 'https://ion.tjhsst.edu/oauth/authorize?response_type=code&client_id=rNa6n9YSg8ftINdyVPpUsaMuxNbHLo9dh1OsOktR&scope=read&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fion%2Fcallback' - : 'https://ion.tjhsst.edu/oauth/authorize?response_type=code&client_id=rNa6n9YSg8ftINdyVPpUsaMuxNbHLo9dh1OsOktR&scope=read&redirect_uri=https%3A%2F%2Fwheelshare.space%2Fauth%2Fion%2Fcallback'; +function createAuthorizationEndpoint(redirectUrl: string) { + const url = new URL('https://ion.tjhsst.edu/oauth/authorize'); -const style: CSSProperties = { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - width: '30rem', - maxWidth: '30rem', - marginLeft: 'auto', - marginRight: 'auto', -}; + url.searchParams.set('response_type', 'code'); + url.searchParams.set('client_id', 'rNa6n9YSg8ftINdyVPpUsaMuxNbHLo9dh1OsOktR'); + url.searchParams.set('redirect_uri', redirectUrl); + url.searchParams.set('scope', 'read'); + + return url.toString(); +} + +const dev = true; +const endpoint = createAuthorizationEndpoint( + dev + ? 'http://localhost:3000/auth/ion/callback' + : 'https://wheelshare.space/auth/ion/callback' +); export default function WheelShareLoggedOut() { return ( - <div style={style}> + <> <UIPrimaryTitle>WheelShare</UIPrimaryTitle> <p>To get started, log in with your Ion account.</p> - <UILink href={ION_AUTHORIZATION_ENDPOINT}>Log in</UILink> - </div> + <UILink href={endpoint}>Log in</UILink> + </> ); }