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
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
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:
-
Product Image (1.2s)
- Slides in from left (x: -40 → 0)
- Fades in (opacity: 0 → 1)
-
Product Info (1.2s, overlaps by 1s)
- Slides in from right (x: 40 → 0)
- Fades in (opacity: 0 → 1)
-
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:
<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
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>
);
}
Navigation to Product Detail
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.