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 && ( -
handleSubmit(e, listPath)}> - -
-
- When to buy: -
- -
+
+ When to buy: + +
+ +
+ +
+ + + )} 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";