Component Architecture
The application follows a modular component structure with clear separation of concerns:
- Layout Components: Header, Footer
- Page Components: Full page views
- Feature Components: Domain-specific components (Carrito, Producto, Contacto)
- Shared Components: Reusable UI elements
Layout Components
The main navigation header with sticky positioning and gradient accent.
src/components/Header.jsx
import { Link } from "react-router-dom";
import { Logo, IcoHome, IcoMail, IcoCart, IcoUser } from "./icono";
const NavBtn = ({ to, icon: Icon, label, accent }) => (
<Link to={to} style={{ textDecoration: "none" }}>
<div
style={{
display: "flex", alignItems: "center", gap: "8px",
padding: "10px 18px", borderRadius: "12px",
fontWeight: 600, fontSize: "13px",
color: accent ? "#fff" : "#3a5244",
background: accent ? "linear-gradient(135deg, #3d6b4f, #2d5140)" : "transparent",
cursor: "pointer",
transition: "all 0.2s ease",
boxShadow: accent ? "0 4px 14px rgba(61,107,79,0.28)" : "none",
}}
>
<Icon />
{label}
</div>
</Link>
);
export const Header = () => (
<header style={{
position: "sticky", top: 0, zIndex: 1000,
height: "72px", width: "100%",
background: "rgba(245,237,224,0.92)",
backdropFilter: "blur(20px)",
borderBottom: "1px solid rgba(85,124,85,0.1)",
display: "flex", alignItems: "center",
justifyContent: "space-between",
padding: "0 5%",
}}>
<Link to="/" style={{ textDecoration: "none", display: "flex", alignItems: "center", gap: "10px" }}>
<Logo />
<span style={{ fontWeight: 800, fontSize: "1.8rem", color: "#2d5140" }}>
Huellitas
</span>
</Link>
<nav style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<NavBtn to="/" icon={IcoHome} label="Inicio" />
<NavBtn to="/contacto" icon={IcoMail} label="Contacto" />
<NavBtn to="/carrito" icon={IcoCart} label="Carrito" />
<NavBtn to="/login" icon={IcoUser} label="Iniciar Sesión" accent />
</nav>
</header>
);
src/components/Header.jsx
Features
Sticky Navigation
Header stays at the top during scroll
Backdrop Blur
Glassmorphism effect with blur backdrop
Gradient Accent
Top border with brand gradient
Hover Effects
Interactive hover states on nav items
Minimal footer with social media links.
src/components/Footer.jsx
export const Footer = () => (
<footer style={{
background: "rgba(245,237,224,0.95)",
borderTop: "1px solid rgba(85,124,85,0.12)",
padding: "2rem 5%",
display: "flex", justifyContent: "space-between", alignItems: "center",
color: "#3a5244",
}}>
<p style={{ fontWeight: 700, fontSize: "1.4rem", opacity: 0.7 }}>
Sitio realizado por Micaela
</p>
<nav style={{ display: "flex", gap: "1rem" }}>
{["Instagram", "Facebook", "WhatsApp"].map(red => (
<span key={red} style={{
fontWeight: 600, fontSize: "1.4rem",
padding: "7px 14px", borderRadius: "10px",
background: "rgba(85,124,85,0.07)",
cursor: "pointer",
}}>
{red}
</span>
))}
</nav>
</footer>
);
Product Components
ProductCard Component
Advanced product card with modal expansion and cart integration.
src/components/Producto/ProductCard.jsx
import React, { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import useCarrito from "../../hooks/useCarrito";
const ProductCard = ({ producto }) => {
const { agregarAlCarrito } = useCarrito();
const [expanded, setExpanded] = useState(false);
const [added, setAdded] = useState(false);
const handleAdd = (e) => {
e.stopPropagation();
agregarAlCarrito({
id: producto.idProducto,
nombre: producto.nombre,
precio: producto.precio,
img: producto.img,
talle: producto.talle ?? null,
});
setAdded(true);
setTimeout(() => setAdded(false), 1500);
};
return (
<>
{/* Modal expandido */}
<AnimatePresence>
{expanded && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={() => setExpanded(false)}
>
{/* Modal content with product details */}
</motion.div>
)}
</AnimatePresence>
{/* Card normal */}
<motion.div
whileHover={{ y: -6 }}
onClick={() => setExpanded(true)}
className="group relative bg-white rounded-2xl overflow-hidden cursor-pointer"
>
<div className="w-full h-[200px] overflow-hidden">
{producto.img ? (
<img
src={producto.img}
alt={producto.nombre}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<span style={{ fontSize: "3.5rem" }}>🐾</span>
</div>
)}
</div>
<div className="p-4">
<h3 className="font-bold text-sm" style={{ color: "#2d5140" }}>
{producto.nombre}
</h3>
<span className="font-extrabold text-sm" style={{ color: "#2d7a3c" }}>
${producto.precio?.toLocaleString("es-AR")}
</span>
<motion.button
whileTap={{ scale: 0.95 }}
onClick={handleAdd}
className="w-full py-2.5 rounded-xl text-sm font-bold"
>
{added ? "✓ Agregado" : "+ Agregar al carrito"}
</motion.button>
</div>
</motion.div>
</>
);
};
src/components/Producto/ProductCard.jsx
Key Features
Hover Animation
Card lifts up on hover using Framer Motion
Modal Expansion
Click to expand product details in a modal overlay
Cart Integration
Direct integration with shopping cart context
Visual Feedback
Button changes state when product is added
The component uses AnimatePresence from Framer Motion to animate the modal entrance and exit smoothly.
Cart Components
CarritoItem Component
Individual cart item with quantity controls.
src/components/Carrito/CarritoItem.jsx
import { useState } from "react";
const CarritoItem = ({ item, updateCantidad, removeItem }) => {
const [hovered, setHovered] = useState(false);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
background: "#fff",
borderRadius: "20px",
border: "1.5px solid #f0e6d8",
boxShadow: hovered
? "0 12px 36px rgba(0,0,0,0.1)"
: "0 4px 16px rgba(0,0,0,0.05)",
padding: "1.2rem",
display: "grid",
gridTemplateColumns: "110px 1fr auto auto",
gap: "1.4rem",
alignItems: "center",
transition: "all 0.25s ease",
}}
>
{/* Imagen */}
<div style={{
width: "110px", height: "110px", borderRadius: "14px",
background: "linear-gradient(135deg, #faf5ee, #f2e9dc)",
}}>
{item.img
? <img src={item.img} alt={item.nombre} style={{ width: "100%", height: "100%", objectFit: "cover" }}/>
: <span style={{ fontSize: "2.8rem" }}>🐾</span>
}
</div>
{/* Info */}
<div>
<h3 style={{ fontWeight: 800, fontSize: "0.95rem", color: "#2d5140" }}>
{item.nombre}
</h3>
<p style={{ fontSize: "0.9rem", fontWeight: 700, color: "#3d6b4f" }}>
${item.precio.toLocaleString("es-AR")}
</p>
</div>
{/* Cantidad */}
<div style={{
display: "flex", alignItems: "center", gap: "10px",
background: "#faf5ee", borderRadius: "12px",
padding: "0.4rem 0.8rem",
}}>
<button onClick={() => updateCantidad(item.id, item.cantidad - 1)}>−</button>
<span style={{ fontWeight: 800 }}>{item.cantidad}</span>
<button onClick={() => updateCantidad(item.id, item.cantidad + 1)}>+</button>
</div>
{/* Total + eliminar */}
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end" }}>
<span style={{ fontWeight: 800, fontSize: "1.1rem", color: "#2d5140" }}>
${(item.precio * item.cantidad).toLocaleString("es-AR")}
</span>
<button onClick={() => removeItem(item.id)}>
🗑 Eliminar
</button>
</div>
</div>
);
};
src/components/Carrito/CarritoItem.jsx
Component Props
ProductCard Props
Product object from the API
Unique product identifier
Product size (if applicable)
CarritoItem Props
Function to update item quantity(id: number, cantidad: number) => void
Function to remove item from cart
Styling Approach
The application uses inline styles with a consistent design system:
Color Palette
- Primary:
#3d6b4f
- Dark:
#2d5140
- Light:
#f5ede0
Typography
- Font: Plus Jakarta Sans
- Weights: 400, 600, 700, 800
Spacing
- Consistent rem-based spacing
- Responsive padding/margins
While Tailwind is installed, most components use inline styles for better component encapsulation.
Animation Patterns
Framer Motion is used extensively for smooth interactions:
import { motion, AnimatePresence } from "framer-motion";
// Hover animation
<motion.div whileHover={{ y: -6 }} transition={{ duration: 0.25 }}>
{/* content */}
</motion.div>
// Tap animation
<motion.button whileTap={{ scale: 0.95 }}>
Click me
</motion.button>
// Enter/exit animation
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
>
{/* content */}
</motion.div>
)}
</AnimatePresence>
Next Steps
Routing
Learn about page routing
Cart System
Deep dive into cart implementation