CMU Coding Bootcamp
at main 157 lines 6.2 kB view raw
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};