Skip to main content

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

Header Component

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

1

Hover Animation

Card lifts up on hover using Framer Motion
2

Modal Expansion

Click to expand product details in a modal overlay
3

Cart Integration

Direct integration with shopping cart context
4

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

producto
object
required
Product object from the API

CarritoItem Props

item
object
required
Cart item object
updateCantidad
function
required
Function to update item quantity
(id: number, cantidad: number) => void
removeItem
function
required
Function to remove item from cart
(id: number) => void

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

Build docs developers (and LLMs) love