Guía Interactiva de Refactorización

Sigue estos pasos para construir y refactorizar la aplicación, usando rutas relativas para máxima compatibilidad.

Paso 1: Actualizar la Base de Datos de Productos

Archivo a modificar: src/data/products.js

Acción: Reemplazar todo el contenido del archivo.


// Nuestra base de datos ahora tiene más productos y un campo de "description".
export const allProducts = [
    // --- 10 Productos de Mujer ---
    { id: 101, name: 'Body Básico Gris', category: 'Mujer', subCategory: 'Bodies', color: 'Gris', price: 99.00, imageUrl: 'https://placehold.co/600x800/6b7280/ffffff?text=Body+Gris', description: 'Confeccionado en algodón pima para una suavidad inigualable.' },
    { id: 102, name: 'Camiseta Gráfica "Nature"', category: 'Mujer', subCategory: 'Camisetas', color: 'Negro', price: 75.00, imageUrl: 'https://placehold.co/600x800/1f2937/ffffff?text=Camiseta+Nature', description: 'Estampado ecológico sobre tejido orgánico.' },
    { id: 103, name: 'Crop Top Blanco', category: 'Mujer', subCategory: 'Tops', color: 'Blanco', price: 79.00, imageUrl: 'https://placehold.co/600x800/f9fafb/111827?text=Crop+Top', description: 'El básico perfecto para cualquier look de verano.' },
    { id: 104, name: 'Joggers Negros', category: 'Mujer', subCategory: 'Pantalones', color: 'Negro', price: 159.00, imageUrl: 'https://placehold.co/600x800/111827/ffffff?text=Joggers', description: 'Comodidad y estilo para tu día a día.' },
    { id: 105, name: 'Vestido Floral', category: 'Mujer', subCategory: 'Vestidos', color: 'Multicolor', price: 189.00, imageUrl: 'https://placehold.co/600x800/fecdd3/ffffff?text=Vestido+Floral', description: 'Silueta fluida y estampado vibrante.' },
    { id: 106, name: 'Falda-Short Crema', category: 'Mujer', subCategory: 'Faldas', color: 'Blanco', price: 119.00, imageUrl: 'https://placehold.co/600x800/f5f5f4/111827?text=Falda-Short', description: 'La versatilidad de una falda, la comodidad de un short.' },
    { id: 107, name: 'Blusa de Seda', category: 'Mujer', subCategory: 'Camisas', color: 'Rosa', price: 165.00, imageUrl: 'https://placehold.co/600x800/fbcfe8/ffffff?text=Blusa+Seda', description: 'Elegancia y sofisticación en una sola prenda.' },
    { id: 108, name: 'Jeans Mom Fit', category: 'Mujer', subCategory: 'Pantalones', color: 'Azul', price: 199.00, imageUrl: 'https://placehold.co/600x800/bfdbfe/ffffff?text=Jeans+Mom+Fit', description: 'Corte retro que nunca pasa de moda.' },
    { id: 109, name: 'Top Deportivo', category: 'Mujer', subCategory: 'Tops', color: 'Negro', price: 89.00, imageUrl: 'https://placehold.co/600x800/262626/ffffff?text=Top+Deportivo', description: 'Soporte y estilo para tus entrenamientos.' },
    { id: 110, name: 'Short de Lino', category: 'Mujer', subCategory: 'Shorts', color: 'Blanco', price: 95.00, imageUrl: 'https://placehold.co/600x800/f1f5f9/1e293b?text=Short+Lino', description: 'Frescura y comodidad para los días más cálidos.' },

    // --- 10 Productos de Hombre ---
    { id: 201, name: 'Polo Clásico Marino', category: 'Hombre', subCategory: 'Polos', color: 'Azul', price: 85.00, imageUrl: 'https://placehold.co/600x800/1e3a8a/ffffff?text=Polo+Marino', description: 'Un clásico atemporal en piqué de algodón.' },
    { id: 202, name: 'Jeans Slim Fit', category: 'Hombre', subCategory: 'Pantalones', color: 'Azul', price: 179.00, imageUrl: 'https://placehold.co/600x800/374151/ffffff?text=Jeans+Slim', description: 'Corte moderno que se adapta a tu silueta.' },
    { id: 203, name: 'Camisa Lino Blanca', category: 'Hombre', subCategory: 'Camisas', color: 'Blanco', price: 110.00, imageUrl: 'https://placehold.co/600x800/e5e7eb/1c1917?text=Camisa+Lino', description: 'Ligera y transpirable, ideal para el clima cálido.' },
    { id: 204, name: 'Sudadera Urbana Gris', category: 'Hombre', subCategory: 'Buzos', color: 'Gris', price: 140.00, imageUrl: 'https://placehold.co/600x800/4b5563/ffffff?text=Sudadera+Urbana', description: 'Algodón perchado para máxima comodidad.' },
    { id: 205, name: 'Bermuda Cargo', category: 'Hombre', subCategory: 'Shorts', color: 'Verde', price: 125.00, imageUrl: 'https://placehold.co/600x800/4d7c0f/ffffff?text=Bermuda+Cargo', description: 'Diseño funcional con múltiples bolsillos.' },
    { id: 206, name: 'Camiseta Básica Negra', category: 'Hombre', subCategory: 'Camisetas', color: 'Negro', price: 55.00, imageUrl: 'https://placehold.co/600x800/0a0a0a/ffffff?text=Camiseta+Negra', description: 'El esencial que no puede faltar en tu armario.' },
    { id: 207, name: 'Chaqueta Rompevientos', category: 'Hombre', subCategory: 'Chaquetas', color: 'Negro', price: 195.00, imageUrl: 'https://placehold.co/600x800/171717/ffffff?text=Chaqueta', description: 'Protección ligera contra el viento y la lluvia.' },
    { id: 208, name: 'Pantalón Chino Beige', category: 'Hombre', subCategory: 'Pantalones', color: 'Blanco', price: 160.00, imageUrl: 'https://placehold.co/600x800/f5f5f4/1c1917?text=Pantalón+Chino', description: 'Versatilidad y elegancia para cualquier ocasión.' },
    { id: 209, name: 'Camisa de Rayas', category: 'Hombre', subCategory: 'Camisas', color: 'Azul', price: 115.00, imageUrl: 'https://placehold.co/600x800/93c5fd/ffffff?text=Camisa+Rayas', description: 'Estilo clásico con un toque moderno.' },
    { id: 210, name: 'Buzo con Capucha', category: 'Hombre', subCategory: 'Buzos', color: 'Negro', price: 150.00, imageUrl: 'https://placehold.co/600x800/262626/ffffff?text=Buzo+Capucha', description: 'Comodidad y calidez para los días fríos.' },
];
                        

Paso 2: Crear Componentes de Iconos

Acción: Crear los siguientes 3 archivos en la carpeta src/assets/components/icons/.

Estos son componentes simples que usan HTML y Tailwind para renderizar los iconos.

MenuIcons.jsx

export default () => (<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 6H20M4 12H20M4 18H20" stroke="#6B7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>);

CartIcon.jsx

export default () => (<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 21C9.55228 21 10 20.5523 10 20C10 19.4477 9.55228 19 9 19C8.44772 19 8 19.4477 8 20C8 20.5523 8.44772 21 9 21Z" stroke="#6B7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M20 21C20.5523 21 21 20.5523 21 20C21 19.4477 20.5523 19 20 19C19.4477 19 19 19.4477 19 20C19 20.5523 19.4477 21 20 21Z" stroke="#6B7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M1 1H5L7.68 14.39C7.77144 14.8504 8.02191 15.264 8.38755 15.5583C8.75318 15.8526 9.2107 16.009 9.68 16H19.4C19.8693 16.009 20.3268 15.8526 20.6925 15.5583C21.0581 15.264 21.3086 14.8504 21.4 14.39L23 6H6" stroke="#6B7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>);

CloseIcon.jsx

export default () => (<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18" stroke="#1F2937" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M6 6L18 18" stroke="#1F2937" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>);

Paso 3: Crear Componente de Navegación

Archivo a crear: src/assets/components/header/NavLinkHeader.jsx

Acción: Crear un nuevo archivo en esta ruta.


import React from 'react';

export default ({ text, onClick }) => {
    const handleClick = (e) => {
        e.preventDefault();
        if (onClick) {
            onClick();
        }
    };

    return (
        <a href="#" onClick={handleClick} className="relative py-10 group">
            <span className="group-hover:text-orange-400 transition-all duration-300">{text}</span>
            <span className="absolute bottom-0 left-0 block h-1 w-full scale-x-0 group-hover:scale-x-100 group-hover:bg-orange-400 transition-all duration-300"></span>
        </a>
    );
};
                        

Paso 4: Crear Componente del Encabezado

Archivo a crear: src/assets/components/header/MainHeader.jsx

Acción: Crear un nuevo archivo en esta ruta. Fíjate cómo todas las rutas son relativas.


import React, { useState } from 'react';
import AvatarImage from '../../images/image-avatar.png';
import MenuIcon from '../icons/MenuIcons.jsx';
import CartIcon from '../icons/CartIcon.jsx';
import CloseIcon from '../icons/CloseIcon.jsx';
import NavLinkHeader from './NavLinkHeader.jsx';

const MainHeader = ({ navigateTo }) => {
    const COMPANY_NAME = "shoreline";
    const [navClass, setnavClass] = useState("hidden font-bold md:mr-auto md:gap-4 md:flex md:flex-row top-0 left-0 p-8 md:static md:p-0 md:h-auto");

    const handleOpenMenu = () => {
        setnavClass("absolute w-4/5 font-bold flex flex-col md:mr-auto md:gap-4 md:flex md:flex-row top-0 left-0 bg-white h-full p-8 gap-y-[21px] md:static md:p-0 md:h-auto z-10");
    };

    const handleClosedMenu = () => {
        setnavClass("hidden font-bold md:mr-auto md:gap-4 md:flex md:flex-row top-0 left-0 p-8 md:static md:p-0 md:h-auto");
    };

    return (
        <>
            <header className='container mx-auto flex items-center px-4 gap-8 bg-white'>
                <button className='md:hidden' onClick={handleOpenMenu}>
                    <MenuIcon />
                </button>
                <div onClick={() => navigateTo('home')} className='mr-auto md:mr-0 font-black text-3xl text-gray-900 tracking-wider cursor-pointer'>
                    {COMPANY_NAME}
                </div>
                <nav className={navClass}>
                    <button className='mb-12 md:hidden' onClick={handleClosedMenu}>
                        <CloseIcon />
                    </button>
                    <NavLinkHeader text="Hombre" onClick={() => navigateTo('men')} />
                    <NavLinkHeader text="Mujer" onClick={() => navigateTo('women')} />
                    <NavLinkHeader text="Oferta" onClick={() => navigateTo('sale')} />
                    <NavLinkHeader text="Acerca de" onClick={() => navigateTo('about')} />
                </nav>
                <div className='flex gap-4'>
                    <button>
                        <CartIcon />
                    </button>
                    <img src={AvatarImage} alt="" className='w-10' />
                </div>
            </header>
            <span className="container mx-auto hidden md:block h-[0.2px] w-full bg-gray-500"></span>
        </>
    );
};

export default MainHeader;
                        

Paso 5: Actualizar la Tarjeta de Producto

Archivo a modificar: src/assets/components/common/ProductCard.jsx

Acción: Reemplazar todo el contenido del archivo.


import React from 'react';

const ProductCard = ({ product }) => {
    return (
        <div className="group relative flex flex-col text-left animate-fade-in">
            <div className="relative overflow-hidden rounded-lg aspect-[3/4]">
                <img 
                    src={product.imageUrl} 
                    alt={product.name}
                    className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
                />
                <div className="absolute inset-0 flex items-end justify-center p-4 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
                    <button className="w-full font-bold text-gray-900 py-3 px-6 rounded-md bg-white opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-y-4 group-hover:translate-y-0">
                        Añadir al carrito
                    </button>
                </div>
            </div>
            <div className="mt-4 flex-grow">
                <h3 className="text-md font-bold text-gray-800">{product.name}</h3>
                <p className="mt-1 text-sm text-gray-500 h-10">{product.description}</p>
            </div>
             <p className="mt-2 text-lg font-semibold text-gray-900">${product.price.toFixed(2)}</p>
        </div>
    );
};

export default ProductCard;
                        

Paso 6: Crear la Nueva Página de Categoría

Archivo a crear: src/assets/components/pages/CategoryPage.jsx

Acción: Crear un nuevo archivo con este nombre y en esa ruta.


import React, { useState, useMemo, useEffect } from 'react';
import ProductCard from '../common/ProductCard.jsx';
import FooterProduct from '../product/FooterProduct.jsx';
import { allProducts } from '../../../data/products.js';

const PRODUCTS_PER_PAGE = 8;

const CategoryPage = ({ title, category }) => {
    const [filters, setFilters] = useState({ subCategory: 'all', color: 'all' });
    const [currentPage, setCurrentPage] = useState(1);

    const filteredProducts = useMemo(() => {
        return allProducts.filter(p => {
            const categoryMatch = p.category === category;
            const subCategoryMatch = filters.subCategory === 'all' || p.subCategory === filters.subCategory;
            const colorMatch = filters.color === 'all' || p.color === filters.color;
            return categoryMatch && subCategoryMatch && colorMatch;
        });
    }, [category, filters]);

    useEffect(() => {
        setCurrentPage(1);
    }, [filters]);

    const pageCount = Math.ceil(filteredProducts.length / PRODUCTS_PER_PAGE);
    const currentProducts = filteredProducts.slice((currentPage - 1) * PRODUCTS_PER_PAGE, currentPage * PRODUCTS_PER_PAGE);

    const handleFilterChange = (filterType, value) => {
        setFilters(prevFilters => ({ ...prevFilters, [filterType]: value }));
    };
    
    const subCategories = ['all', ...new Set(allProducts.filter(p => p.category === category).map(p => p.subCategory))];
    const colors = ['all', ...new Set(allProducts.filter(p => p.category === category).map(p => p.color))];

    return (
        <div className="animate-fade-in">
            <header className="bg-gray-50 py-12">
                <div className="container mx-auto px-4 text-center">
                    <h1 className="text-5xl font-black text-gray-800">{title}</h1>
                    <p className="text-lg text-gray-600 mt-2">{filteredProducts.length} productos encontrados</p>
                </div>
            </header>

            <div className="container mx-auto px-4 my-12 flex flex-col lg:flex-row gap-8">
                <aside className="lg:w-1/4 lg:sticky top-24 self-start bg-white p-6 rounded-xl shadow-lg">
                    <h3 className="text-xl font-bold mb-4">Filtros</h3>
                    
                    <div className="mb-6">
                        <h4 className="font-semibold mb-2">Categoría</h4>
                        <div className="flex flex-col gap-2">
                            {subCategories.map(sub => (
                                <button key={sub} onClick={() => handleFilterChange('subCategory', sub)} className={`text-left capitalize py-1 px-2 rounded-md transition ${filters.subCategory === sub ? 'bg-gray-800 text-white font-bold' : 'hover:bg-gray-100'}`}>
                                    {sub === 'all' ? 'Todas' : sub}
                                </button>
                            ))}
                        </div>
                    </div>

                    <div>
                        <h4 className="font-semibold mb-2">Color</h4>
                        <div className="flex flex-wrap gap-2">
                            {colors.map(color => (
                                <button key={color} onClick={() => handleFilterChange('color', color)} className={`capitalize h-8 px-4 rounded-full text-sm transition border-2 ${filters.color === color ? 'border-gray-800 font-bold' : 'border-gray-200 hover:border-gray-400'}`}>
                                    {color === 'all' ? 'Todos' : color}
                                </button>
                            ))}
                        </div>
                    </div>
                </aside>

                <main className="flex-1">
                    <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-10">
                        {currentProducts.map(product => (
                            <ProductCard key={product.id} product={product} />
                        ))}
                    </div>
                    
                    <nav className="mt-12 flex justify-center items-center gap-4">
                        <button onClick={() => setCurrentPage(p => Math.max(p - 1, 1))} disabled={currentPage === 1} className="px-4 py-2 bg-gray-200 rounded-md disabled:opacity-50 hover:bg-gray-300">Anterior</button>
                        <span className="font-semibold">Página {currentPage} de {pageCount}</span>
                        <button onClick={() => setCurrentPage(p => Math.min(p + 1, pageCount))} disabled={currentPage === pageCount} className="px-4 py-2 bg-gray-200 rounded-md disabled:opacity-50 hover:bg-gray-300">Siguiente</button>
                    </nav>
                </main>
            </div>
            
            <FooterProduct />
        </div>
    );
};

export default CategoryPage;
                        

Paso 7: Actualizar el Componente Principal `App.jsx` (Versión Corregida)

Archivo a modificar: src/App.jsx

Acción: Reemplazar todo el contenido del archivo con esta versión final.


import React, { useState } from 'react';

// --- IMPORTACIONES CLAVE ---
// 1. Ya NO importamos MenPage ni WomenPage.
// 2. SÍ importamos el nuevo CategoryPage.
import MainHeader from './assets/components/header/MainHeader.jsx';
import MainProduct from './assets/components/product/MainProduct.jsx';
import PromocionBanner from './assets/components/product/PromocionBanner.jsx';
import FeaturedProducts from './assets/components/product/FeaturedProducts.jsx';
import FooterProduct from './assets/components/product/FooterProduct.jsx';
import CategoryPage from './assets/components/pages/CategoryPage.jsx'; // La nueva página

const HomePage = () => (
    <>
        <MainProduct />
        <PromocionBanner />
        <FeaturedProducts />
        <FooterProduct /> 
    </>
);

const App = () => {
    const [currentPage, setCurrentPage] = useState('home');

    const navigateTo = (page) => {
        setCurrentPage(page);
    };

    const renderPage = () => {
        // --- LÓGICA CLAVE ---
        // Ahora usamos CategoryPage para 'women' y 'men',
        // pasándole las props 'title' y 'category' para que sepa qué mostrar.
        switch (currentPage) {
            case 'women':
                return <CategoryPage title="Colección Mujer" category="Mujer" />;
            case 'men':
                return <CategoryPage title="Colección Hombre" category="Hombre" />;
            default:
                return <HomePage />;
        }
    };

    return (
        <div>
            <MainHeader navigateTo={navigateTo} />
            {renderPage()}
        </div>
    );
};

export default App;
                        

Paso 8: Limpieza del Proyecto (¡El paso clave!)

Ahora que CategoryPage.jsx hace todo el trabajo, vamos a eliminar los archivos que se han vuelto innecesarios para mantener nuestro código limpio y ordenado.

Acción: Eliminar los siguientes dos archivos de la carpeta src/assets/components/pages/:

  • MenPage.jsx
  • WomenPage.jsx

Paso 9: ¡A Probar!

¡Excelente trabajo!

Guarda todos los cambios, ejecuta tu aplicación y comprueba que todo funcione como se espera. Las secciones "Hombre" y "Mujer" ahora deben tener filtros funcionales y controles de paginación.