CMU Coding Bootcamp
1// Import necessary hooks from React for creating context, using context, managing state with reducer, and side effects
2import { createContext, useContext, useReducer, useEffect } from "react";
3// Import PropTypes for validating prop types
4import PropTypes from "prop-types";
5
6// Create a context for managing blog data
7const BlogContext = createContext();
8
9// Define the initial state for the blog context
10const initialState = {
11 posts: [], // Initially empty array for posts
12 categories: [], // Initially empty array for categories
13 tags: [], // Initially empty array for tags
14 isLoading: false, // Initially not loading
15 error: null, // Initially no error
16};
17
18// Define the reducer function to handle state updates
19function blogReducer(state, action) {
20 // Use a switch statement to handle different action types
21 switch (action.type) {
22 case "SET_LOADING":
23 // Update loading state
24 return { ...state, isLoading: action.payload };
25 case "SET_ERROR":
26 // Update error state and set loading to false
27 return { ...state, error: action.payload, isLoading: false };
28 case "SET_POSTS":
29 // Update posts state and set loading to false
30 return { ...state, posts: action.payload, isLoading: false };
31 case "SET_CATEGORIES":
32 // Update categories state
33 return { ...state, categories: action.payload };
34 case "SET_TAGS":
35 // Update tags state
36 return { ...state, tags: action.payload };
37 default:
38 // Return current state if action type is not recognized
39 return state;
40 }
41}
42
43// Define the BlogProvider component to provide the blog context
44export function BlogProvider({ children }) {
45 // Use the useReducer hook to manage state and dispatch actions
46 const [state, dispatch] = useReducer(blogReducer, initialState);
47
48 // Use the useEffect hook to fetch data when the component mounts
49 useEffect(() => {
50 // Define an asynchronous function to fetch all posts
51 const fetchAllPosts = async () => {
52 // Dispatch action to set loading to true
53 dispatch({ type: "SET_LOADING", payload: true });
54
55 try {
56 // Get authentication token from local storage
57 const { token } = JSON.parse(localStorage.getItem("auth_user") || "{}");
58 // Throw error if no token found
59 if (!token) throw new Error("Authentication token not found");
60
61 // Get API URL from environment variables
62 const apiUrl = import.meta.env.VITE_API_URL;
63 console.log(apiUrl);
64 // Throw error if API URL is not defined
65 if (!apiUrl) throw new Error("API URL is not defined in .env");
66
67 // Initialize variables for pagination
68 let allPosts = [];
69 let currentPage = 1;
70 let totalPages = 1;
71
72 // Fetch posts in batches using pagination
73 do {
74 const response = await fetch(
75 `${apiUrl}/api/posts?page=${currentPage}&results_per_page=5`,
76 {
77 headers: {
78 Authorization: `Bearer ${token}`, // Include token in authorization header
79 },
80 }
81 );
82
83 // Throw error if response is not ok
84 if (!response.ok) throw new Error("Failed to fetch posts");
85
86 const data = await response.json(); // Parse response data as JSON
87 totalPages = data.totalPages; // Update totalPages from response
88
89 // Transform fetched posts into desired format
90 const transformedPosts = data.posts.map((post) => ({
91 id: post._id,
92 title: post.title,
93 content: post.content,
94 tags: post.tags.map((tag) => tag.name), // Extract tag names
95 categories: post.categories.map((category) => category.name), // Extract category names
96 author: post.author, //author id
97 date: new Date(post.createdAt).toLocaleDateString(), // Format date
98 likes: post.likes.length, //count likes
99 comments: post.comments.length, //count comments
100 }));
101
102 // Add transformed posts to allPosts array
103 allPosts = [...allPosts, ...transformedPosts];
104 // Increment currentPage for next iteration
105 currentPage++;
106 // Continue loop until currentPage exceeds totalPages
107 } while (currentPage <= totalPages);
108
109 // Dispatch action to set posts in state
110 dispatch({ type: "SET_POSTS", payload: allPosts });
111
112 // Extract unique categories and tags
113 const categories = [
114 ...new Set(allPosts.flatMap((post) => post.categories)),
115 ];
116 const tags = [...new Set(allPosts.flatMap((post) => post.tags))];
117
118 // Dispatch actions to set categories and tags in state
119 dispatch({ type: "SET_CATEGORIES", payload: categories });
120 dispatch({ type: "SET_TAGS", payload: tags });
121 } catch (error) {
122 // Dispatch action to set error in state if any error occurs
123 dispatch({ type: "SET_ERROR", payload: error.message });
124 } finally {
125 // Dispatch action to set loading to false in finally block to ensure loading is always set to false after the try...catch block
126 dispatch({ type: "SET_LOADING", payload: false });
127 }
128 };
129
130 // Call fetchAllPosts function
131 fetchAllPosts();
132 }, []); // Empty dependency array ensures this effect runs only once on mount
133
134 return (
135 //renders the children components wrapped with this provider and passes the state and dispatch function from useReducer
136 <BlogContext.Provider value={{ state, dispatch }}>
137 {children}
138 </BlogContext.Provider>
139 );
140}
141
142// Define propTypes for BlogProvider to ensure children prop is required
143BlogProvider.propTypes = {
144 children: PropTypes.node.isRequired,
145};
146
147// Define a custom hook to access the blog context
148export const useBlog = () => {
149 // Use useContext hook to access the BlogContext
150 const context = useContext(BlogContext);
151 // Throw an error if context is not found (meaning the hook is used outside of a BlogProvider)
152 if (!context) {
153 throw new Error("useBlog must be used within a BlogProvider");
154 }
155 // Return the context value (state and dispatch)
156 return context;
157};