hngprojects / hng_boilerplate_remix

132 stars 127 forks source link

Feat/hng 93 implement pagination for career page #356

Closed Jomagene closed 1 month ago

Jomagene commented 1 month ago

implement pagination for career page:

(This PR introduces a pagination feature to the HNG-boiler-plate-Remix application due to the issue [FEAT] External Dynamic Pages > Career Page > Implement Pagination https://github.com/hngprojects/hng_boilerplate_remix/issues/93. CLOSED BY MISTAKE MERGING AN EMPTY PR)

Add Pagination Component and Job Listing Feature for Career Page

PR Description:

This PR introduces a pagination component and integrates it with the job listing feature on the career page. The new pagination system allows users to navigate through multiple pages of job listings seamlessly. Below are the key features and implementation details:

Features:

  1. Pagination Component:

    • Pagination: Container for the pagination navigation.
    • PaginationContent: Holds the pagination items.
    • PaginationItem: Represents an individual page item.
    • PaginationLink: Link for each page, styled according to its state (active/inactive).
    • PaginationPrevious and PaginationNext: Navigation links to move to the previous or next page.
    • PaginationEllipsis: Ellipsis for indicating skipped pages.
  2. Job Listing Integration:

    • CareerPage: A page that displays job listings with pagination controls.
    • Dynamically generates page numbers based on the current page and total pages.
    • Adjusts the number of visible page links based on the window width.

How to Use:

To use the pagination component and fetch a job list, follow these steps:

  1. Loader Function:

    • The loader function fetches job listings based on the current page from the URL query parameters.
    • If the page number is invalid, it returns a 404 response.
    export const loader: LoaderFunction = async ({ request }) => {
     const url = new URL(request.url);
     const page = parseInt(url.searchParams.get("page") || "1", 10);
     const jobListings = dummyJobListings(page);
    
     if (isNaN(page) || page < 1 || page > jobListings.totalPages) {
       throw new Response("Page not found", { status: 404 });
     }
    
     return json({ jobListings });
    };
  2. CareerPage Component:

    • The CareerPage component uses the useLoaderData hook to fetch job listings data.
    • It dynamically generates the pagination links based on the current page and total pages.
    • Adjusts the number of visible pages based on the window width.
    const CareerPage: React.FC = () => {
     const { jobListings } = useLoaderData<LoaderData>();
     const [searchParams] = useSearchParams();
     const currentPage = parseInt(searchParams.get("page") || "1", 10);
     const totalPages = jobListings.totalPages;
    
     const [maxPagesToShow, setMaxPagesToShow] = useState(3);
    
     useEffect(() => {
       const handleResize = () => {
         const width = window.innerWidth;
         if (width <= 400) {
           setMaxPagesToShow(0);
         } else if (width <= 435) {
           setMaxPagesToShow(1);
         } else if (width <= 450) {
           setMaxPagesToShow(2);
         } else if (width <= 470) {
           setMaxPagesToShow(3);
         } else if (width <= 1024) {
           setMaxPagesToShow(5);
         } else {
           setMaxPagesToShow(totalPages);
         }
       };
    
       window.addEventListener("resize", handleResize);
       handleResize();
    
       return () => window.removeEventListener("resize", handleResize);
     }, [totalPages]);
    
     const generatePageNumbers = (): (number | string)[] => {
       const pageNumbers: (number | string)[] = [];
       const half = Math.floor(maxPagesToShow / 2);
    
       let start = Math.max(1, currentPage - half);
       let end = Math.min(totalPages, currentPage + half);
    
       if (currentPage - half <= 0) {
         end = Math.min(totalPages, maxPagesToShow);
       }
    
       if (currentPage + half > totalPages) {
         start = Math.max(1, totalPages - maxPagesToShow + 1);
       }
    
       for (let i = start; i <= end; i++) {
         pageNumbers.push(i);
       }
    
       if (start > 1) {
         if (start > 2) {
           pageNumbers.unshift(1, "ellipsis");
         } else {
           pageNumbers.unshift(1);
         }
       }
    
       if (end < totalPages - 1) {
         pageNumbers.push("ellipsis", totalPages);
       } else if (end === totalPages - 1) {
         pageNumbers.push(totalPages);
       }
    
       return pageNumbers;
     };
    
     return (
       <div className="container mx-auto px-4">
         <h1>Career Page</h1>
         <div>
           {jobListings.items.map((job) => (
             <div key={job.id}>{job.title}</div>
           ))}
         </div>
         {jobListings.totalPages > 1 && (
           <Pagination className="my-4 flex justify-center font-[inter]">
             <PaginationContent className="flex items-center justify-between p-0">
               <PaginationItem className="mx-1">
                 <Link
                   to={`?page=${Math.max(currentPage - 1, 1)}`}
                   className={`flex items-center gap-2 space-x-1 rounded-md py-2 pl-2.5 pr-4 ${
                     currentPage === 1
                       ? "pointer-events-none text-gray-400 opacity-50"
                       : "hover:bg-[#F4F4F5]"
                   }`}
                 >
                   <PaginationPrevious size={250} />
                 </Link>
               </PaginationItem>
               {generatePageNumbers().map((page, index) =>
                 page === "ellipsis" ? (
                   <PaginationItem key={index}>
                     <PaginationEllipsis className="flex items-center" />
                   </PaginationItem>
                 ) : (
                   <PaginationItem key={index} className="mx-1">
                     <Link
                       to={`?page=${page}`}
                       className={`rounded-md px-3 py-2 text-[14px] ${
                         currentPage === page
                           ? "bg-orange-500 text-white"
                           : `hover:bg-[#F4F4F5] ${
                               maxPagesToShow === 0 ? "hidden" : ""
                             }`
                       } `}
                     >
                       <PaginationLink size={20}>{page}</PaginationLink>
                     </Link>
                   </PaginationItem>
                 ),
               )}
               <PaginationItem className="mx-1">
                 <Link
                   to={`?page=${Math.min(currentPage + 1, totalPages)}`}
                   className={`flex items-center gap-2 space-x-1 rounded-md py-2 pl-2.5 pr-4 ${
                     currentPage === totalPages
                       ? "pointer-events-none text-[#CBD5E1]"
                       : "hover:bg-[#F4F4F5]"
                   }`}
                   aria-disabled={currentPage === totalPages}
                 >
                   <PaginationNext size={250} />
                 </Link>
               </PaginationItem>
             </PaginationContent>
           </Pagination>
         )}
       </div>
     );
    };
    
    export default CareerPage;

Additional Notes: