Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -26,11 +27,9 @@ function App() {
<Route
path="/dashboard"
element={
user ? (
<ProtectedRoute>
<Dashboard setShowModal={setShowModal} />
) : (
<Navigate to="/login" />
)
</ProtectedRoute>
}
/>
<Route
Expand Down
21 changes: 21 additions & 0 deletions src/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// ProtectedRoute.tsx
import { Navigate } from "react-router-dom";
import { useAuthContext } from "./hooks/useAuthContext.tsx";
import Loader from './components/Loader';
import { JSX } from "react";

const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
const { user, authIsLoading } = useAuthContext();

if (authIsLoading) {
return <Loader />; // block until auth is known
}

if (!user) {
return <Navigate to="/login" />;
}

return children;
};

export default ProtectedRoute;
22 changes: 5 additions & 17 deletions src/components/BlogpostDetails.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -82,19 +76,13 @@ const BlogDetails = ({ blogpost }: BlogPostProps ) => {
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
{isPending && (
<div>
<Loader />
</div>
)}
{error && <div>{error}</div>}
{blog && (
{blogpost && (
<article>
<Typography variant="h3" align="left">
{title}
</Typography>
<Typography variant="subtitle1">
<strong>Written by {author.email}</strong>
<strong>Written by {author?.email}</strong>
</Typography>
<Typography variant="subtitle2">
{formatDistanceToNow(new Date(createdAt), {
Expand Down
27 changes: 21 additions & 6 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,13 +22,18 @@ interface AuthAction {

export const AuthContext = createContext<{
user: User | null;
dispatch: React.Dispatch<AuthAction>
dispatch: React.Dispatch<AuthAction>;
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 };
Expand All @@ -38,6 +49,8 @@ export const AuthContextProvider = ({ children }: { children: ReactNode }) => {
user: null,
});

const [authIsLoading, setAuthIsLoading] = useState(true);

useEffect(() => {
const storedUser = localStorage.getItem("user");
try {
Expand All @@ -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 (
<AuthContext.Provider value={{ ...state, dispatch }}>
<AuthContext.Provider value={{ ...state, dispatch, authIsLoading }}>
{children}
</AuthContext.Provider>
);
Expand Down
33 changes: 18 additions & 15 deletions src/context/BlogpostContext.tsx
Original file line number Diff line number Diff line change
@@ -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;
}

Expand All @@ -19,34 +15,41 @@ export const BlogpostsContext = createContext<{
dispatch: React.Dispatch<BlogpostsAction>;
}>({
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,
});
Expand Down
86 changes: 31 additions & 55 deletions src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,50 @@
import useFetch from "../useFetch.ts";
import { useEffect, useMemo } from "react";
import { useBlogpostsContext } from "../hooks/useBlogpostsContext.tsx";
import { useAuthContext } from "../hooks/useAuthContext.tsx";

// components
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<any>;
};

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 (
<Container className="home">
<Container className="blogposts">
{error && <div>{error}</div>}
{isPending && <Shimmer />}
{blogposts &&
blogposts?.map((blogpost: Blogpost) => {
const { title, author, createdAt, _id } = blogpost;
return (
<BlogDetails
key={_id}
title={title}
author={author}
createdAt={createdAt}
blogpost={blogpost}
setShowModal={setShowModal}
/>
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 (
<Container className="home">
<Container className="usersBlogposts">
{usersBlogposts &&
usersBlogposts?.map((blogpost: Blogpost) => {
const { title, author, createdAt, _id } = blogpost;
return (
<BlogDetails
key={_id}
title={title}
author={author}
createdAt={createdAt}
blogpost={blogpost}
setShowModal={setShowModal}
/>
);
})}
})}
</Container>
<BlogpostForm />
</Container>
<BlogpostForm />
</Container>
);
);
} else {
return <>NO USER!</>;
}
};

export default Dashboard;
11 changes: 4 additions & 7 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,17 @@ const Home = ({ setShowModal }: HomeProps) => {
dispatch: React.Dispatch<any>;
};

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]);

Expand Down