Skip to main content
The ProductoDetalle (Product Detail) component displays individual product information with animated entrance effects and shopping cart integration.

Overview

This component provides:
  • Dynamic product loading from URL parameters
  • GSAP entrance animations
  • Shopping cart integration
  • Animated Squares background
  • Glassmorphic product card design

Props

productos
Product[]
required
Array of all products fetched from Supabase

Product Data Structure

interface Product {
  id: number;
  nombre: string;
  precio: number;
  imagen: string;
  categoria: string;
  marca: string;
  descripcion?: string;
}

Implementation

ProductosDetalle.jsx
import { useParams } from "react-router-dom";
import { useEffect, useRef } from "react";
import { useCart } from "../../context/CartContext/CartContext.jsx";
import gsap from "gsap";
import NavbarGlass from "../Navbar/NavbarGlass";
import Squares from "../Squares/Squares";

function ProductoDetalle({ productos }) {
  const { id } = useParams();
  const { agregarProducto } = useCart();
  const producto = productos.find(p => p.id === Number(id));
  
  const imgRef = useRef(null);
  const infoRef = useRef(null);
  const descRef = useRef(null);

  useEffect(() => {
    if (!producto) return;

    const ctx = gsap.context(() => {
      gsap.timeline({ defaults: { ease: "power3.out" } })
        .fromTo(imgRef.current,
          { x: -40, opacity: 0 },
          { x: 0, opacity: 1, duration: 1.2 }
        )
        .fromTo(infoRef.current,
          { x: 40, opacity: 0 },
          { x: 0, opacity: 1, duration: 1.2 },
          "-=1"
        )
        .fromTo(descRef.current,
          { y: 30, opacity: 0 },
          { y: 0, opacity: 1, duration: 1 },
          "-=0.6"
        );
    });

    return () => ctx.revert();
  }, [id, producto]);

  if (!producto) {
    return (
      <div className="bg-[#0e0e0e] text-white min-h-screen flex items-center justify-center">
        <h2 className="text-2xl text-white/50">Producto no encontrado</h2>
      </div>
    );
  }

  return (
    <div className="relative bg-[#0e0e0e] text-white min-h-screen overflow-hidden">
      <div className="absolute inset-0 z-0">
        <Squares speed={0.3} squareSize={30} direction="diagonal" />
      </div>

      <div className="relative z-10">
        <NavbarGlass />
        
        <main className="max-w-6xl mx-auto px-6 pt-32 pb-20">
          <div className="bg-white/5 backdrop-blur-md rounded-3xl p-8 md:p-12 border border-white/10">
            <div className="grid grid-cols-1 md:grid-cols-2 gap-12">
              {/* Product image */}
              <div ref={imgRef} className="flex items-center justify-center">
                <img
                  src={producto.imagen}
                  alt={producto.nombre}
                  className="max-w-full h-auto object-contain rounded-xl"
                  style={{ maxHeight: "500px" }}
                />
              </div>

              {/* Product info */}
              <div ref={infoRef} className="flex flex-col justify-center">
                <p className="text-xs uppercase tracking-widest text-white/40 mb-4">
                  {producto.marca}
                </p>
                <h1 className="text-4xl md:text-5xl font-bold mb-6">
                  {producto.nombre}
                </h1>
                <p ref={descRef} className="text-white/60 text-lg mb-8 leading-relaxed">
                  {producto.descripcion || "Producto de alta calidad."}
                </p>
                <p className="text-3xl font-semibold mb-8">
                  ${producto.precio.toLocaleString()}
                </p>
                <button
                  onClick={() => agregarProducto(producto)}
                  className="px-8 py-4 bg-white text-black rounded-full font-semibold hover:bg-white/90 transition-all duration-300"
                >
                  Agregar al Carrito
                </button>
              </div>
            </div>
          </div>
        </main>
      </div>
    </div>
  );
}

export default ProductoDetalle;

Animation Timeline

Three elements animate in sequence:
  1. Product Image (1.2s)
    • Slides in from left (x: -40 → 0)
    • Fades in (opacity: 0 → 1)
  2. Product Info (1.2s, overlaps by 1s)
    • Slides in from right (x: 40 → 0)
    • Fades in (opacity: 0 → 1)
  3. Description (1.0s, overlaps by 0.6s)
    • Slides up (y: 30 → 0)
    • Fades in (opacity: 0 → 1)
All use power3.out easing for smooth deceleration.

Cart Integration

The component uses the CartContext hook:
const { agregarProducto } = useCart();

// Add to cart on button click
<button onClick={() => agregarProducto(producto)}>
  Agregar al Carrito
</button>
See Shopping Cart documentation for more details.

Routing

Product detail is accessed via URL parameter:
App.jsx
<Route path="/producto/:id" element={<ProductoDetalle productos={productos} />} />
The component extracts the product ID:
const { id } = useParams();
const producto = productos.find(p => p.id === Number(id));

Error Handling

If the product is not found, a fallback message is displayed:
if (!producto) {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <h2 className="text-2xl text-white/50">Producto no encontrado</h2>
    </div>
  );
}

Styling

Layout

  • Two-column grid on desktop (stacks on mobile)
  • Maximum width container (max-w-6xl)
  • Centered vertical alignment

Visual Effects

  • Glassmorphic product card (bg-white/5 with backdrop-blur)
  • Animated Squares background
  • Rounded corners with subtle borders
  • White CTA button with black text

Usage in Routing

App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import ProductoDetalle from "./components/productos/ProductosDetalle";

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

  useEffect(() => {
    async function fetchProductos() {
      const { data } = await supabase.from("productos").select("*");
      setProductos(data);
    }
    fetchProductos();
  }, []);

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/producto/:id" element={
          <ProductoDetalle productos={productos} />
        } />
      </Routes>
    </BrowserRouter>
  );
}
From category or product lists:
import { Link } from "react-router-dom";

<Link to={`/producto/${producto.id}`}>
  <img src={producto.imagen} alt={producto.nombre} />
  <h3>{producto.nombre}</h3>
  <p>${producto.precio.toLocaleString()}</p>
</Link>
The component expects productos prop to be passed from the parent App component where data is fetched from Supabase.
Consider adding a loading state while productos array is being populated to avoid the “not found” message on initial render.

Build docs developers (and LLMs) love