Skip to content

Commit

Permalink
Restrict Navigation Bar Links Based on User Authentication Status (#27)
Browse files Browse the repository at this point in the history
* removed useAuth call in Layout to pass user info from app.tsx usage of useAuth

* add user check on navbar to make sure only shows if log in, and update login buttons for user in or out

* add ProtectedRoute to redirect navigation if not logged in to /

* moved react-hot-toast Toaster to top level app to not have to use in each file toast is needed

* updating signout button to redirect to home page on success & toast error to retry on error

* added a PageNotFound componet with a catch all route incase manual nav to nonexistent page

* removed unused import in useAuth

* update useAuth to have useFindUser & useGetUser

* updated protectedRoute to handle the User context needed in Outlet components

* wrap all protected routes in one ProtectedRoute call

* changes to short-circuit evaluations of the signin button and navbar in layout

* moved navbar to own component

* clean up the Toaster import on the files since it moved to the root

* updated name of NavBar to reflect it is authenticated version

* removing sign out button redirect since now handled in the protectedRoute

* updated ShareListForm to utilize getUser in the ProtectedRoute hierarchy

* update src/components index.ts to include ShareListForm
  • Loading branch information
bbland1 authored Sep 1, 2024
1 parent a1086ef commit 0c74a03
Show file tree
Hide file tree
Showing 14 changed files with 253 additions and 160 deletions.
44 changes: 30 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import React from "react";

import { Home, Layout, List, ManageList } from "./views";
import { Routes, Route } from "react-router-dom";

import { useAuth, useShoppingListData, useShoppingLists } from "./api";
import { Home, Layout, List, ManageList, PageNotFound } from "./views";

import { useFindUser, useShoppingListData, useShoppingLists } from "./api";

import { useStateWithStorage } from "./utils";
import { ProtectRoute } from "./components";

/**
* Putting Toaster at the top level of the App allows
* react-hot-toast to work anywhere in the app by just
* importing toast as done in useAuth.
*/

import { Toaster } from "react-hot-toast";

Expand All @@ -26,7 +35,7 @@ export function App() {
* This custom hook holds info about the current signed in user.
* Check ./api/useAuth.jsx for its implementation.
*/
const { user } = useAuth();
const { user } = useFindUser();

/**
* This custom hook takes a user ID and email and fetches
Expand All @@ -42,23 +51,30 @@ export function App() {
const data = useShoppingListData(listPath);

return (
<Router>
<>
<Toaster />
<Routes>
<Route path="/" element={<Layout />}>
<Route path="/" element={<Layout user={user} />}>
<Route
index
path="/"
element={
<Home data={lists} setListPath={setListPath} user={user} />
}
/>
<Route path="/list" element={<List data={data} />} />
<Route
path="/manage-list"
element={<ManageList listPath={listPath} />}
/>

{/* protected routes */}
<Route element={<ProtectRoute user={user} redirectPath="/" />}>
<Route path="/list" element={<List data={data} />} />
<Route
path="/manage-list"
element={<ManageList listPath={listPath} />}
/>
</Route>

{/* a catch all route for if someone tries to manually navigate to something not created yet */}
<Route path="*" element={<PageNotFound />} />
</Route>
</Routes>
<Toaster />
</Router>
</>
);
}
2 changes: 1 addition & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./firebase";
export { useAuth } from "./useAuth";
export { useFindUser, SignInButton, SignOutButton } from "./useAuth";
28 changes: 21 additions & 7 deletions src/api/useAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { auth } from "./config.js";
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import { addUserToDatabase, User } from "./firebase";
import toast from "react-hot-toast";

/**
* A button that signs the user in using Google OAuth. When clicked,
Expand All @@ -20,22 +21,32 @@ export const SignInButton = () => (
/**
* A button that signs the user out of the app using Firebase Auth.
*/
export const SignOutButton = () => (
<button type="button" onClick={() => auth.signOut()}>
Sign Out
</button>
);
export const SignOutButton = () => {
return (
<button
type="button"
onClick={() => {
auth.signOut().catch((error) => {
console.error(error);
toast.error("An error occurred while signing out. Please try again.");
});
}}
>
Sign Out
</button>
);
};

/**
* A custom hook that listens for changes to the user's auth state.
* Check out the Firebase docs for more info on auth listeners:
* @see https://firebase.google.com/docs/auth/web/start#set_an_authentication_state_observer_and_get_user_data
*/
export const useAuth = () => {
export const useFindUser = () => {
const [user, setUser] = useState<User | null>(null);

useEffect(() => {
auth.onAuthStateChanged((firebaseUser) => {
const unsubscribe = auth.onAuthStateChanged((firebaseUser) => {
if (firebaseUser === null) {
setUser(null);
return;
Expand All @@ -54,6 +65,9 @@ export const useAuth = () => {
setUser(user);
addUserToDatabase(user);
});

// Cleanup the subscription when the component unmounts
return () => unsubscribe();
}, []);

return { user };
Expand Down
35 changes: 35 additions & 0 deletions src/components/AuthenticatedNavBar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.Nav {
background-color: var(--color-bg);
border-top: 1px solid var(--color-border);
bottom: 0;
display: flex;
flex-direction: row;
padding-bottom: max(env(safe-area-inset-bottom), 1rem);
padding-top: 1rem;
place-content: center;
position: fixed;
width: 100%;
}

.Nav-container {
display: flex;
flex-direction: row;
justify-content: space-evenly;
width: min(72ch, 100%);
}

.Nav-link {
--color-text: var(--color-accent);
color: var(--color-text);
font-size: 1.4em;
flex: 0 1 auto;
line-height: 1;
padding: 0.8rem;
text-align: center;
text-underline-offset: 0.1em;
}

.Nav-link.active {
text-decoration-thickness: 0.22em;
text-underline-offset: 0.1em;
}
28 changes: 28 additions & 0 deletions src/components/AuthenticatedNavBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";
import { NavLink } from "react-router-dom";
import { SignOutButton } from "../api";

import "./AuthenticatedNavBar.css";

export function AuthenticatedNavBar() {
return (
<nav className="Nav">
<div className="Nav-container">
<SignOutButton />
<NavLink to="/" className="Nav-link" aria-label="Home">
Home
</NavLink>
<NavLink to="/list" className="Nav-link" aria-label="List">
List
</NavLink>
<NavLink
to="/manage-list"
className="Nav-link"
aria-label="Manage List"
>
Manage List
</NavLink>
</div>
</nav>
);
}
30 changes: 30 additions & 0 deletions src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";
import { Navigate, Outlet, useOutletContext } from "react-router-dom";
import { User } from "../api";

interface Props {
user: User | null;
redirectPath: string;
}

type ProtectedRouteProps = { user: User };

export function ProtectRoute({ user, redirectPath }: Props) {
return user ? (
<Outlet context={{ user } satisfies ProtectedRouteProps} />
) : (
<Navigate to={redirectPath} />
);
}

/**
Gets the `user` object from the context of the `Outlet` component it is called in.
The function has to be called within a nested component of a route protected by `ProtectRote`.
It will allow user-specific information without the needs to pass `user` explicity through props.
@returns {User}
*/
export function getUser() {
return useOutletContext<ProtectedRouteProps>();
}
12 changes: 4 additions & 8 deletions src/components/forms/ShareListForm.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { ChangeEvent, FormEvent, useState } from "react";
import { shareList } from "../../api/firebase";
import { shareList } from "../../api";
import { getUser } from "../ProtectedRoute";

import toast from "react-hot-toast";

import { useAuth } from "../../api/useAuth";

import { User } from "../../api/firebase";

interface Props {
listPath: string | null;
}

const ShareListForm = ({ listPath }: Props) => {
const { user: currentUser } = useAuth();
const { user: currentUser } = getUser();

const [emailName, setEmailName] = useState("");

Expand All @@ -24,15 +21,14 @@ const ShareListForm = ({ listPath }: Props) => {
e: FormEvent<HTMLFormElement>,
listPath: string | null,
) => {
console.log("Button clicked! Inviting user!");
e.preventDefault();

if (!listPath) {
return;
}

try {
await toast.promise(shareList(listPath, currentUser as User, emailName), {
await toast.promise(shareList(listPath, currentUser, emailName), {
loading: "sharing list with existing user",
success: () => {
setEmailName("");
Expand Down
3 changes: 3 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from "./ListItem";
export * from "./SingleList";
export * from "./CreateList";
export * from "./ProtectedRoute";
export * from "./AuthenticatedNavBar";
export * from "./forms/ShareListForm";
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import { App } from "./App";

import "./index.css";

const root = createRoot(document.getElementById("root") as HTMLElement);
root.render(
<StrictMode>
<App />
<Router>
<App />
</Router>
</StrictMode>,
);
36 changes: 0 additions & 36 deletions src/views/Layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,39 +34,3 @@
padding-block-end: 6.26rem;
width: min(72ch, 100%);
}

.Nav {
background-color: var(--color-bg);
border-top: 1px solid var(--color-border);
bottom: 0;
display: flex;
flex-direction: row;
padding-bottom: max(env(safe-area-inset-bottom), 1rem);
padding-top: 1rem;
place-content: center;
position: fixed;
width: 100%;
}

.Nav-container {
display: flex;
flex-direction: row;
justify-content: space-evenly;
width: min(72ch, 100%);
}

.Nav-link {
--color-text: var(--color-accent);
color: var(--color-text);
font-size: 1.4em;
flex: 0 1 auto;
line-height: 1;
padding: 0.8rem;
text-align: center;
text-underline-offset: 0.1em;
}

.Nav-link.active {
text-decoration-thickness: 0.22em;
text-underline-offset: 0.1em;
}
41 changes: 9 additions & 32 deletions src/views/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import React from "react";
import { Outlet, NavLink } from "react-router-dom";
import { useAuth, SignInButton, SignOutButton } from "../api/useAuth";
import { Outlet } from "react-router-dom";
import { SignInButton, User } from "../api";
import { AuthenticatedNavBar } from "../components";

import "./Layout.css";

// 1) import NavLink component

/**
* TODO: The links defined in this file don't work!
*
* Instead of anchor element, they should use a component
* from `react-router-dom` to navigate to the routes
* defined in `App.jsx`.
*/

export function Layout() {
const { user } = useAuth();
interface Props {
user: User | null;
}

export function Layout({ user }: Props) {
return (
<>
<div className="Layout">
Expand All @@ -25,25 +18,9 @@ export function Layout() {
</header>
<main className="Layout-main">
<Outlet />
{!user && <SignInButton />}
</main>
<nav className="Nav">
<div className="Nav-container">
{!!user ? <SignOutButton /> : <SignInButton />}
<NavLink to="/" className="Nav-link" aria-label="Home">
Home
</NavLink>
<NavLink to="/list" className="Nav-link" aria-label="List">
List
</NavLink>
<NavLink
to="/manage-list"
className="Nav-link"
aria-label="Manage List"
>
Manage List
</NavLink>
</div>
</nav>
{user && <AuthenticatedNavBar />}
</div>
</>
);
Expand Down
Loading

0 comments on commit 0c74a03

Please sign in to comment.