// Components for Zubeltzu Torre — reusable bits // Exports to window for cross-file use. const { useState, useEffect, useRef, useCallback, useMemo, useLayoutEffect } = React; /* ============================================================ CUSTOM CURSOR ============================================================ */ function Cursor({ enabled }) { const ref = useRef(null); const [variant, setVariant] = useState(""); useEffect(() => { if (!enabled) { document.documentElement.classList.remove("cursor-on"); return; } document.documentElement.classList.add("cursor-on"); let x = window.innerWidth / 2, y = window.innerHeight / 2; let tx = x, ty = y; let raf; const onMove = (e) => { tx = e.clientX; ty = e.clientY; }; const tick = () => { x += (tx - x) * 0.22; y += (ty - y) * 0.22; if (ref.current) ref.current.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`; raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); const onOver = (e) => { const target = e.target.closest("[data-cursor]"); setVariant(target?.dataset.cursor || ""); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseover", onOver); return () => { cancelAnimationFrame(raf); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseover", onOver); document.documentElement.classList.remove("cursor-on"); }; }, [enabled]); if (!enabled) return null; const cls = "cursor" + (variant === "hover" ? " is-hover" : variant === "drag" ? " is-drag" : ""); const label = variant === "drag" ? "Drag" : variant === "hover" ? "View" : ""; return
{label}
; } /* ============================================================ PARALLAX wrapper ============================================================ */ function ParallaxY({ speed = 0.2, children, className = "" }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; let raf; const tick = () => { if (!el) return; const rect = el.getBoundingClientRect(); const center = rect.top + rect.height / 2; const vh = window.innerHeight; const delta = (center - vh / 2) / vh; const intensity = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--parallax")) || 1; el.style.transform = `translate3d(0, ${(-delta * speed * 120 * intensity).toFixed(2)}px, 0)`; raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [speed]); return
{children}
; } /* ============================================================ LANG DROPDOWN (with flag SVGs) ============================================================ */ const LANGS = [ { code: "eu", name: "Euskara" }, { code: "es", name: "Castellano" }, { code: "en", name: "English" }, { code: "fr", name: "Français" } ]; function Flag({ code }) { const common = { width: 22, height: 14, viewBox: "0 0 30 20", style: { borderRadius: 2, boxShadow: "0 0 0 1px rgba(0,0,0,0.18)", flexShrink: 0, display: "block" } }; if (code === "eu") { return ( {/* aspa verde (cruz de San Andrés) */} {/* cruz blanca encima */} ); } if (code === "es") { return ( ); } if (code === "en") { return ( {/* white diagonals */} {/* red diagonals */} {/* white cross */} {/* red cross */} ); } if (code === "fr") { return ( ); } return null; } function LangDropdown({ lang, setLang }) { const [open, setOpen] = useState(false); const ref = useRef(null); const cur = LANGS.find(l => l.code === lang) || LANGS[1]; useEffect(() => { if (!open) return; const onDocClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; const onKey = (e) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", onDocClick); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDocClick); document.removeEventListener("keydown", onKey); }; }, [open]); return (
{LANGS.map(l => ( ))}
); } function Nav({ t, lang, setLang, activePage, goTo, onBook }) { const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY >= 80); onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); // lock body scroll while mobile menu is open useEffect(() => { document.body.style.overflow = menuOpen ? "hidden" : ""; return () => { document.body.style.overflow = ""; }; }, [menuOpen]); const nav = (page) => { setMenuOpen(false); goTo(page); }; // hide only at top of HOME — on inner pages always visible const hidden = activePage === "home" && !scrolled && !menuOpen; return (
goTo("home")}>
Zubeltzu Torre
{/* MOBILE: hamburger */} {/* MOBILE: drawer */}
); } /* ============================================================ BOOKING PILL — floating CTA ============================================================ */ function BookPill({ label, onClick }) { return ( ); } /* ============================================================ TILT — 3D hover wrapper ============================================================ */ function Tilt({ children, max = 8, className = "" }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; // En dispositivos táctiles (sin hover) no aplicamos tilt: evita repintados y problemas de toque. if (window.matchMedia && window.matchMedia("(hover: none)").matches) return; let raf = null; let rx = 0, ry = 0, trx = 0, try_ = 0; const inner = el.querySelector(".tilt-inner") || el; // El bucle se PARA al asentarse. Escribir transform en cada frame de forma // continua (incluso en reposo) repinta toda la página y hace parpadear el // cursor/hover ("no se mantiene la opción de clickar"). Aquí solo corre // mientras el ratón se mueve y se detiene en cuanto converge. const tick = () => { rx += (trx - rx) * 0.18; ry += (try_ - ry) * 0.18; const settled = Math.abs(trx - rx) < 0.02 && Math.abs(try_ - ry) < 0.02; if (settled) { rx = trx; ry = try_; } if (trx === 0 && try_ === 0 && settled) { inner.style.transform = ""; // reposo total: sin transform → sin repintado ni stacking 3D } else { inner.style.transform = `perspective(1000px) rotateX(${rx.toFixed(2)}deg) rotateY(${ry.toFixed(2)}deg)`; } if (settled) { raf = null; return; } // detener el bucle raf = requestAnimationFrame(tick); }; const start = () => { if (raf == null) raf = requestAnimationFrame(tick); }; const onMove = (e) => { const rect = el.getBoundingClientRect(); const px = (e.clientX - rect.left) / rect.width - 0.5; const py = (e.clientY - rect.top) / rect.height - 0.5; trx = -py * max; try_ = px * max; start(); }; const onLeave = () => { trx = 0; try_ = 0; start(); }; el.addEventListener("mousemove", onMove); el.addEventListener("mouseleave", onLeave); return () => { if (raf != null) cancelAnimationFrame(raf); el.removeEventListener("mousemove", onMove); el.removeEventListener("mouseleave", onLeave); }; }, [max]); return
{children}
; } /* ============================================================ MIST PARTICLES (canvas overlay) ============================================================ */ function Mist({ density = 0.6 }) { const ref = useRef(null); useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext("2d"); let w, h, raf; const dpr = Math.min(window.devicePixelRatio || 1, 2); const resize = () => { w = canvas.clientWidth; h = canvas.clientHeight; canvas.width = w * dpr; canvas.height = h * dpr; ctx.scale(dpr, dpr); }; resize(); window.addEventListener("resize", resize); const count = Math.floor(80 * density); const parts = Array.from({ length: count }, () => ({ x: Math.random() * w, y: Math.random() * h, r: 30 + Math.random() * 90, vx: (Math.random() - 0.5) * 0.15, vy: (Math.random() - 0.4) * 0.1, a: 0.02 + Math.random() * 0.05 })); const tick = () => { ctx.clearRect(0, 0, w, h); for (const p of parts) { p.x += p.vx; p.y += p.vy; if (p.x < -p.r) p.x = w + p.r; if (p.x > w + p.r) p.x = -p.r; if (p.y < -p.r) p.y = h + p.r; if (p.y > h + p.r) p.y = -p.r; const grd = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r); grd.addColorStop(0, `rgba(255,255,255,${p.a})`); grd.addColorStop(1, "rgba(255,255,255,0)"); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); }; }, [density]); return ; } /* ============================================================ useDragScroll — horizontal drag for the rooms rail ============================================================ */ function useDragScroll(ref) { useEffect(() => { const el = ref.current; if (!el) return; let isDown = false, startX = 0, scrollStart = 0, moved = false; const down = (e) => { isDown = true; moved = false; startX = (e.touches?.[0]?.pageX ?? e.pageX) - el.offsetLeft; scrollStart = el.scrollLeft; el.classList.add("is-dragging"); }; const move = (e) => { if (!isDown) return; const x = (e.touches?.[0]?.pageX ?? e.pageX) - el.offsetLeft; const walk = (x - startX) * 1.2; if (Math.abs(walk) > 6) moved = true; el.scrollLeft = scrollStart - walk; }; const up = () => { isDown = false; el.classList.remove("is-dragging"); }; el.addEventListener("mousedown", down); el.addEventListener("mousemove", move); el.addEventListener("mouseleave", up); el.addEventListener("mouseup", up); el.addEventListener("touchstart", down, { passive: true }); el.addEventListener("touchmove", move, { passive: true }); el.addEventListener("touchend", up); // wheel: convierte el scroll vertical en horizontal MIENTRAS quede carrete. // Al llegar al final (bajando) o al inicio (subiendo), libera el evento para // que la página siga su scroll vertical normal. const onWheel = (e) => { if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return; // gestos horizontales: nativo const atStart = el.scrollLeft <= 0; const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1; if ((e.deltaY > 0 && atEnd) || (e.deltaY < 0 && atStart)) { return; // sin preventDefault → la página baja/sube normalmente } el.scrollLeft += e.deltaY; e.preventDefault(); }; el.addEventListener("wheel", onWheel, { passive: false }); return () => { el.removeEventListener("mousedown", down); el.removeEventListener("mousemove", move); el.removeEventListener("mouseleave", up); el.removeEventListener("mouseup", up); el.removeEventListener("touchstart", down); el.removeEventListener("touchmove", move); el.removeEventListener("touchend", up); el.removeEventListener("wheel", onWheel); }; }, [ref]); } /* ============================================================ useActiveSection — scrollspy ============================================================ */ function useActiveSection(ids) { const [active, setActive] = useState(ids[0]); useEffect(() => { const obs = new IntersectionObserver((entries) => { const visible = entries.filter(e => e.isIntersecting).sort((a,b) => b.intersectionRatio - a.intersectionRatio); if (visible[0]) setActive(visible[0].target.id); }, { threshold: [0.2, 0.5, 0.8] }); ids.forEach(id => { const el = document.getElementById(id); if (el) obs.observe(el); }); return () => obs.disconnect(); }, [ids.join(",")]); return active; } /* ============================================================ CHATBOT — floating assistant (replaces book pill) ============================================================ */ const WA_URL = "https://wa.me/34637905164"; const PHONE = "+34 637 905 164"; /* Diccionario multilingüe (es/eu/en/fr) de respuestas y palabras clave. El bot funciona 100% local: clasifica la intención por palabras clave y devuelve una respuesta enlatada. No depende de ningún servicio externo. */ const LOCAL_BOT = { // Patrones por intención — el primero que casa, gana. ORDEN IMPORTA: // 1. inappropriate (alto riesgo, primero para cortar jailbreaks) // 2. greeting / thanks / bye (atajos sociales) // 3. categorías específicas (wifi/desayuno/perros/capacidad/aparcamiento/ // ubicación/cómo llegar/playa/actividades/animales/idiomas/contacto) // 4. booking AL FINAL como red de seguridad — usa solo palabras-reserva // inequívocas. Si una pregunta tiene "disponible" pero también casa antes // con otra categoría (ej. "wifi disponible"), la otra gana. intents: [ { id: "inappropriate", kw: ["politica","política","politik","politics","politique","religion","religión","sex","sexo","sexual","porn","kill","matar","drug","droga","invest","invertir","bitcoin","crypto","hack","jailbreak","ignore all","ignore the","ignora todas","ignora las","ignora todo","olvida tus","forget your","prompt injection","system prompt","developer mode","modo desarrollador","stock market","acciones bolsa","dieta","doctor","medic","trump","biden","gobierno","gobierno actual"] }, { id: "greeting", kw: ["hola","kaixo","hello","hi ","bonjour","salut","good morning","buenos dias","buenos días","egun on","aupa","buenas"] }, { id: "thanks", kw: ["gracias","eskerrik asko","thanks","thank you","merci","eskerrik"] }, { id: "bye", kw: ["adios","adiós","agur","bye","goodbye","au revoir","ciao","hasta luego"] }, { id: "wifi", kw: ["wifi","wi-fi","internet"] }, { id: "linen", kw: ["toalla","toallas","towel","serviette","eskuoihal","sábana","sabana","sábanas","sabanas","ropa de cama","bed linen","bed sheets","sheets","draps","maindire","ohe-arropa","secador","hair dryer","hairdryer","sèche-cheveux","seche-cheveux","ile-lehorgailu","amenities","amenity"] }, { id: "climate", kw: ["calefac","heating","chauffage","berogailu","aire acondicionado","aire acond","air conditioning","air-conditioning","climatiz","climatis","aire acondi","hace frío","hace frio","hace calor","hotza","beroa"] }, { id: "tv", kw: ["televis","television","télévision","telebista","tele "] }, { id: "breakfast", kw: ["desayun","gosari","breakfast","petit déjeuner","petit dejeuner","comida","cena","dinner","cenar","comer","food","what to eat"] }, { id: "pets", kw: ["perro","pet","animal de compañ","mascota","txakur","dog","chien"] }, { id: "capacity", kw: ["cuantas personas","cuántas personas","capacidad","capacity","capacit","grupo","personas","logelak","habitaciones","cuartos","rooms","apartamentos","apartment","appartement","cabe","caben","hold "] }, { id: "parking", kw: ["aparcam","parking","park","aparkalek"] }, { id: "directions",kw: ["cómo llegar","como llegar","cómo se llega","como se llega","se llega","cómo llego","como llego","llego","llegas","cómo ir","como ir","cómo voy","como voy","ir a","nola iritsi","nola iritsiko","direcc","direction","llegar","get there","get here","how do i get","arrive","comment venir","comment aller","trajet","itinerario","itinéraire","mapa","map","gps","carretera","autopista","ruta"] }, { id: "location", kw: ["donde esta","donde está","dónde está","dónde se encuentra","where is","where are","ubica","situa","ubicación","ubicacion","situé","situated","non dago","nondik","address","adresse","helbide","gipuzkoa","itziar","deba"] }, { id: "beach", kw: ["playa","beach","hondartza","plage","mar","sea","océan","ocean","itsaso","zumaia","zarautz","itzurun","mutriku","surf","getaria"] }, { id: "activities",kw: ["actividades","activities","jarduera","jarduerak","activit","que hacer","qué hacer","para hacer","what to do","quoi faire","sendero","sender","hike","hiking","sidra","sagardo","cider","cidre","camino de santiago","santiago","donejakue","kayak","canoa","caballo","horse","cheval","zaldi","flysch","cueva","cave","ekain","montar","bici","bike","vélo","ciclis","visitar","cerca","near","près","plan","planes","excursion","excursión","entreten","fun","divers"] }, { id: "animals", kw: ["animal","granja","farm","ganaderia","ganadería","abeltz","vaca","cow","behi","oilo","gallina","hen","chicken","ferme","gallinero","oveja","sheep","ardi","cabra","goat","ahuntz","cerdo","pig","txerri","ternero","calf","txahal","huerta","baratze","vegetable garden","leche","milk","kaiku"] }, { id: "languages", kw: ["idiom","language","langue","euskera","euskara","basque","english","ingles","inglés","francés","francais","frances","castellano","español","spanish","espagnol","hablan","habláis","hablais"] }, { id: "contact", kw: ["contact","contacto","kontaktu","whatsapp","telefono","teléfono","phone","email","correo","llamar","call","appeler"] }, { id: "booking", kw: ["reserv","reservar","reservation","réserv","erreserba","book ","booking","tenéis disponibilidad","tienes disponibilidad","tiene disponibilidad","tenéis libre","disponibilidad","availability","libreak","precio","tarifa","price","cuesta","cost ","prix","cuanto cuesta","cuánto cuesta","cancel","modif","factura","invoice","pago","pagar","pay","paiement","evento","grupo grande","big group","large group","check in","check-in","llegada tarde","late arrival","alergia","allergy","silla de ruedas","wheelchair","fechas","importante","urgente","urgent","urgentea","emergencia","emergency","larrialdi","problema","problème","problem","arazoa","queja","complaint","kexa","plainte","reclamac","incidencia","hitzordu"] } ], // textos por idioma txt: { es: { greeting: "¡Hola! ¿En qué puedo ayudarte? Pregúntame por el caserío, el entorno, los animales de la granja o cómo llegar.", booking: "Para reservas, fechas, precios o cambios de reserva, mejor que hables directamente con la familia del caserío. Te abro WhatsApp: " + WA_URL + " (o llama al " + PHONE + ").", inappropriate: "Lo siento, no puedo responder a eso. Solo te ayudo con información sobre Zubeltzu Torre y su entorno: el caserío, los animales, cómo llegar o qué hacer cerca.", wifi: "Sí, tienes WiFi gratuito en todo el caserío —habitaciones, apartamentos y zonas comunes—, así que puedes teletrabajar, ver una serie por la noche o compartir las fotos del viaje sin problema.", breakfast: "Servimos desayuno con productos del propio caserío: pan, leche fresca, mermeladas caseras y producto local. Cocina-comedor compartida para que comáis a vuestro ritmo.", pets: "Sí, admitimos perros pequeños. Si vienes con perro grande u otro animal, escríbenos por WhatsApp para confirmar: " + WA_URL + ".", capacity: "Hasta 16 personas (23 con camas supletorias). 5 habitaciones dobles con baño privado, 2 apartamentos independientes o 1 estudio, o la casa completa para grupos.", parking: "Sí, contamos con aparcamiento propio y gratuito dentro de la finca, con sitio de sobra. Puedes dejar el coche tranquilo y moverte a pie por los alrededores.", barbacoa: "Sí, tenemos barbacoa en las zonas comunes para que disfrutes de comidas al aire libre con vistas a la naturaleza.", linen: "Sí, tanto las habitaciones como los apartamentos incluyen ropa de cama y toallas, además de secador de pelo. No necesitas traer nada de eso: solo tus cosas personales y ganas de descansar.", climate: "Todas las habitaciones tienen calefacción para las mañanas frescas de montaña. Estamos en pleno valle atlántico, así que en verano el caserío se mantiene fresco de forma natural; si tienes alguna necesidad concreta, escríbenos por WhatsApp: " + WA_URL + ".", tv: "Sí, todas las habitaciones tienen televisión, y hay WiFi gratis en todo el caserío. Ideal para relajarse después de un día por la costa vasca.", directions: "Estamos en Itziar, a 5 minutos de Zumaia, 20 de San Sebastián y 50 de Bilbao. Gipuzkoa, País Vasco. En la web tienes el botón \"Cómo llegar\" que abre la ruta en Google Maps desde tu ubicación.", location: "Zubeltzu Torre está en Itziar, municipio de Deba (Gipuzkoa, País Vasco), en pleno valle de Arroa Erreka. Coordenadas: 43.26638, -2.30071.", beach: "A 6 km tienes Itzurun (Zumaia) con los acantilados del Flysch, Geoparque UNESCO. Deba a 20 min, Zarautz a 20 min (capital del surf), Mutriku, Getaria. Todo en coche.", activities: "Cerca: surf en Zarautz, Flysch y barco en Zumaia, senderismo en Izarraitz, Camino de Santiago del Norte a 2 km, cuevas de Ekain, sidrerías, kayak, paseos a caballo. En la web (Entorno & Actividades) tienes el mapa con distancias.", animals: "Agroturismo real: vacas de leche (sello Q de calidad, leche Kaiku), terneros, caballos, ovejas, cabras, gallinas, cerdo. Hay huerta propia. Los peques pueden ver a los animales en la granja.", languages: "Te atendemos en euskera, castellano, inglés y francés.", thanks: "Un placer. Si te surge cualquier otra duda, aquí estoy.", bye: "¡Un placer! Si quieres reservar, escríbenos por WhatsApp: " + WA_URL + ".", contact: "Teléfono: " + PHONE + ". WhatsApp: " + WA_URL + ". Email: info@zubeltzutorre.es. También estamos en Booking e Instagram.", fallback: "No estoy segura sobre eso. Si quieres una respuesta concreta, mejor escribe por WhatsApp a la familia del caserío: " + WA_URL + "." }, eu: { greeting: "Kaixo! Zertan lagundu dezaket? Galdetu baserriaz, inguruaz, baserriko animaliez edo nola iristeko.", booking: "Erreserbak, datak, prezioak edo aldaketak baserriko familiarekin zuzenean tratatzeko mesedez. WhatsApp irekitzen dizut: " + WA_URL + " (edo deitu " + PHONE + " telefonora).", inappropriate: "Sentitzen dut, ezin dut hori erantzun. Zubeltzu Torreri eta bere inguruari buruzko informazioarekin baino ezin dizut lagundu: baserria, animaliak, nola iritsi edo zer egin inguruan.", wifi: "Bai, doako WiFi-a duzu baserri osoan —logelak, apartamentuak eta gune komunak—, beraz lasai lan egin, gauez serie bat ikusi edo bidaiako argazkiak partekatu ditzakezu.", breakfast: "Bertako produktuekin egindako gosaria eskaintzen dugu: ogia, esne freskoa, etxeko marmeladak. Sukalde-jangela partekatua erabili ahal duzue zuen erritmoan jateko.", pets: "Bai, txakur txikiak onartzen ditugu. Bestelako animaliarekin baldin bazatoz, idatzi WhatsAppez: " + WA_URL + ".", capacity: "16 lagunentzat (23 koltxoi gehigarriekin). 5 logela bikoitz bainugela pribatuekin, 3 apartamentu independente (Andutz, Izarraitz, Arno) edo baserri osoa.", parking: "Bai, doaneko aparkaleku propioa dugu finkaren barruan, leku askorekin. Utzi kotxea lasai eta ibili oinez baserriaren inguruan.", linen: "Bai, logelek eta apartamentuek ohe-arropa eta eskuoihalak barne dituzte, baita ile-lehorgailua ere. Ez duzu ezer ekarri behar: zure gauza pertsonalak eta atseden hartzeko gogoa besterik ez.", climate: "Logela guztiek berogailua dute mendiko goiz freskoetarako. Haran atlantikoan gaude, beraz udan baserria berez fresko mantentzen da; behar zehatzen bat baduzu, idatzi WhatsAppez: " + WA_URL + ".", tv: "Bai, logela guztiek telebista dute, eta WiFi doan baserri osoan. Ezin hobea euskal kostaldean egun bat pasatu ondoren atseden hartzeko.", directions: "Itziarren gaude (Deba, Gipuzkoa), Zumaiatik 5 minutura, Donostiatik 20ra eta Bilbotik 50era. Webean \"Nola iritsi\" botoia duzu, Google Maps zuzenean irekitzeko.", location: "Zubeltzu Torre Itziarren dago, Deba (Gipuzkoa), Arroa Errekako haranean. Koordenatuak: 43.26638, -2.30071.", beach: "6 km-ra Itzurun (Zumaia) eta Flysch-a duzu, UNESCO Geoparkea. Deba 20 minutura, Zarautz 20 minutura (surfaren hiriburua), Mutriku, Getaria.", activities: "Inguruan: surfa Zarautzen, Flysch-a Zumaian, mendi ibilaldiak Izarraitzen, Donejakue Bidea Iparraldekoa 2 km-ra, Ekaingo kobazuloak, sagardotegiak, kayaka, zaldi ibilaldiak. Webean mapa duzu distantziekin.", animals: "Benetako agroturismoa: esnetarako behiak (Q kalitate zigilua, Kaiku esnea), txahalak, zaldiak, ardiak, ahuntzak, oiloak, txerria. Baratzea ere bai. Umeek animaliak ikusi ditzakete.", languages: "Euskara, gaztelania, ingelesa eta frantsesa erabiltzen ditugu.", thanks: "Eskerrik asko zuri! Beste edozer behar baduzu, hemen nago.", bye: "Eskerrik asko! Erreserbatu nahi baduzu, idatzi WhatsAppez: " + WA_URL + ".", contact: "Telefonoa: " + PHONE + ". WhatsApp: " + WA_URL + ". Emaila: info@zubeltzutorre.es. Booking eta Instagrameran ere bai.", fallback: "Ez nago ziur horretaz. Erantzun zehatza nahi baduzu, idatzi WhatsAppez familia: " + WA_URL + "." }, en: { greeting: "Hello! How can I help? Ask me about the farmhouse, the area, the farm animals or how to get here.", booking: "For bookings, dates, prices or changes, please contact the family directly. I'll open WhatsApp for you: " + WA_URL + " (or call " + PHONE + ").", inappropriate: "Sorry, I can't answer that. I can only help with information about Zubeltzu Torre and its surroundings: the farmhouse, the animals, how to get here or what to do nearby.", wifi: "Yes, there's free WiFi throughout the farmhouse —rooms, apartments and common areas— so you can work remotely, stream a series in the evening or share your trip photos with ease.", breakfast: "We serve breakfast made with our own farm products: bread, fresh milk, homemade jams, local produce. Shared kitchen-diner so you can eat at your own pace.", pets: "Yes, small dogs are welcome. For larger pets, please message us on WhatsApp to confirm: " + WA_URL + ".", capacity: "Up to 16 people (23 with extra beds). 5 double rooms with private bathroom, 3 self-contained apartments (Andutz, Izarraitz, Arno) or the whole farmhouse for groups.", parking: "Yes, we have our own free parking within the property, with plenty of space. Leave the car with peace of mind and explore the surroundings on foot.", linen: "Yes, both the rooms and the apartments come with bed linen and towels, plus a hair dryer. You don't need to bring any of that —just your personal things and a wish to relax.", climate: "All rooms have heating for the cool mountain mornings. We're deep in an Atlantic valley, so in summer the farmhouse stays naturally cool; if you have any specific need, message us on WhatsApp: " + WA_URL + ".", tv: "Yes, every room has a TV, and there's free WiFi throughout the farmhouse. Perfect to unwind after a day along the Basque coast.", directions: "We're in Itziar (Deba, Gipuzkoa) — 5 min from Zumaia, 20 min from San Sebastián and 50 min from Bilbao. The \"How to get there\" button on the site opens Google Maps directions from your location.", location: "Zubeltzu Torre is in Itziar, Deba (Gipuzkoa, Basque Country), in the Arroa Erreka valley. Coordinates: 43.26638, -2.30071.", beach: "6 km away you have Itzurun (Zumaia) with the Flysch cliffs, a UNESCO Geopark. Deba 20 min, Zarautz 20 min (surf capital), Mutriku, Getaria. All by car.", activities: "Nearby: surfing at Zarautz, Flysch and boat trips in Zumaia, hiking on Izarraitz, the Northern Way of St. James 2 km away, Ekain caves, cider houses, kayak, horse riding. See the map on the Surroundings page.", animals: "A real agritourism: dairy cows (Q-quality seal, Kaiku milk), calves, horses, sheep, goats, hens, a pig. Vegetable garden too. Kids can visit the animals.", languages: "We speak Basque, Spanish, English and French.", thanks: "A pleasure. If anything else comes up, I'm here.", bye: "Take care! To book, message us on WhatsApp: " + WA_URL + ".", contact: "Phone: " + PHONE + ". WhatsApp: " + WA_URL + ". Email: info@zubeltzutorre.es. Also on Booking and Instagram.", fallback: "I'm not sure about that. For a precise answer, please message the family on WhatsApp: " + WA_URL + "." }, fr: { greeting: "Bonjour ! Comment puis-je vous aider ? Demandez-moi sur la ferme, les environs, les animaux ou comment venir.", booking: "Pour les réservations, dates, prix ou changements, il vaut mieux contacter directement la famille. Je vous ouvre WhatsApp : " + WA_URL + " (ou appelez le " + PHONE + ").", inappropriate: "Désolée, je ne peux pas répondre à cela. Je peux seulement vous aider avec des informations sur Zubeltzu Torre et ses environs : la ferme, les animaux, comment venir ou que faire à proximité.", wifi: "Oui, le WiFi gratuit couvre toute la ferme —chambres, appartements et espaces communs— pour télétravailler, regarder une série le soir ou partager vos photos de voyage sans souci.", breakfast: "Petit-déjeuner avec les produits de la ferme : pain, lait frais, confitures maison, produits locaux. Cuisine commune pour manger à votre rythme.", pets: "Oui, nous acceptons les petits chiens. Pour d'autres animaux, écrivez-nous sur WhatsApp : " + WA_URL + ".", capacity: "Jusqu'à 16 personnes (23 avec lits d'appoint). 5 chambres doubles avec salle de bain privée, 3 appartements indépendants (Andutz, Izarraitz, Arno) ou la ferme entière.", parking: "Oui, nous disposons d'un parking privé gratuit sur la propriété, avec beaucoup de place. Laissez la voiture tranquille et explorez les environs à pied.", linen: "Oui, les chambres comme les appartements incluent le linge de lit et les serviettes, ainsi qu'un sèche-cheveux. Vous n'avez rien à apporter : juste vos affaires personnelles et l'envie de vous reposer.", climate: "Toutes les chambres disposent du chauffage pour les matinées fraîches de montagne. Nous sommes au cœur d'une vallée atlantique : en été, la ferme reste naturellement fraîche ; pour un besoin particulier, écrivez-nous sur WhatsApp : " + WA_URL + ".", tv: "Oui, chaque chambre a une télévision, et le WiFi gratuit couvre toute la ferme. Idéal pour se détendre après une journée sur la côte basque.", directions: "Nous sommes à Itziar (Deba, Gipuzkoa), à 5 min de Zumaia, 20 min de Saint-Sébastien et 50 min de Bilbao. Le bouton \"Comment venir\" du site ouvre l'itinéraire dans Google Maps.", location: "Zubeltzu Torre est à Itziar, Deba (Gipuzkoa, Pays Basque), dans la vallée d'Arroa Erreka. Coordonnées : 43.26638, -2.30071.", beach: "À 6 km, Itzurun (Zumaia) avec les falaises du Flysch, Géoparc UNESCO. Deba à 20 min, Zarautz à 20 min (capitale du surf), Mutriku, Getaria.", activities: "À proximité : surf à Zarautz, Flysch et bateau à Zumaia, randonnée à Izarraitz, Chemin de Saint-Jacques à 2 km, grottes d'Ekain, cidreries, kayak, balades à cheval. Carte sur la page Cadre.", animals: "Vrai agritourisme : vaches laitières (label Q de qualité, lait Kaiku), veaux, chevaux, brebis, chèvres, poules, un cochon. Potager aussi. Les enfants peuvent visiter les animaux.", languages: "Nous parlons basque, espagnol, anglais et français.", thanks: "Avec plaisir. Si vous avez d'autres questions, je suis là.", bye: "À bientôt ! Pour réserver, écrivez-nous sur WhatsApp : " + WA_URL + ".", contact: "Téléphone : " + PHONE + ". WhatsApp : " + WA_URL + ". Email : info@zubeltzutorre.es. Aussi sur Booking et Instagram.", fallback: "Je n'en suis pas sûre. Pour une réponse précise, écrivez à la famille sur WhatsApp : " + WA_URL + "." } } }; function classifyIntent(text) { const low = ((text || "") + "").toLowerCase(); // las inadecuadas y las de reserva tienen prioridad para no responder por error for (const intent of LOCAL_BOT.intents) { if (intent.kw.some(k => low.includes(k))) return intent.id; } return null; } function localReply(userText, lang) { const dict = LOCAL_BOT.txt[lang] || LOCAL_BOT.txt.es; const id = classifyIntent(userText); return dict[id] || dict.fallback; } /* Convierte URLs (http/https) y enlaces wa.me en clicables. Los wa.me/* se etiquetan como "WhatsApp" y abren la app si está instalada. */ function renderChatContent(text) { if (!text) return text; const re = /(https?:\/\/[^\s<>"')]+|wa\.me\/[0-9]+)/g; const parts = []; let last = 0, m; while ((m = re.exec(text)) !== null) { if (m.index > last) parts.push(text.slice(last, m.index)); let url = m[0]; if (url.startsWith("wa.me/")) url = "https://" + url; const isWa = /wa\.me\//.test(url); parts.push( React.createElement("a", { key: parts.length, href: url, target: "_blank", rel: "noopener noreferrer", className: isWa ? "chat-link chat-link-wa" : "chat-link" }, isWa ? "WhatsApp →" : url) ); last = m.index + m[0].length; } if (last < text.length) parts.push(text.slice(last)); return parts.length ? parts : text; } function ChatBot({ t, lang, onBook }) { const c = t.chat; const botLang = (["es","eu","en","fr"].includes(lang) ? lang : "es"); const [open, setOpen] = useState(false); const [msgs, setMsgs] = useState([{ role: "assistant", content: c.greeting }]); const [input, setInput] = useState(""); const [busy, setBusy] = useState(false); const [unread, setUnread] = useState(false); const bodyRef = useRef(null); const inputRef = useRef(null); // reset greeting when language changes useEffect(() => { const localGreeting = (LOCAL_BOT.txt[botLang] || LOCAL_BOT.txt.es).greeting; setMsgs([{ role: "assistant", content: localGreeting }]); }, [botLang]); useEffect(() => { if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight; }, [msgs, busy, open]); useEffect(() => { if (open) { setUnread(false); setTimeout(() => inputRef.current?.focus(), 250); } }, [open]); // Bot 100% local: clasifica la intención por palabras clave y responde con // textos enlatados (multilingüe). No depende de ningún servicio externo; // funciona igual en local y en producción. const ask = async (text) => { const q = (text ?? input).trim(); if (!q || busy) return; const next = [...msgs, { role: "user", content: q }]; setMsgs(next); setInput(""); setBusy(true); // pequeño retardo para simular que está "pensando" y dar tiempo al render const delay = 350 + Math.min(900, q.length * 12); setTimeout(() => { const reply = localReply(q, botLang); setMsgs(m => [...m, { role: "assistant", content: reply }]); setBusy(false); if (!open) setUnread(true); }, delay); }; return (
Ainara — Zubeltzu Torre {c.title} {c.subtitle}
{msgs.map((m, i) => (
{m.role === "assistant" && Z} {renderChatContent(m.content)}
))} {busy && (
Z
)} {msgs.length <= 1 && !busy && (
{c.quick.map((q, i) => ( ))}
)}
{ e.preventDefault(); ask(); }}> setInput(e.target.value)} placeholder={c.placeholder} aria-label={c.placeholder} />
); } /* ============================================================ COOKIE CONSENT — bottom fixed banner with localStorage ============================================================ */ function CookieConsent({ onOpenCookies, t }) { const { useState: useSt, useEffect: useEf } = React; const [visible, setVisible] = useSt(false); const c = (t && t.cookie) || { label: "Aviso de cookies", text: "Utilizamos cookies propias y de terceros para analizar la navegación. Si continúas navegando, consideramos que aceptas su uso.", policy: "Política de Cookies", accept: "Aceptar" }; useEf(() => { if (!localStorage.getItem("zub-cookies")) { setTimeout(() => setVisible(true), 1200); } }, []); function accept() { localStorage.setItem("zub-cookies", "accepted"); setVisible(false); } if (!visible) return null; return (

{c.text}{" "}

); } /* ============================================================ LEGAL MODAL — full-screen overlay for legal pages ============================================================ */ const LEGAL_CONTENT = { "aviso-legal": { title: "Aviso Legal", body: `

En cumplimiento de lo estipulado en la Ley de Servicios de la Sociedad de la Información y el Comercio Electrónico, ZUBELTZU TORRE E.Z, como titular de la presente página web y por tanto prestador de Servicios de la Sociedad de la Información, pone a disposición de los usuarios de la misma la siguiente información:

Razón social y dirección

ZUBELTZU TORRE E.Z
Barrio Mardari 20829 Itziar (Deba) GUIPUZCOA.
CIF: J20960670

` }, "privacidad": { title: "Política de Privacidad", body: `

Los términos de uso y política de privacidad

De conformidad con lo establecido en la normativa vigente en Protección de Datos de Carácter Personal, le informamos que sus datos serán incorporados al sistema de tratamiento titularidad de ZUBELTZU TORRE E.Z. con CIF J200960670 y domicilio social sito en Zubeltzu torre, barrio Mardari 20829 Itziar (Deba) GUIPÚZCOA con la finalidad de atender sus consultas. En cumplimiento con la normativa vigente, ZUBELTZU TORRE E.Z informa que los datos serán conservados durante el plazo estrictamente necesario para cumplir con los preceptos mencionados con anterioridad.

Mientras no nos comunique lo contrario, entenderemos que sus datos no han sido modificados, que usted se compromete a notificarnos cualquier variación y que tenemos su consentimiento para utilizarlos para las finalidades mencionadas.

ZUBELTZU TORRE E.Z. informa que procederá a tratar los datos de manera lícita, leal, transparente, adecuada, pertinente, limitada, exacta y actualizada. Es por ello que ZUBELTZU TORRE E.Z. se compromete a adoptar todas las medidas razonables para que estos se supriman o rectifiquen sin dilación cuando sean inexactos.

Con envío del formulario de recogida de datos usted acepta la política de privacidad de ZUBELTZU TORRE E.Z.

De acuerdo con los derechos que le confiere la normativa vigente en protección de datos podrá ejercer los derechos de acceso, rectificación, limitación de tratamiento, supresión, portabilidad y oposición al tratamiento de sus datos de carácter personal así como del consentimiento prestado para el tratamiento de los mismos, dirigiendo su petición a la dirección postal indicada más arriba o al correo electrónico zubeltzutorre2008@gmail.com. El interesado tendrá derecho a presentar una reclamación ante la Agencia Española de Protección de Datos, especialmente cuando no haya obtenido satisfacción en el ejercicio de sus derechos, bien a través de la sede electrónica de su portal web (aepd.es), o bien mediante escrito dirigido a su dirección postal (C/Jorge Juan, 6, 28001-Madrid).

` }, "cookies": { title: "Política de Cookies", body: `

¿Qué son las cookies?

Las cookies son pequeños archivos que normalmente contienen letras y números y que se descargan en un dispositivo cada vez que los usuarios acceden a un sitio web. Se utilizan de forma extensiva con el objetivo de conseguir que los sitios web funcionen con mayor eficacia. Las cookies se crean cada vez que el explorador de un usuario carga un sitio web determinado. El sitio web envía la información al explorador, el cual crea un archivo de texto. Cada vez que el usuario vuelve a visitar el mismo sitio web, el explorador recupera el archivo y lo envía al servidor del sitio web. Las cookies tienen diversas funciones entre las que se incluyen, entre otras, recoger información sobre las preferencias del usuario y recordarla, utilizar el carro de compra y, en general, facilitar el uso del sitio web al usuario.

Utilizamos las cookies para llevar a cabo determinadas funciones, entre ellas:

A continuación le detallamos los tipos de cookies:

Tipo 1: Cookies estrictamente necesarias

Estas cookies son necesarias para que los usuarios puedan explorar el sitio web y utilizar funciones como el acceso a una zona segura sin necesidad de iniciar sesión repetidas veces y recordar las acciones que se han efectuado en cada página durante la sesión.

IMPORTANTE: Sin esas cookies no podemos garantizar el rendimiento del sitio web o la seguridad en línea durante las sesiones.

Tipo 2: Cookies de rendimiento

Utilizamos cookies de rendimiento, específicamente las de Google Analytics, con el fin de recopilar información sobre el uso de nuestro sitio web por parte de los usuarios. Entre los datos recopilados se encuentran los clics del sitio web, el número de páginas web visitadas, la duración de cada sesión y los mensajes de error, datos que sirven para mejorar el sitio web y ofrecer a los usuarios una experiencia mejor.

También es probable que recopilemos información periódicamente acerca de cómo utilizan los usuarios el sitio web mediante otras cookies de rendimiento similares a las de Google Analytics. Aunque la información recopilada mediante el uso de estas cookies puede incluir la dirección IP del usuario y la información de explorador y dominio, esos datos se recopilan y analizan de forma anónima y conjunta.

Para Google Adwords: esta página web también utiliza cookies para el seguimiento de conversiones en Google Adwords. La cookie que se añade al equipo de un usuario cuando este ve un anuncio o hace clic en él caduca a los 30 días. Esta medida y el hecho de que Google utiliza servidores distintos para el seguimiento de conversiones y para los resultados de las búsquedas, protege la privacidad del usuario. El proveedor de dichas cookies es Google.

Los usuarios que no deseen participar en actividades de seguimiento simplemente pueden no aceptar esta cookie bloqueando las cookies en las preferencias de usuario del navegador de Internet.

Tipo 3: Cookies de funcionalidad

Las cookies de funcionalidad nos permiten recordar las elecciones que realizan los usuarios en el sitio web, como el nombre de usuario, el idioma y las preferencias. Esta información nos permite proporcionar funciones más personalizadas a nuestros usuarios cuando exploran el sitio web.

Tipo 4: Cookies publicitarias y personalizadas

Utilizamos estas cookies en nuestro sitio web para ofrecer anuncios que le resulten de interés. Recopilan y almacenan información acerca de las actividades de un usuario en el sitio web como las páginas exploradas, los enlaces y los artículos que seleccionan los usuarios, los hábitos de navegación, etc., y rastrean los movimientos de los usuarios en los distintos sitios web para que podamos limitar el número o evitar la repetición de los anuncios del sitio web y evaluar la efectividad de las campañas publicitarias y, así, mejorar su experiencia.

Además, algunas de estas cookies se pueden alojar en el ordenador y pueden ser utilizadas por terceros con fines publicitarios previa autorización. Ocurre cuando el sitio web que se visita muestra contenido de un sitio web de terceros o se hace clic en el botón "compartir" del sitio web y el ordenador recibe las cookies de terceros. Entre los terceros se pueden incluir los proveedores de redes sociales como Facebook y YouTube, los cuales proporcionan servicios a los usuarios y a nosotros. Estas cookies permiten que terceros puedan recopilar y almacenar información como las preferencias de usuario y las páginas visitadas con fines publicitarios o para mejorar servicios. Recuerde que no podemos controlar las cookies de terceros y que, por consiguiente, no nos responsabilizamos en ese sentido. Visite los sitios web de terceros para consultar sus respectivas políticas sobre cookies: LinkedIn, YouTube, Twitter, Google+.

Para Google Remarketing: este sitio web utiliza la herramienta de remarketing de Google Adwords. Esta herramienta permite publicar en la Red de contenido de Google anuncios basados en visitas anteriores de usuarios a nuestra web. Para conseguirlo, algunas de las páginas de nuestro sitio web incluyen un código o etiqueta de remarketing que configuran las cookies para determinar el tipo de anuncio que se mostrará al usuario según la navegación que haya realizado por el sitio web. Las listas de remarketing se guardan en una base de datos de los servidores de Google, donde se almacenan todos los ID de cookie asociados a cada lista o categoría de interés. Respecto a las cookies de remarketing, el usuario puede inhabilitar el uso de cookies de Google, visitando la página de inhabilitación de publicidad de Google. Dichas cookies son proporcionadas por Google.

Cómo evitar el uso de cookies

Puede evitar el uso de cookies modificando la configuración del explorador. Si acepta el uso de cookies pero desea desactivarlo posteriormente, puede eliminar las cookies establecidas y modificar la configuración de su explorador para bloquearlas en un futuro. Si desea obtener más información acerca del control de cookies, visite:

Estas páginas web ofrecen información para configurar la mayoría de los exploradores.

Cómo desactivar las cookies anónimas de Google Analytics: puede instalar el "plug-in" de Google Analytics en el explorador para evitar que el sitio web envíe información de sus visitas a Google Analytics. Para obtener más información, visite: www.google.com/analytics/learn/privacy

IMPORTANTE: Las cookies se utilizan con el objetivo de mejorar o habilitar la usabilidad y los procesos de nuestro sitio web. El hecho de deshabilitar o eliminar cookies puede impedir que los usuarios puedan utilizar las funciones del sitio web o puede hacer que este funcione en el explorador de un modo distinto al diseñado inicialmente.

Política de uso; cambios en la política sobre cookies

Los términos de esta Política sobre cookies regirán el uso de cookies y el de cualquier información recopilada por el sitio, excepto cuando se trate de cookies de terceros. En la medida que lo permita la legislación aplicable de su jurisdicción, Zubeltzu Torre se reserva el derecho de modificar esta Política sobre cookies en cualquier momento y por ello le aconsejamos que visite el sitio web regularmente para comprobar las actualizaciones. Las modificaciones de esta Política sobre cookies son efectivas una vez publicadas en el sitio web.

` } }; function LegalModal({ which, onClose }) { const { useEffect: useEf } = React; const content = LEGAL_CONTENT[which]; useEf(() => { document.body.style.overflow = "hidden"; const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = ""; window.removeEventListener("keydown", onKey); }; }, []); if (!content) return null; return (
e.stopPropagation()}>

{content.title}

); } Object.assign(window, { Cursor, ParallaxY, Nav, BookPill, Tilt, Mist, useDragScroll, useActiveSection, ChatBot, CookieConsent, LegalModal });