Skip to main content

Overview

The Courses page provides comprehensive search and filtering capabilities, allowing students to find courses by title, category, and price. The page also includes pagination for easy navigation through large course catalogs. Location: src/views/Courses/Courses.jsx

Course Data State

The component manages two data arrays for flexible filtering:
const [dataCourses, setDataCourses] = useState({
  data: [],
  filteredData: [],
});
Location: src/views/Courses/Courses.jsx:12-15
data
array
Complete list of all available courses loaded from localStorage
filteredData
array
Subset of courses matching current filter criteria. When empty, displays full course list.

Search by Title

Real-time search functionality filters courses as users type:
const [dataTitle, setDataTitle] = useState("");

const dataFilteredByTitle = async (event) => {
  const newValue = event.target.value;
  setDataTitle(newValue);

  const url = `/courses?title=${newValue}`;

  if (newValue) {
    const { data } = await axios.get(url);
    setDataCourses((prevState) => ({
      ...prevState,
      filteredData: data,
    }));
  }
};
Location: src/views/Courses/Courses.jsx:18,104-117

Search Input UI

<div className={Styles.SearchBarContainer}>
  <input
    type="text"
    value={dataTitle}
    name="title"
    onChange={dataFilteredByTitle}
  />
  <svg>
    {/* Search icon */}
  </svg>
</div>
Location: src/views/Courses/Courses.jsx:191-208

Auto-reset on Clear

When search is cleared, filters automatically reset:
useEffect(() => {
  if (dataTitle === "") {
    handleReset();
  }
}, [dataTitle]);
Location: src/views/Courses/Courses.jsx:180-184

Category Filter

Users can filter courses by category using a dropdown:
const [categoriesData, setCategoriesData] = useState([]);

const dataFiltered = async (e) => {
  const categoryFilter = e.target.value;

  const url = `/courses?category=${categoryFilter}`;
  if (categoryFilter !== "categories") {
    if (categoryFilter) {
      const { data } = await axios.get(url);
      setDataCourses((prevState) => ({
        ...prevState,
        filteredData: data,
      }));
    }
  } else {
    handleReset();
    return;
  }
};
Location: src/views/Courses/Courses.jsx:17,86-102

Category Dropdown

<select
  className={Styles.filterButt}
  onChange={dataFiltered}
  name="category"
  id="category"
>
  <option name="categories" value="categories">
    Categorías
  </option>
  {categoriesData?.map((category, index) => (
    <option key={index} value={category.name}>
      {category.name}
    </option>
  ))}
</select>
Location: src/views/Courses/Courses.jsx:210-223

Loading Categories

Categories are loaded from localStorage on component mount:
useEffect(() => {
  const storedData = localStorage.getItem("coursesData");
  const parsedData = storedData ? JSON.parse(storedData) : [];
  setDataCourses((prevState) => ({
    ...prevState,
    data: parsedData.filter((element) => element.enabled !== false),
  }));

  setCategoriesData(JSON.parse(localStorage.getItem("categoriesData")));
  const session = JSON.parse(localStorage.getItem("userOnSession"));
  if (session?.email !== "") {
    updateContextUser(session);
  }
}, []);
Location: src/views/Courses/Courses.jsx:71-84

Price Sorting

Courses can be sorted by price in ascending or descending order:
const sortByPrice = (e) => {
  const sortedData = [...dataCourses.data];
  const sortedFilteredData = dataCourses.filteredData.length
    ? [...dataCourses.filteredData]
    : [];
  const selectedOpt = e.target.value;
  
  if (selectedOpt === "DESC") {
    sortedData.sort((a, b) => b.price - a.price);
    sortedFilteredData.sort((a, b) => b.price - a.price);
  } else if (selectedOpt === "ASC") {
    sortedData.sort((a, b) => a.price - b.price);
    sortedFilteredData.sort((a, b) => a.price - b.price);
  }

  setDataCourses((prevState) => ({
    ...prevState,
    data: sortedData,
    filteredData: sortedFilteredData,
  }));
};
Location: src/views/Courses/Courses.jsx:119-138

Sort Options

ASC
option
Menor a mayor (Low to High) - Sorts courses from cheapest to most expensive
DESC
option
Mayor a menor (High to Low) - Sorts courses from most expensive to cheapest
Default
option
Default sorting (no price sort applied)

Price Sort Dropdown

<select
  className={Styles.filterButt}
  onChange={sortByPrice}
  name="order"
  id="order"
>
  <option name="Default" value="Default">
    Ordenar por precio
  </option>
  <option value="ASC">Menor a mayor</option>
  <option value="DESC">Mayor a menor</option>
</select>
Location: src/views/Courses/Courses.jsx:238-248

Reset Filters

Users can clear all filters and return to the full course list:
const handleReset = () => {
  const selectElement = document.getElementById("category");
  const optionToSelect = selectElement.querySelector(
    'option[name="categories"]'
  );

  if (optionToSelect) {
    optionToSelect.selected = true;
  }
  
  const selectElement2 = document.getElementById("order");
  const optionToSelect2 = selectElement2.querySelector(
    'option[name="Default"]'
  );
  if (optionToSelect2) {
    optionToSelect2.selected = true;
  }

  setDataCourses(() => ({
    data: JSON.parse(localStorage.getItem("coursesData")),
    filteredData: [],
  }));
};
Location: src/views/Courses/Courses.jsx:148-178

Reset Button

<div className={Styles.reset}>
  <Button
    className="reset-button"
    onClick={handleReset}
    text={"Restablecer"}
  />
</div>
Location: src/views/Courses/Courses.jsx:250-256

Pagination

The course list implements pagination for better performance and UX:
const ITEMS_PER_PAGE = 10;
const MAX_PAGES_DISPLAYED = 5;

const [currentPage, setCurrentPage] = useState(1);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;

const displayCourses =
  dataCourses.filteredData.length > 0
    ? dataCourses?.filteredData
    : dataCourses?.data;

const totalPages =
  displayCourses && displayCourses.length > 0
    ? Math.ceil(displayCourses.length / ITEMS_PER_PAGE)
    : 1;

const currentItems = displayCourses
  ? displayCourses.slice(startIndex, endIndex)
  : [];
Location: src/views/Courses/Courses.jsx:8-35

Pagination Constants

ITEMS_PER_PAGE
number
default:"10"
Number of courses displayed per page
MAX_PAGES_DISPLAYED
number
default:"5"
Maximum number of page buttons shown in pagination controls
const handleNext = () => {
  const totalElements = displayCourses.length;
  const nextPage = currentPage + 1;
  const totalPages = Math.ceil(totalElements / ITEMS_PER_PAGE);

  if (nextPage <= totalPages) {
    setCurrentPage(nextPage);
    window.scrollTo({ top: 0 });
  }
};

const handlePrev = () => {
  if (currentPage > 1) {
    setCurrentPage(currentPage - 1);
    window.scrollTo({ top: 0 });
  }
};
Location: src/views/Courses/Courses.jsx:54-70

Pagination UI

<div className={Styles.paginationContainer}>
  <button
    className={Styles.paginationBotton}
    onClick={handlePrev}
    disabled={currentPage === 1}
  >
    Prev
  </button>
  {Array.from({ length: endPage - startPage + 1 }, (_, index) => (
    <button
      key={startPage + index}
      onClick={() => setCurrentPage(startPage + index)}
      className={
        currentPage === startPage + index
          ? Styles.currentPage
          : Styles.buttonPage
      }
    >
      {startPage + index}
    </button>
  ))}
  <button
    className={Styles.paginationBotton}
    onClick={handleNext}
    disabled={currentPage === totalPages}
  >
    Next
  </button>
</div>
Location: src/views/Courses/Courses.jsx:274-299

Page Range Calculation

The pagination displays a sliding window of page numbers:
let startPage = currentPage - Math.floor(MAX_PAGES_DISPLAYED / 2);
let endPage = currentPage + Math.floor(MAX_PAGES_DISPLAYED / 2);

if (startPage < 1) {
  startPage = 1;
  endPage = Math.min(MAX_PAGES_DISPLAYED, totalPages);
}

if (endPage > totalPages) {
  endPage = totalPages;
  startPage = Math.max(1, totalPages - MAX_PAGES_DISPLAYED + 1);
}
Location: src/views/Courses/Courses.jsx:41-52

Course Display

Courses are filtered to show only enabled courses:
const filteredCourses = currentItems.filter(
  (element) => element.enabled === true
);
Location: src/views/Courses/Courses.jsx:37-39

Rendering Courses

<div className={Styles.courses}>
  {dataCourses.data && dataCourses.data.length !== 0 ? (
    dataCourses.filteredData === false ? (
      <div>No hay coincidencias</div>
    ) : (
      filteredCourses.map((course, index) => (
        <Card key={index} course={course} />
      ))
    )
  ) : (
    <div>No hay cursos disponibles</div>
  )}
</div>
Location: src/views/Courses/Courses.jsx:260-273

Filter Workflow

1

Initial Load

Courses and categories load from localStorage, displaying all enabled courses
2

Apply Filter

User selects category or enters search term, triggering API request with filter parameters
3

Update Filtered Data

API response populates filteredData array, which takes precedence over full data array
4

Apply Sorting

User can sort filtered results by price (ASC/DESC) without re-fetching data
5

Pagination Update

Pagination recalculates based on filtered results length
6

Reset Option

User clicks “Restablecer” to clear filters and return to full course list

API Endpoints

The filters use the following API patterns:

Filter by Title

GET /courses?title={searchTerm}
Returns courses matching the search term in their title.

Filter by Category

GET /courses?category={categoryName}
Returns courses belonging to the specified category.

Data Persistence

Course and category data are cached in localStorage:
coursesData
array
Full list of all courses, loaded on app initialization
categoriesData
array
List of all available categories for the filter dropdown
userOnSession
object
Current user session data, used to check course ownership

Filter Combination

Filters work together:
  1. Search or Category: Only one can be active at a time (selecting a category clears search, and vice versa)
  2. Price Sort: Applied on top of filtered results
  3. Enabled Filter: Always active - only shows courses with enabled: true
  4. Pagination: Works with both filtered and unfiltered data

Empty States

No courses available
message
Shown when coursesData is empty or not loaded
No hay coincidencias
message
Shown when filters return zero results

Build docs developers (and LLMs) love