diff --git a/src/App.tsx b/src/App.tsx index e665e8e..8bf422c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import { useState } from "react"; import Footer from "./components/Footer.tsx"; import { useAuthContext } from "./hooks/useAuthContext.tsx"; import Home from "./pages/Home.tsx"; +import ProtectedRoute from "./ProtectedRoute.tsx"; function App() { const { user } = useAuthContext(); @@ -26,11 +27,9 @@ function App() { - ) : ( - - ) + } /> { + const { user, authIsLoading } = useAuthContext(); + + if (authIsLoading) { + return ; // block until auth is known + } + + if (!user) { + return ; + } + + return children; +}; + +export default ProtectedRoute; diff --git a/src/components/BlogpostDetails.tsx b/src/components/BlogpostDetails.tsx index 6105914..88d9d13 100644 --- a/src/components/BlogpostDetails.tsx +++ b/src/components/BlogpostDetails.tsx @@ -1,6 +1,4 @@ import { useBlogpostsContext } from "../hooks/useBlogpostsContext"; -import { useMemo } from "react"; -import useFetch from "../useFetch.ts"; import { Button, Typography } from "@mui/material"; import RemoveIcon from "@mui/icons-material/Remove"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; @@ -9,7 +7,6 @@ import { useAuthContext } from "../hooks/useAuthContext"; // date fns import { formatDistanceToNow } from "date-fns"; -import Loader from "./Loader"; export interface Blogpost { _id: string; @@ -27,16 +24,13 @@ interface BlogPostProps { setShowModal: (value: boolean) => void; } -const BlogDetails = ({ blogpost }: BlogPostProps ) => { +const BlogDetails = ({ blogpost }: BlogPostProps) => { const { title, author, createdAt } = blogpost; const { user } = useAuthContext(); const { dispatch } = useBlogpostsContext(); - const headers = useMemo(() => { - return user?.token ? { Authorization: `Bearer ${user.token}` } : {}; - }, [user?.token]); - const fetchUrl = "https://gentle-plateau-25780.herokuapp.com/api/blogpost/"; - const { data: blog, error, isPending } = useFetch(fetchUrl, headers); + // to come back to + // we already have blogpost as a prop to this component so we don't need to fetch it again to display it const detailsBlogPost = async () => { if (!user) { return; @@ -82,19 +76,13 @@ const BlogDetails = ({ blogpost }: BlogPostProps ) => { animate={{ opacity: 1 }} transition={{ delay: 0.2 }} > - {isPending && ( -
- -
- )} - {error &&
{error}
} - {blog && ( + {blogpost && (
{title} - Written by {author.email} + Written by {author?.email} {formatDistanceToNow(new Date(createdAt), { diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 3680f29..566b6dc 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,4 +1,10 @@ -import React, { createContext, useReducer, useEffect, type ReactNode } from "react"; +import React, { + createContext, + useReducer, + useEffect, + useState, + type ReactNode, +} from "react"; interface User { email: string; @@ -16,13 +22,18 @@ interface AuthAction { export const AuthContext = createContext<{ user: User | null; - dispatch: React.Dispatch + dispatch: React.Dispatch; + authIsLoading: boolean; }>({ - user : null, + user: null, dispatch: () => {}, + authIsLoading: false, }); -export const authReducer = (state: AuthState, action: AuthAction): AuthState => { +export const authReducer = ( + state: AuthState, + action: AuthAction +): AuthState => { switch (action.type) { case "LOGIN": return { user: action.payload }; @@ -38,6 +49,8 @@ export const AuthContextProvider = ({ children }: { children: ReactNode }) => { user: null, }); + const [authIsLoading, setAuthIsLoading] = useState(true); + useEffect(() => { const storedUser = localStorage.getItem("user"); try { @@ -48,12 +61,14 @@ export const AuthContextProvider = ({ children }: { children: ReactNode }) => { dispatch({ type: "LOGIN", payload: user }); } } catch (err) { - console.log("Failed to parse user from localStorage") + console.log("Failed to parse user from localStorage"); + } finally { + setAuthIsLoading(false); } }, []); return ( - + {children} ); diff --git a/src/context/BlogpostContext.tsx b/src/context/BlogpostContext.tsx index 74924d5..70721f6 100644 --- a/src/context/BlogpostContext.tsx +++ b/src/context/BlogpostContext.tsx @@ -1,16 +1,12 @@ import { createContext, useReducer, type ReactNode } from "react"; - -interface Blogpost { - _id: string; - [key: string]: any; -} +import { Blogpost } from "../components/BlogpostDetails"; interface BlogpostsState { blogposts: Blogpost[] | null; } interface BlogpostsAction { - type: "SET_BLOGPOSTS" | "GET_BLOGPOSTS" | "CREATE_BLOGPOST" | "DELETE_BLOGPOST"; + type: "SET_BLOGPOSTS" | "CREATE_BLOGPOST" | "DELETE_BLOGPOST"; payload: any; } @@ -19,34 +15,41 @@ export const BlogpostsContext = createContext<{ dispatch: React.Dispatch; }>({ blogposts: null, - dispatch: () => {} + dispatch: () => {}, }); -export const blogpostsReducer = (state: BlogpostsState, action: BlogpostsAction): BlogpostsState => { +export const blogpostsReducer = ( + state: BlogpostsState, + action: BlogpostsAction +): BlogpostsState => { switch (action.type) { case "SET_BLOGPOSTS": - return { - blogposts: action.payload, - }; - case "GET_BLOGPOSTS": return { ...state, blogposts: action.payload, }; case "CREATE_BLOGPOST": return { + ...state, blogposts: [action.payload, ...(state.blogposts || [])], }; case "DELETE_BLOGPOST": - return { - blogposts: state.blogposts ? state.blogposts.filter((b) => b._id !== action.payload._id) : [], + return { + ...state, + blogposts: state.blogposts + ? state.blogposts.filter((b) => b._id !== action.payload._id) + : [], }; default: return state; } }; -export const BlogpostsContextProvider = ({ children } : { children: ReactNode}) => { +export const BlogpostsContextProvider = ({ + children, +}: { + children: ReactNode; +}) => { const [state, dispatch] = useReducer(blogpostsReducer, { blogposts: null, }); diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 2a83b10..11fe0be 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,5 +1,3 @@ -import useFetch from "../useFetch.ts"; -import { useEffect, useMemo } from "react"; import { useBlogpostsContext } from "../hooks/useBlogpostsContext.tsx"; import { useAuthContext } from "../hooks/useAuthContext.tsx"; @@ -7,68 +5,46 @@ import { useAuthContext } from "../hooks/useAuthContext.tsx"; import BlogDetails, { Blogpost } from "../components/BlogpostDetails.tsx"; import BlogpostForm from "../components/BlogpostForm.tsx"; import { Container } from "@mui/material"; -import Shimmer from "../components/Shimmer.tsx"; interface DashboardProps { setShowModal: (value: boolean) => void; } const Dashboard = ({ setShowModal }: DashboardProps) => { - const { blogposts, dispatch } = useBlogpostsContext() as { - blogposts: Blogpost[] | null; - dispatch: React.Dispatch; - }; - + const { blogposts } = useBlogpostsContext(); const { user } = useAuthContext(); - - const authHeaders = useMemo( - () => user ? { Authorization: `Bearer ${user.token}` } : undefined, - [user?.token] - ); - - const { data, isPending, error } = useFetch( - user ? "https://gentle-plateau-25780.herokuapp.com/api/blogpost" : "", - authHeaders - ); - - const { } = useFetch( - user ? "https://gentle-plateau-25780.herokuapp.com/api/blogpost" : "", - ); - - // const handleDelete = (id) => { - // const newBlogs = blogs.filter((blog) => blog.id !== id); - // setBlogs(newBlogs); - // }; - - useEffect(() => { - if (data) { - dispatch({ type: 'SET_BLOGPOSTS', payload: data }); - } - }, [data, dispatch]); - - return ( - - - {error &&
{error}
} - {isPending && } - {blogposts && - blogposts?.map((blogpost: Blogpost) => { - const { title, author, createdAt, _id } = blogpost; - return ( - + if (user) { + // @todo the types say the user can be null. + // IF user is null at this point we need to handle that + // with an error boundary ideally and let the app handle it + // as we can't show blog posts if we have no user! + const usersBlogposts = blogposts?.filter( + (post) => post.author.email === user?.email + ); + return ( + + + {usersBlogposts && + usersBlogposts?.map((blogpost: Blogpost) => { + const { title, author, createdAt, _id } = blogpost; + return ( + ); - })} + })} + + - -
- ); + ); + } else { + return <>NO USER!; + } }; export default Dashboard; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 7bd77d8..3bd5bc2 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -18,20 +18,17 @@ const Home = ({ setShowModal }: HomeProps) => { dispatch: React.Dispatch; }; - const headers = useMemo( - () => { + const headers = useMemo(() => { const token = localStorage.getItem("token") || ""; return { Authorization: `Bearer ${token}` }; - }, - [] - ); - + }, []); + const fetchUrl = "https://gentle-plateau-25780.herokuapp.com/api/blogpost"; const { isPending, error, data } = useFetch(fetchUrl, headers); useEffect(() => { if (data) { - dispatch({ type: "GET_BLOGPOSTS", payload: data }); + dispatch({ type: "SET_BLOGPOSTS", payload: data }); } }, [data, dispatch]);