Skip to main content

Overview

The Categories component is a sophisticated, responsive navigation element that fetches and displays menu categories. It provides two distinct experiences:
  • Desktop: Horizontal icon-based menu with category names
  • Mobile: Hamburger menu that opens a slide-in drawer

Location

src/components/Categories/Categories.jsx
src/components/Categories/Categories.css

Props

onCategoryChange
function
required
Callback function invoked when a category is selected. Receives the category ID as a parameter.
(categoryId: number) => void

State Management

The component manages multiple pieces of internal state:
const [active, setActive] = useState(null);        // Currently selected category ID
const [categories, setCategories] = useState([]);  // Array of category objects from API
const [loading, setLoading] = useState(true);      // Loading state for API fetch
const [error, setError] = useState(null);          // Error message if fetch fails
const [drawerOpen, setDrawerOpen] = useState(false); // Mobile drawer open/close state

Data Fetching

Categories are fetched from the API on component mount:
import { getCategorias } from "../../services/api";

useEffect(() => {
  getCategorias()
    .then((data) => {
      setCategories(data);
    })
    .catch((err) => setError(err.message))
    .finally(() => setLoading(false));
}, []);
The getCategorias() function fetches from:
https://apiqsp-production.up.railway.app/categories

Expected API Response

The API returns an array of category objects:
[
  {
    "id": 1,
    "name": "Hamburguesas",
    "icon": "lunch_dining"
  },
  {
    "id": 2,
    "name": "Bebidas",
    "icon": "local_cafe"
  }
  // ...
]
Each category must have an id, name, and icon property. The icon value corresponds to Material Symbols icon names.

Icon Integration

The component uses Material Symbols Outlined icons:
<span className="material-symbols-outlined">{cat.icon}</span>
Make sure Material Symbols is included in your project:
<!-- In your index.html or via import -->
<link rel="stylesheet" 
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" />

Desktop UI

The desktop version renders as a horizontal menu:
<div className="categories-wrapper">
  <div className="categories-container">
    {categories.map((cat) => (
      <button
        key={cat.id}
        className={`category-btn ${active === cat.id ? "active" : ""}`}
        onClick={() => handleClick(cat.id)}
      >
        <div className="category-icon">
          <span className="material-symbols-outlined">{cat.icon}</span>
        </div>
        <span className="category-label">{cat.name}</span>
        <div className="category-underline" />
      </button>
    ))}
  </div>
</div>
Key Features:
  • Icon + label for each category
  • Active state styling with .active class
  • Underline animation on hover/active state

Mobile UI

The mobile experience includes three elements:

1. Hamburger Toggle Button

<button
  className="drawer-toggle"
  onClick={() => setDrawerOpen(true)}
  aria-label="Abrir menú de categorías"
>
  <span />
  <span />
  <span />
</button>

2. Overlay Backdrop

<div
  className={`drawer-overlay ${drawerOpen ? "open" : ""}`}
  onClick={() => setDrawerOpen(false)}
/>
Clicking the overlay closes the drawer.

3. Slide-in Drawer Panel

<nav className={`drawer-panel ${drawerOpen ? "open" : ""}`}>
  <span className="drawer-title">Categorías</span>
  {categories.map((cat) => (
    <button
      key={cat.id}
      className={`drawer-item ${active === cat.id ? "active" : ""}`}
      onClick={() => handleClick(cat.id)}
    >
      <div className="drawer-item-icon">
        <span className="material-symbols-outlined">{cat.icon}</span>
      </div>
      <span className="drawer-item-label">{cat.name}</span>
      <div className="drawer-item-dot" />
    </button>
  ))}
</nav>

Keyboard Accessibility

The drawer can be closed with the Escape key:
useEffect(() => {
  const handleKey = (e) => {
    if (e.key === "Escape") setDrawerOpen(false);
  };
  window.addEventListener("keydown", handleKey);
  return () => window.removeEventListener("keydown", handleKey);
}, []);

Event Handling

When a category is clicked:
const handleClick = (id) => {
  setActive(id);              // Update active state
  onCategoryChange?.(id);     // Notify parent component
  setDrawerOpen(false);       // Close mobile drawer
};
The optional chaining operator ?. ensures the callback is only invoked if provided.

Loading and Error States

if (loading) return <p className="categories-status">Cargando...</p>;
if (error)   return <p className="categories-status error">{error}</p>;
These states are displayed while fetching categories or if the fetch fails.

Complete Source Code

src/components/Categories/Categories.jsx
import "./Categories.css";
import { useState, useEffect } from "react";
import { getCategorias } from "../../services/api";

export default function Categories({ onCategoryChange }) {
  const [active, setActive] = useState(null);
  const [categories, setCategories] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [drawerOpen, setDrawerOpen] = useState(false);

  useEffect(() => {
    getCategorias()
      .then((data) => {
        setCategories(data);
      })
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  // Cierra el drawer con Escape
  useEffect(() => {
    const handleKey = (e) => {
      if (e.key === "Escape") setDrawerOpen(false);
    };
    window.addEventListener("keydown", handleKey);
    return () => window.removeEventListener("keydown", handleKey);
  }, []);

  const handleClick = (id) => {
    setActive(id);
    onCategoryChange?.(id);
    setDrawerOpen(false);
  };

  if (loading) return <p className="categories-status">Cargando...</p>;
  if (error)   return <p className="categories-status error">{error}</p>;

  return (
    <>
      {/* ── DESKTOP: menú horizontal ── */}
      <div className="categories-wrapper">
        <div className="categories-container">
          {categories.map((cat) => (
            <button
              key={cat.id}
              className={`category-btn ${active === cat.id ? "active" : ""}`}
              onClick={() => handleClick(cat.id)}
            >
              <div className="category-icon">
                <span className="material-symbols-outlined">{cat.icon}</span>
              </div>
              <span className="category-label">{cat.name}</span>
              <div className="category-underline" />
            </button>
          ))}
        </div>
      </div>

      {/* ── MOBILE: botón hamburguesa ── */}
      <button
        className="drawer-toggle"
        onClick={() => setDrawerOpen(true)}
        aria-label="Abrir menú de categorías"
      >
        <span />
        <span />
        <span />
      </button>

      {/* ── MOBILE: overlay ── */}
      <div
        className={`drawer-overlay ${drawerOpen ? "open" : ""}`}
        onClick={() => setDrawerOpen(false)}
      />

      {/* ── MOBILE: panel lateral ── */}
      <nav className={`drawer-panel ${drawerOpen ? "open" : ""}`}>
        <span className="drawer-title">Categorías</span>
        {categories.map((cat) => (
          <button
            key={cat.id}
            className={`drawer-item ${active === cat.id ? "active" : ""}`}
            onClick={() => handleClick(cat.id)}
          >
            <div className="drawer-item-icon">
              <span className="material-symbols-outlined">{cat.icon}</span>
            </div>
            <span className="drawer-item-label">{cat.name}</span>
            <div className="drawer-item-dot" />
          </button>
        ))}
      </nav>
    </>
  );
}

Usage Example

import Categories from "./components/Categories/Categories";

function MyPage() {
  const handleCategoryChange = (categoryId) => {
    console.log(`Selected category: ${categoryId}`);
    // Fetch products for this category
  };
  
  return (
    <Categories onCategoryChange={handleCategoryChange} />
  );
}

Responsive Behavior

  • Horizontal scrollable menu of category buttons
  • Icons displayed above labels
  • Hover effects and active state underline
  • Hamburger button hidden via CSS
The component assumes the API endpoint is available and returns properly formatted data. Network errors are caught and displayed to the user.

Build docs developers (and LLMs) love