// components.jsx — Tienda Padel home (e-commerce clásico optimizado) const { useState, useEffect, useRef, useMemo } = React; // ───────── Iconos ───────── const Icon = { Search: (p) => , Bag: (p) => , User: (p) => , Heart: (p) => , Phone: (p) => , Truck: (p) => , Shield: (p) => , Return: (p) => , Card: (p) => , Chevron: (p) => , Arrow: (p) => , Star: (p) => , Plus: (p) => , Check: (p) => , Menu: (p) => , Mail: (p) => , }; // ───────── Favoritos (localStorage, sincronizado entre páginas/pestañas) ───────── const FAV_PAGE = "/?page_id=11062"; const FAV_KEY = "top_favs"; const Fav = { list() { try { return JSON.parse(localStorage.getItem(FAV_KEY) || "[]").map(Number); } catch (e) { return []; } }, has(id) { return Fav.list().includes(Number(id)); }, toggle(id) { id = Number(id); const l = Fav.list(); const i = l.indexOf(id); if (i >= 0) l.splice(i, 1); else l.push(id); localStorage.setItem(FAV_KEY, JSON.stringify(l)); window.dispatchEvent(new CustomEvent("favchange", { detail: l })); return l; }, count() { return Fav.list().length; }, }; function useFavSync(read) { const [val, setVal] = useState(read); useEffect(() => { const h = () => setVal(read()); window.addEventListener("favchange", h); window.addEventListener("storage", h); // sincroniza entre pestañas return () => { window.removeEventListener("favchange", h); window.removeEventListener("storage", h); }; }, []); return val; } const useFavCount = () => useFavSync(() => Fav.count()); function FavButton({ id, className = "pc-fav" }) { const on = useFavSync(() => Fav.has(id)); const toggle = (e) => { if (e) { e.preventDefault(); e.stopPropagation(); } Fav.toggle(id); }; return ( ); } // ───────── Carrito REAL (WooCommerce Store API) ───────── const CART_PAGE = "/carrito/"; const CHECKOUT_PAGE = "/checkout/"; const STORE_API = "/wp-json/wc/store/v1"; function fmtMinor(value, minor) { const div = Math.pow(10, (minor == null ? 2 : minor)); return (Number(value || 0) / div).toFixed(2).replace(".", ",") + " €"; } const Cart = { _nonce: null, _grabNonce(res) { const n = res.headers.get("Nonce") || res.headers.get("X-WC-Store-API-Nonce"); if (n) Cart._nonce = n; }, _broadcast(cart) { try { window.__tpCart = cart; window.dispatchEvent(new CustomEvent("cartchange", { detail: cart })); } catch (e) {} }, async get() { const res = await fetch(`${STORE_API}/cart`, { credentials: "same-origin", headers: { "Accept": "application/json" } }); Cart._grabNonce(res); const cart = await res.json(); Cart._broadcast(cart); return cart; }, async _ensureNonce() { if (!Cart._nonce) await Cart.get(); return Cart._nonce; }, async add(id, quantity = 1) { await Cart._ensureNonce(); const res = await fetch(`${STORE_API}/cart/add-item`, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "Accept": "application/json", "Nonce": Cart._nonce || "" }, body: JSON.stringify({ id: Number(id), quantity }), }); Cart._grabNonce(res); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.message || ("Error " + res.status)); } const cart = await res.json(); Cart._broadcast(cart); return cart; }, async removeItem(key) { await Cart._ensureNonce(); const res = await fetch(`${STORE_API}/cart/remove-item`, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "Accept": "application/json", "Nonce": Cart._nonce || "" }, body: JSON.stringify({ key }), }); Cart._grabNonce(res); const cart = await res.json(); Cart._broadcast(cart); return cart; }, }; // Hook: estado del carrito (se hidrata una vez y escucha "cartchange") function useCart() { const [cart, setCart] = useState(() => (typeof window !== "undefined" ? window.__tpCart : null) || null); useEffect(() => { const h = (e) => setCart(e.detail || window.__tpCart || null); window.addEventListener("cartchange", h); if (!window.__tpCart) Cart.get().catch(() => {}); return () => window.removeEventListener("cartchange", h); }, []); return cart; } // Función global: añadir al carrito desde cualquier tarjeta/ficha + abrir drawer. // Usa el endpoint AJAX de WooCommerce (maneja productos simples Y variables igual que // el "add to cart" nativo); luego refresca el carrito vía Store API y abre el drawer. // opts: { quantity, variation_id, attributes: { "attribute_pa_color": "rojo", ... } } window.tpAddToCart = async function (id, btn, opts) { opts = opts || {}; try { if (btn) { btn.classList.add("is-loading"); btn.disabled = true; } const body = new URLSearchParams(); body.set("product_id", id); body.set("quantity", opts.quantity || 1); if (opts.variation_id) body.set("variation_id", opts.variation_id); if (opts.attributes) for (const k in opts.attributes) { if (opts.attributes[k]) body.set(k, opts.attributes[k]); } const res = await fetch("/?wc-ajax=add_to_cart", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, body: body.toString(), }); const data = await res.json().catch(() => null); // Producto variable sin opciones elegidas → WooCommerce pide ir a la ficha if (data && data.error && data.product_url) { window.location.href = data.product_url; return; } await Cart.get(); // refresca badge + drawer (misma sesión) window.dispatchEvent(new CustomEvent("cartopen")); } catch (e) { console.warn("[Cart] add:", e.message); alert("No se pudo añadir al carrito."); } finally { if (btn) { btn.classList.remove("is-loading"); btn.disabled = false; } } }; // ───────── Drawer lateral del carrito ───────── function CartDrawer() { const cart = useCart(); const [open, setOpen] = useState(false); useEffect(() => { const openH = () => setOpen(true); window.addEventListener("cartopen", openH); return () => window.removeEventListener("cartopen", openH); }, []); useEffect(() => { const esc = (e) => { if (e.key === "Escape") setOpen(false); }; if (open) document.addEventListener("keydown", esc); return () => document.removeEventListener("keydown", esc); }, [open]); const items = (cart && cart.items) || []; const minor = cart && cart.totals ? cart.totals.currency_minor_unit : 2; const count = cart ? cart.items_count : 0; const subtotal = cart && cart.totals ? cart.totals.total_items : 0; return ( <>
{/* Buscador: Motive Commerce Search se engancha a `.hd-search` (MOTIVE_TRIGGER_SELECTOR)
y abre su capa al hacer clic. Si Motive no estuviera activo, el form hace
búsqueda nativa (/?s=) que Motive también enriquece en la página de resultados. */}
{s.text}
{c.desc}
{c.n} productosAplicable a la selección de palas Nox, Head, Bullpadel y Adidas. Hasta agotar stock.
Responde 4 preguntas y nuestro algoritmo te recomienda la pala que mejor encaja con tu juego.
Stock anterior con descuento real, mientras dure el inventario.
Ir al outletTests, tutoriales y entrevistas con quienes pisan pista cada día.
"{r.text}"
Drops, tests y pre-acceso a ofertas. Una vez al mes, sin spam.