// 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;