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:
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:
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:
const marcas = [
... new Set (
productos
. filter ( p => p . categoria === nombre )
. map ( p => p . marca )
)
];
Process:
Filter products by current category
Map to extract brand names
Use Set to get unique brands
Spread into array
Brand Selection State
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
< 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:
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
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:
{ 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:
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.