Skip to main content

Overview

The Music Store product catalog fetches data from Supabase and provides filtering capabilities by category and brand. Products are displayed in a responsive grid with smooth animations.

Data Fetching

Products are fetched from Supabase on application mount:
src/App.jsx
import { useEffect, useState } from "react";
import { supabase } from "../backend/supabaseClient.js";

function App() {
  const [productos, setProductos] = useState([]);
  const [cargando, setCargando] = useState(true);

  useEffect(() => {
    async function fetchProductos() {
      const { data, error } = await supabase.from("productos").select("*");
      if (error) console.error("Error al traer productos:", error);
      else setProductos(data);
      setCargando(false);
    }
    fetchProductos();
  }, []);

  return (
    // Routes receive productos as props
    <Route path="/categoria/:nombre" element={<Categoria productos={productos} />} />
  );
}
The productos array is fetched once at the app level and passed down to all routes as props.

Product Data Structure

Each product from Supabase contains the following fields:
interface Product {
  id: number;
  nombre: string;
  precio: number;
  imagen: string;
  marca: string;
  categoria: string;
  descripcion: string;
}

Example Product

{
  "id": 1,
  "nombre": "Fender Stratocaster",
  "precio": 1200000,
  "imagen": "https://example.com/strat.jpg",
  "marca": "Fender",
  "categoria": "guitarras",
  "descripcion": "Guitarra eléctrica profesional"
}

Category Filtering

The Categoria component filters products based on the URL parameter:
src/pages/Categoria.jsx
import { useParams } from "react-router-dom";

function Categoria({ productos }) {
  const { nombre } = useParams(); // e.g., "guitarras", "bajos", "baterias"
  
  const productosFiltrados = productos.filter(p => {
    const coincideCategoria = p.categoria === nombre;
    const coincideMarca =
      marcasSeleccionadas.length === 0 ||
      marcasSeleccionadas.includes(p.marca);
    return coincideCategoria && coincideMarca;
  });
}

Route

/categoria/:nombre where nombre matches the product category

Filtering

Products are filtered by comparing p.categoria === nombre

Brand Filtering

Extracting Available Brands

The component dynamically extracts unique brands from products in the current category:
src/pages/Categoria.jsx
const marcas = [
  ...new Set(
    productos
      .filter(p => p.categoria === nombre)
      .map(p => p.marca)
  )
];
Process:
  1. Filter products by current category
  2. Map to extract brand names
  3. Use Set to get unique brands
  4. Spread into array

Brand Selection State

src/pages/Categoria.jsx
const [marcasSeleccionadas, setMarcasSeleccionadas] = useState([]);

const toggleMarca = (marca) => {
  setMarcasSeleccionadas(prev =>
    prev.includes(marca)
      ? prev.filter(m => m !== marca)
      : [...prev, marca]
  );
};
Behavior:
  • If brand is already selected: remove it from the array
  • If brand is not selected: add it to the array
  • Empty array means “show all brands”

Filter UI Implementation

src/pages/Categoria.jsx
<aside className="relative rounded-2xl h-fit sticky top-8">
  <div className="bg-white/5 backdrop-blur-md p-6">
    <h1 className="text-2xl mb-6 capitalize">{nombre}</h1>
    <h3 className="text-white/50 text-xs tracking-[0.3em] uppercase mb-5">Filtros</h3>
    <h4 className="text-sm uppercase tracking-widest text-white/40 mb-3">Marca</h4>
    <div className="flex flex-col gap-3">
      {marcas.map((marca) => (
        <label key={marca} className="flex items-center gap-3 cursor-pointer group">
          <input 
            type="checkbox" 
            onChange={() => toggleMarca(marca)} 
            className="cursor-pointer accent-white" 
          />
          <span className="text-white/60 group-hover:text-white transition-colors duration-200 text-sm">
            {marca}
          </span>
        </label>
      ))}
    </div>
  </div>
</aside>

Combined Filtering Logic

Products are filtered by both category and selected brands:
src/pages/Categoria.jsx
const productosFiltrados = productos.filter(p => {
  const coincideCategoria = p.categoria === nombre;
  const coincideMarca =
    marcasSeleccionadas.length === 0 ||
    marcasSeleccionadas.includes(p.marca);
  return coincideCategoria && coincideMarca;
});
  • coincideCategoria: Product must match the URL parameter category
  • coincideMarca: If no brands selected, show all; otherwise only show selected brands
  • Both conditions must be true for the product to appear

Product Grid Display

src/pages/Categoria.jsx
import { Link } from "react-router-dom";

<div className="grid grid-cols-[repeat(auto-fill,minmax(230px,1fr))] gap-6">
  {productosFiltrados.map(prod => (
    <Link
      key={prod.id}
      to={`/producto/${prod.id}`}
      className="bg-white/5 backdrop-blur-sm rounded-2xl p-4 text-center border border-white/10"
    >
      <img 
        src={prod.imagen} 
        alt={prod.nombre} 
        className="w-full h-[200px] object-contain mb-4" 
      />
      <h3 className="text-base font-medium mb-2">{prod.nombre}</h3>
      <p className="text-white/60 text-sm font-semibold">
        ${prod.precio.toLocaleString()}
      </p>
    </Link>
  ))}
</div>

Grid Features

Responsive Layout

Uses CSS Grid with auto-fill and minmax(230px, 1fr) for responsive columns

Navigation

Each product card links to /producto/:id for the detail view

Price Formatting

Uses toLocaleString() to format prices with thousand separators

Image Sizing

Fixed height of 200px with object-contain to maintain aspect ratio

Loading State

The catalog displays a loading indicator while fetching data:
src/pages/Categoria.jsx
{cargando ? (
  <div className="flex items-center justify-center min-h-screen">
    <div className="flex flex-col items-center gap-4">
      <div className="w-10 h-10 border-2 border-white/20 border-t-white rounded-full animate-spin" />
      <p className="text-white/30 text-sm tracking-widest uppercase">Cargando</p>
    </div>
  </div>
) : (
  // Display filtered products
)}
The cargando prop is passed from the App component and tracks the Supabase fetch status.

Filter Reset Behavior

When navigating to a different category, filters are automatically reset:
src/pages/Categoria.jsx
useEffect(() => {
  // Animations run when nombre or productosFiltrados change
}, [nombre, productosFiltrados]);
The marcasSeleccionadas state is component-local, so it resets when the component unmounts and remounts for a new category.

Build docs developers (and LLMs) love