// PLP.jsx — Product Listing Page with left filters const { useState: usePLPState, useMemo: usePLPMemo } = React; const PLP = ({ params, onNav, onAdd, onFav, favs }) => { const PLP = ({ params, onNav, onAdd, onFav, favs }) => { const [sort, setSort] = usePLPState(params?.sort || "relevance"); const [activeCat, setActiveCat] = usePLPState(params?.cat || "all"); const [priceMax, setPriceMax] = usePLPState(8000); const [onlyDiscount, setOnlyDiscount] = usePLPState(!!params?.onlyDiscount); const [brands, setBrands] = usePLPState(new Set()); const [q, setQ] = usePLPState(params?.q || ""); const [filtersOpen, setFiltersOpen] = usePLPState(false); const { isMobile, isTablet } = useViewport(); const filtered = usePLPMemo(() => { let r = CATALOG; if (activeCat !== "all") r = r.filter(p => p.cat === activeCat); if (onlyDiscount) r = r.filter(p => p.discount); if (brands.size) r = r.filter(p => brands.has(p.brand)); r = r.filter(p => p.price <= priceMax); if (q.trim()) { const needle = q.trim().toLowerCase(); r = r.filter(p => p.name.toLowerCase().includes(needle) || p.brand.toLowerCase().includes(needle) || (CATEGORIES.find(c => c.id === p.cat)?.label || "").toLowerCase().includes(needle) ); } if (sort === "price-asc") r = [...r].sort((a, b) => a.price - b.price); if (sort === "price-desc") r = [...r].sort((a, b) => b.price - a.price); if (sort === "discount") r = [...r].sort((a, b) => (b.discount || 0) - (a.discount || 0)); if (sort === "rating") r = [...r].sort((a, b) => b.rating - a.rating); return r; }, [activeCat, onlyDiscount, brands, priceMax, sort, q]); const allBrands = [...new Set(CATALOG.map(p => p.brand))]; return (
c.id === activeCat)?.label || activeCat]} /> {q.trim() && (
Resultados para "{q}"
)} {/* Category chip strip */}
setActiveCat("all")}>Todo {CATEGORIES.map(c => ( setActiveCat(c.id)}>{c.label} ))}
{/* FILTERS — desktop sidebar / mobile drawer */} {!isMobile && ( )} {isMobile && ( )} {isMobile && filtersOpen && (
setFiltersOpen(false)} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.55)", zIndex: 100 }}>
e.stopPropagation()} style={{ position: "absolute", bottom: 0, left: 0, right: 0, background: "var(--bg-elev-1)", borderRadius: "var(--radius-xl) var(--radius-xl) 0 0", padding: 20, maxHeight: "85vh", overflowY: "auto" }}>

Filtros

)} {/* GRID */}
{filtered.length} resultados
{!isMobile && Ordenar por}
{filtered.length === 0 ? (

No encontramos resultados

Cambia los filtros o amplía el rango de precio.

) : (
{filtered.map(p => onNav("pdp", { id: p.id })} onAdd={onAdd} onFav={onFav} fav={favs.has(p.id)} />)}
)}
); }; const FilterPanel = ({ priceMax, setPriceMax, onlyDiscount, setOnlyDiscount, brands, setBrands, allBrands, embedded }) => { const all = allBrands || [...new Set(CATALOG.map(p => p.brand))]; const Wrapper = embedded ? React.Fragment : 'aside'; const wrapperProps = embedded ? {} : { style: { position: "sticky", top: 160, background: "var(--bg-elev-1)", border: "1px solid var(--border-soft)", borderRadius: "var(--radius-lg)", padding: 20 } }; return ( {!embedded &&

Filtros

}
$0${priceMax.toLocaleString("es-MX")}
setPriceMax(+e.target.value)} style={{ width: "100%", accentColor: "var(--primary)" }} />
{all.map(b => ( { const next = new Set(brands); v ? next.add(b) : next.delete(b); setBrands(next); }} label={b} /> ))}
{[4, 3, 2].map(r => ( y más ))}
); }; const Chip = ({ selected, onClick, children }) => ( ); const FilterBlock = ({ title, children }) => (

{title}

{children}
); const Toggle = ({ checked, onChange, label }) => ( ); window.PLP = PLP;