// 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 ( <>
setOpen(false)} aria-hidden="true"/> ); } // ───────── Pala SVG ───────── function PalaSVG({ tone = "lime", n = "01" }) { const tones = { lime: { a: "#D9FF3C", b: "#7BE000", g: "#0a3a00" }, emerald: { a: "#3CFFA9", b: "#00B570", g: "#003322" }, deep: { a: "#0E3A1F", b: "#08210F", g: "#0a1a0d" }, cream: { a: "#F1EBDA", b: "#D9CFB6", g: "#3a3520" }, black: { a: "#1f1f1f", b: "#000000", g: "#222" }, blue: { a: "#3CC4FF", b: "#0066B5", g: "#001a33" }, red: { a: "#FF5C5C", b: "#B50000", g: "#330000" }, yellow: { a: "#FFE03C", b: "#E0A800", g: "#332000" }, }; const t = tones[tone] || tones.lime; const id = `${tone}-${n}`; return ( {n} ); } // ───────── Top bar avisos ───────── // Wordmarks blancos (visibles sobre fondo negro) para el ticker const KlarnaMark = () => ( Klarna ); const PayPalMark = () => ( PayPal ); function TopBar() { const items = [ "Envío GRATIS desde 40€", "Devoluciones 30 días", Fracciona tus pagos con , ]; const [i, setI] = useState(0); const barRef = useRef(null); useEffect(() => { const id = setInterval(() => setI((x) => (x + 1) % items.length), 3500); return () => clearInterval(id); }, []); // Mide la altura real de la franja negra y la expone como --tp-tbh para que // el header sticky se apile justo debajo (franja + menú ambos fijos al hacer scroll). useEffect(() => { const el = barRef.current; if (!el) return; const apply = () => document.documentElement.style.setProperty("--tp-tbh", el.offsetHeight + "px"); apply(); window.addEventListener("resize", apply); let ro; if (typeof ResizeObserver !== "undefined") { ro = new ResizeObserver(apply); ro.observe(el); } return () => { window.removeEventListener("resize", apply); if (ro) ro.disconnect(); }; }, []); return (
· {items[i]}
662 655 185 Tiendas físicas
); } // ───────── Header ───────── // `cat` = slug REAL de product_cat (no usar id como categoría: "ropa"/"calzado" no existen → caerían en palas) // `cat` = slug REAL de product_cat → URL limpia /categoria-producto// const NAV_CATS = [ { id: "palas", cat: "palas", label: "Palas", href: "/categoria-producto/palas/", sub: ["Control", "Polivalentes", "Potencia", "Junior", "Edición limitada", "Por marca"] }, { id: "ropa", cat: "ropa-y-textil", label: "Ropa", href: "/categoria-producto/ropa-y-textil/", sub: ["Hombre", "Mujer", "Niños", "Camisetas", "Pantalones", "Sudaderas"] }, { id: "calzado", cat: "zapatillas", label: "Calzado", href: "/categoria-producto/zapatillas/", sub: ["Hombre", "Mujer", "Niños", "Pista dura", "Tierra batida", "Indoor"] }, { id: "pelotas", cat: "pelotas", label: "Pelotas", href: "/categoria-producto/pelotas/", sub: ["Pro", "Entreno", "Indoor", "Junior", "Por marca", "Pack ahorro"] }, { id: "accesorios", cat: "accesorios", label: "Accesorios", href: "/categoria-producto/accesorios/", sub: ["Mochilas", "Paleteros", "Grips", "Protectores", "Overgrips", "Termos"] }, { id: "marcas", label: "Marcas", href: "/marcas/", solo: true }, { id: "outlet", label: "Outlet", href: "/outlet/", solo: true, accent: true }, ]; // Megamenú — destinos. El listado pre-aplica solo ?categoria=; las facetas extra son // pistas (todo aterriza en una categoría relevante; ningún item queda en #). const SLUGM = (s) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "") .replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); const NIVEL_SLUG = { "Iniciación": "iniciacion", "Intermedio": "intermedio", "Avanzado": "avanzado", "Competición": "competicion" }; function subHref(c, s) { if (c.id === "marcas") return "/marcas/"; if (c.id === "outlet") return "/outlet/"; return `/categoria-producto/${c.cat}/`; } function Header() { const [open, setOpen] = useState(null); const [menuOpen, setMenuOpen] = useState(false); const [search, setSearch] = useState(""); const favCount = useFavCount(); const cart = useCart(); const cartCount = cart ? cart.items_count : 0; const cartTotal = cart && cart.totals ? fmtMinor(cart.totals.total_items, cart.totals.currency_minor_unit) : "0,00 €"; // Rebote de la cesta del menú cuando aumenta la cantidad (feedback "se añadió"). const bagRef = useRef(null); const prevCount = useRef(cartCount); useEffect(() => { const el = bagRef.current; if (el && cartCount > prevCount.current) { el.classList.remove("is-bump"); void el.offsetWidth; // fuerza reflow para reiniciar la animación el.classList.add("is-bump"); const t = setTimeout(() => el.classList.remove("is-bump"), 500); prevCount.current = cartCount; return () => clearTimeout(t); } prevCount.current = cartCount; }, [cartCount]); return (
Tienda Online Pádel {/* 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. */}
setSearch(e.target.value)} />
Mi cuenta {favCount} {favCount === 1 ? "pieza" : "piezas"}
{/* ── Menú móvil (hamburguesa) ── */}
setMenuOpen(false)} aria-hidden="true"/>
); } // ───────── Hero — slider de banners ───────── const SLIDES = [ { eyebrow: "Nueva temporada 2026", title: ["Tu juego.", "Tu pala.", "Tu pista."], text: "Más de 312 palas seleccionadas y testadas en pista por jugadores reales. Encuentra la tuya.", cta1: "Ver palas nuevas", cta2: "Encuentra tu pala", badge: "—25%", tone: "lime", grad: "main", }, { eyebrow: "Ofertas especiales", title: ["Outlet", "hasta", "—40%"], text: "Stock anterior, mismo equipo. Palas, calzado y ropa con descuento real mientras dure el inventario.", cta1: "Ir al outlet", cta2: "Ver más vendidos", badge: "—40%", tone: "deep", grad: "deep", }, { eyebrow: "Edición limitada", title: ["Hexa Pro", "24K Carbon"], text: "La pala que están usando los pros. Carbono 24K, EVA Soft, edición limitada de 500 unidades.", cta1: "Comprar 329€", cta2: "Ver ficha técnica", badge: "Pro", tone: "emerald", grad: "emerald", }, ]; function Hero() { const [i, setI] = useState(0); const timerRef = useRef(null); useEffect(() => { timerRef.current = setInterval(() => setI((x) => (x + 1) % SLIDES.length), 6000); return () => clearInterval(timerRef.current); }, []); const goto = (n) => { setI(n); clearInterval(timerRef.current); timerRef.current = setInterval(() => setI((x) => (x + 1) % SLIDES.length), 6000); }; const s = SLIDES[i]; return (
{s.eyebrow}

{s.title.map((t, idx) => ( {t} ))}

{s.text}

4.7/5 · Más de 22.400 reseñas verificadas
{s.badge}
{SLIDES.map((_, idx) => (
{String(i + 1).padStart(2, "0")} / {String(SLIDES.length).padStart(2, "0")}
{/* Trust bar */}
Envío gratis desde 40€
30 días para devolver
2 años de garantía
Pago en 3 plazos sin intereses
); } // ───────── Categorías destacadas ───────── const CATS = [ { id: "palas", label: "Palas", n: 312, tone: "lime", desc: "Control · Potencia · Polivalentes" }, { id: "ropa", label: "Ropa", n: 184, tone: "emerald", desc: "Camisetas · Faldas · Sudaderas" }, { id: "calzado", label: "Calzado", n: 76, tone: "deep", desc: "Pista · Tierra · Indoor" }, { id: "pelotas", label: "Pelotas", n: 29, tone: "yellow", desc: "Pro · Entreno · Indoor" }, { id: "accesorios", label: "Accesorios", n: 142, tone: "blue", desc: "Mochilas · Grips · Protectores" }, ]; function Categories() { return (
Compra por categoría

Encuentra lo que necesitas

Ver todas
{CATS.map((c) => (

{c.label}

{c.desc}

{c.n} productos
))}
); } // ───────── Producto card ───────── function ProductCard({ p }) { const discount = p.was ? Math.round((1 - p.price / p.was) * 100) : 0; return (
{p.tag && {p.tag}} {discount > 0 && −{discount}%}
{p.brand}

{p.name}

({p.reviews})
{p.was && {p.was.toFixed(2)}€} {p.price.toFixed(2)}€
); } const PRODUCTS_BEST = [ { id: 1, name: "Hexa Pro 24K", brand: "Cancha Lab", price: 329, was: 379, tone: "lime", tag: "Edición limitada", tagTone: "green", reviews: 248 }, { id: 2, name: "Blackline CTRL", brand: "Norte", price: 269, was: null, tone: "emerald", tag: "Best seller", tagTone: "lime", reviews: 412 }, { id: 3, name: "Viper Essential", brand: "Pista 7", price: 189, was: 219, tone: "deep", tag: "Nuevo", tagTone: "blue", reviews: 156 }, { id: 4, name: "Smash Tour", brand: "Cancha Lab", price: 299, was: null, tone: "blue", tag: "Pro player", tagTone: "dark", reviews: 89 }, { id: 5, name: "Drive 03", brand: "Atlas", price: 159, was: 199, tone: "red", tag: "Outlet -20%", tagTone: "red", reviews: 324 }, { id: 6, name: "Spin Cluster", brand: "Norte", price: 245, was: null, tone: "yellow", tag: "Spin máximo", tagTone: "lime", reviews: 178 }, { id: 7, name: "Court King", brand: "Volea", price: 219, was: 249, tone: "emerald", tag: null, tagTone: null, reviews: 92 }, { id: 8, name: "Power Strike", brand: "Atlas", price: 199, was: null, tone: "black", tag: "Best seller", tagTone: "lime", reviews: 267 }, ]; const PRODUCTS_NEW = [ { id: 11, name: "Aero Wing 26", brand: "Cancha Lab", price: 289, was: null, tone: "blue", tag: "Nuevo", tagTone: "blue", reviews: 23 }, { id: 12, name: "Drop Shot Plus", brand: "Doble Pared",price: 179, was: null, tone: "lime", tag: "Nuevo", tagTone: "blue", reviews: 14 }, { id: 13, name: "Reaction X", brand: "Tándem", price: 229, was: null, tone: "deep", tag: "Nuevo", tagTone: "blue", reviews: 9 }, { id: 14, name: "Combat 3D", brand: "Norte", price: 259, was: null, tone: "red", tag: "Nuevo", tagTone: "blue", reviews: 11 }, ]; function ProductsSection({ title, eyebrow, products, link, id }) { const [tab, setTab] = useState("all"); const tabs = ["Todos", "Control", "Potencia", "Polivalentes"]; const filtered = products; // placeholder — visual tab solo return (
{eyebrow}

{title}

{tabs.map((t) => ( ))} Ver todo
{filtered.map((p) => )}
); } // ───────── Banner promo full width ───────── function PromoBanner() { return (
Promoción del mes · Abril 2026

Compra 2 palas y llévate la 2ª al 50%

Aplicable a la selección de palas Nox, Head, Bullpadel y Adidas. Hasta agotar stock.

Aprovechar oferta Hasta el 30/04 · Código PADEL2X1
−50%
); } // ───────── Quiz encuentra tu pala ───────── const QUIZ = [ { q: "¿Cuál es tu nivel?", k: "level", options: [ { v: "starter", label: "Empezando", sub: "0–1 año" }, { v: "regular", label: "Cada semana", sub: "1–4 años" }, { v: "comp", label: "Competición", sub: "Federado" }, ] }, { q: "¿Qué priorizas?", k: "style", options: [ { v: "control", label: "Control", sub: "Coloco la bola" }, { v: "balance", label: "Polivalencia", sub: "Mitad y mitad" }, { v: "power", label: "Potencia", sub: "Remate y fondo" }, ] }, { q: "¿Tu posición?", k: "side", options: [ { v: "right", label: "Derecha", sub: "Defensivo" }, { v: "left", label: "Revés", sub: "Atacante" }, { v: "any", label: "Ambas", sub: "Mixto" }, ] }, { q: "¿Presupuesto?", k: "budget", options: [ { v: "low", label: "Hasta 150€", sub: "Buena relación" }, { v: "mid", label: "150 – 250€", sub: "Gama media-alta" }, { v: "hi", label: "250€ +", sub: "Top performance" }, ] }, ]; function recommend(a) { const { style = "balance", level = "regular", budget = "mid" } = a; if (style === "power" && budget === "hi") return PRODUCTS_BEST[0]; if (style === "control" && level === "comp") return PRODUCTS_BEST[1]; if (level === "starter") return PRODUCTS_BEST[4]; if (style === "balance") return PRODUCTS_BEST[3]; if (style === "control") return PRODUCTS_BEST[2]; return PRODUCTS_BEST[5]; } function Quiz() { const [step, setStep] = useState(0); const [answers, setAnswers] = useState({}); const total = QUIZ.length; const done = step >= total; const result = useMemo(() => recommend(answers), [answers]); const select = (v) => { const k = QUIZ[step].k; setAnswers((a) => ({ ...a, [k]: v })); setTimeout(() => setStep((s) => s + 1), 200); }; const reset = () => { setAnswers({}); setStep(0); }; return (
Test interactivo

Encuentra tu pala
en 30 segundos

Responde 4 preguntas y nuestro algoritmo te recomienda la pala que mejor encaja con tu juego.

Paso {Math.min(step + (done ? 0 : 1), total)} / {total} {done ? "✓ Listo" : `${Math.round(((step) / total) * 100)}% completado`}
{!done ? (
PASO {String(step + 1).padStart(2, "0")} / {String(total).padStart(2, "0")}

{QUIZ[step].q}

{QUIZ[step].options.map((o) => ( ))}
) : (
Tu pala ideal
{result.brand}

{result.name}

({result.reviews})
  • Encaja con tu nivel
  • Optimizada para tu estilo
  • Dentro de tu presupuesto
)}
); } // ───────── Marcas ───────── const BRANDS = [ "CANCHA LAB", "NORTE", "PISTA 7", "ATLAS", "VOLEA", "DOBLE PARED", "SMASH&CO", "TÁNDEM", "ESQUINA", "SPIN CO.", ]; function Brands() { return (
+47 firmas seleccionadas

Las marcas que llevas en pista

Ver todas
{BRANDS.map((b, i) => ( {b} ))}
); } // ───────── Editorial split ───────── function EditorialSplit() { return (
Outlet

Hasta −40% en piezas de pista

Stock anterior con descuento real, mientras dure el inventario.

Ir al outlet
−40%
Blog Tienda Online Padel

El cuaderno de la cancha

Tests, tutoriales y entrevistas con quienes pisan pista cada día.

  • Cómo elegir el grosor de la goma
  • Calendario WPT 2026 actualizado
  • Pala de control vs. potencia
Leer artículos
); } // ───────── Reviews ───────── const REVIEWS = [ { name: "Carlos M.", role: "Jugador 3ª categoría", text: "La Hexa Pro 24K es increíble. Llevo 2 meses con ella y mi remate ha mejorado muchísimo. Envío en 24h.", rating: 5 }, { name: "Laura V.", role: "Jugadora aficionada", text: "Pedí una pala y unas zapatillas. Llegó todo perfecto, con caja sostenible. Muy buena atención.", rating: 5 }, { name: "Diego R.", role: "Jugador federado", text: "El test de pala me recomendó la Blackline CTRL y acertaron de pleno. Mejor que ir a la tienda.", rating: 5 }, { name: "Sara P.", role: "Iniciación", text: "Soy nueva en el pádel y me han asesorado por chat para elegir pala. Súper amables y profesionales.", rating: 5 }, ]; function Reviews() { return (
Lo que dicen los jugadores

22.400 reseñas, 4.7 / 5

{REVIEWS.map((r, i) => (
{Array.from({ length: r.rating }).map((_, k) => )}

"{r.text}"

{r.name[0]}
{r.name} {r.role}
))}
); } // ───────── Newsletter ───────── function Newsletter() { const [email, setEmail] = useState(""); const [sent, setSent] = useState(false); return (
Newsletter

Suscríbete y llévate −10% en tu primer pedido

Drops, tests y pre-acceso a ofertas. Una vez al mes, sin spam.

{ e.preventDefault(); setSent(true); }}> {!sent ? ( <> setEmail(e.target.value)}/> ) : (
¡Listo! Revisa tu email para tu cupón.
)}
); } // ───────── Logos de métodos de pago (compartido: footer / ficha / checkout) ───────── function PaymentLogos() { return (
VISA PayPal Klarna. G Pay  Pay
); } // ───────── Footer ───────── function Footer() { return ( ); } Object.assign(window, { Icon, PalaSVG, TopBar, Header, Hero, Categories, ProductsSection, PROD_BEST: PRODUCTS_BEST, PROD_NEW: PRODUCTS_NEW, PromoBanner, Quiz, Brands, EditorialSplit, Reviews, Newsletter, Footer, FavButton, Fav, Cart, useCart, CartDrawer, fmtMinor, PaymentLogos, });