diff --git a/src/App.tsx b/src/App.tsx
index b299d8c..fc236ea 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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";
@@ -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
@@ -42,23 +51,30 @@ export function App() {
const data = useShoppingListData(listPath);
return (
-
+ <>
+
- }>
+ }>
}
/>
- } />
- }
- />
+
+ {/* protected routes */}
+ }>
+ } />
+ }
+ />
+
+
+ {/* a catch all route for if someone tries to manually navigate to something not created yet */}
+ } />
-
-
+ >
);
}
diff --git a/src/api/index.ts b/src/api/index.ts
index 2879bf0..aef6f44 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1,2 +1,2 @@
export * from "./firebase";
-export { useAuth } from "./useAuth";
+export { useFindUser, SignInButton, SignOutButton } from "./useAuth";
diff --git a/src/api/useAuth.tsx b/src/api/useAuth.tsx
index 4cb97d9..c15426e 100644
--- a/src/api/useAuth.tsx
+++ b/src/api/useAuth.tsx
@@ -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,
@@ -20,22 +21,32 @@ export const SignInButton = () => (
/**
* A button that signs the user out of the app using Firebase Auth.
*/
-export const SignOutButton = () => (
-
-);
+export const SignOutButton = () => {
+ return (
+
+ );
+};
/**
* 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(null);
useEffect(() => {
- auth.onAuthStateChanged((firebaseUser) => {
+ const unsubscribe = auth.onAuthStateChanged((firebaseUser) => {
if (firebaseUser === null) {
setUser(null);
return;
@@ -54,6 +65,9 @@ export const useAuth = () => {
setUser(user);
addUserToDatabase(user);
});
+
+ // Cleanup the subscription when the component unmounts
+ return () => unsubscribe();
}, []);
return { user };
diff --git a/src/components/AuthenticatedNavBar.css b/src/components/AuthenticatedNavBar.css
new file mode 100644
index 0000000..21cb43a
--- /dev/null
+++ b/src/components/AuthenticatedNavBar.css
@@ -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;
+}
diff --git a/src/components/AuthenticatedNavBar.tsx b/src/components/AuthenticatedNavBar.tsx
new file mode 100644
index 0000000..6441ee5
--- /dev/null
+++ b/src/components/AuthenticatedNavBar.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx
new file mode 100644
index 0000000..ec89550
--- /dev/null
+++ b/src/components/ProtectedRoute.tsx
@@ -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 ? (
+
+ ) : (
+
+ );
+}
+
+/**
+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();
+}
diff --git a/src/components/forms/ShareListForm.tsx b/src/components/forms/ShareListForm.tsx
index c4c2ae0..32c1317 100644
--- a/src/components/forms/ShareListForm.tsx
+++ b/src/components/forms/ShareListForm.tsx
@@ -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("");
@@ -24,7 +21,6 @@ const ShareListForm = ({ listPath }: Props) => {
e: FormEvent,
listPath: string | null,
) => {
- console.log("Button clicked! Inviting user!");
e.preventDefault();
if (!listPath) {
@@ -32,7 +28,7 @@ const ShareListForm = ({ listPath }: Props) => {
}
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("");
diff --git a/src/components/index.ts b/src/components/index.ts
index 92ad738..50121f3 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,3 +1,6 @@
export * from "./ListItem";
export * from "./SingleList";
export * from "./CreateList";
+export * from "./ProtectedRoute";
+export * from "./AuthenticatedNavBar";
+export * from "./forms/ShareListForm";
diff --git a/src/index.tsx b/src/index.tsx
index b3423b1..bdf1e7d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,5 +1,6 @@
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";
@@ -7,6 +8,8 @@ import "./index.css";
const root = createRoot(document.getElementById("root") as HTMLElement);
root.render(
-
+
+
+
,
);
diff --git a/src/views/Layout.css b/src/views/Layout.css
index e29a397..de8cbbd 100644
--- a/src/views/Layout.css
+++ b/src/views/Layout.css
@@ -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;
-}
diff --git a/src/views/Layout.tsx b/src/views/Layout.tsx
index e1c5ab6..0a65806 100644
--- a/src/views/Layout.tsx
+++ b/src/views/Layout.tsx
@@ -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 (
<>
@@ -25,25 +18,9 @@ export function Layout() {
+ {!user && }
-
+ {user &&
}
>
);
diff --git a/src/views/ManageList.tsx b/src/views/ManageList.tsx
index 3e61b53..1d80b48 100644
--- a/src/views/ManageList.tsx
+++ b/src/views/ManageList.tsx
@@ -84,72 +84,77 @@ export function ManageList({ listPath }: Props) {
Hello from the /manage-list
page!
{listPath && (
-
+
+
+
+ >
)}
diff --git a/src/views/PageNotFound.tsx b/src/views/PageNotFound.tsx
new file mode 100644
index 0000000..a2d2be8
--- /dev/null
+++ b/src/views/PageNotFound.tsx
@@ -0,0 +1,21 @@
+import React, { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+
+export function PageNotFound() {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ navigate("/", { replace: true });
+ }, 3000);
+
+ return () => clearTimeout(timer);
+ }, [navigate]);
+
+ return (
+
+
404 - Page Not Found
+
Redirecting to home...
+
+ );
+}
diff --git a/src/views/index.ts b/src/views/index.ts
index cb89d32..d329f27 100644
--- a/src/views/index.ts
+++ b/src/views/index.ts
@@ -2,3 +2,4 @@ export * from "./ManageList";
export * from "./Home";
export * from "./Layout";
export * from "./List";
+export * from "./PageNotFound";