// Section components for Zubeltzu Torre
const { useState: useState2, useEffect: useEffect2, useRef: useRef2, useMemo: useMemo2 } = React;
// Google-style profile avatars for reviews
const REVIEW_AVATARS = ["#2d5a44", "#9c5a32", "#3a5566"];
function initialsOf(name) {
return (name || "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0]).join("").replace(/\./g, "").toUpperCase();
}
// Pone en negrita la palabra "Zumaia" (con sufijo, p. ej. "Zumaiatik" en
// euskera) dentro de un texto plano de i18n.
function boldZumaia(text) {
const parts = String(text || "").split(/(\bZumaia\w*)/);
return parts.map((p, i) => /^Zumaia/.test(p) ? {p} : p);
}
/* ============================================================
HERO
============================================================ */
function Hero({ t, variant, mist, goTo, onBook }) {
const [idx, setIdx] = useState2(0);
const imgs = window.SITE.heroImages;
useEffect2(() => {
const id = setInterval(() => setIdx((i) => (i + 1) % imgs.length), 6000);
return () => clearInterval(id);
}, []);
useEffect2(() => {imgs.forEach((src) => {const i = new Image();i.src = src;});}, []);
return (
{imgs.map((src, i) =>
)}
{mist &&
}
{t.hero.since}
{window.SITE.contact.googleScore}/5 · Google
{t.hero.title_a}{t.hero.title_em}
{boldZumaia(t.hero.lede)}
{t.check_avail}→
document.getElementById("lodging")?.scrollIntoView({ behavior: "smooth" })}>
{t.cta.rooms}
★
{window.SITE.contact.googleScore} {t.hero.trust1}
16 {t.hero.trust2}
{t.hero.trust3}
{imgs.map((_, i) =>
setIdx(i)}
data-cursor="hover" aria-label={`slide ${i + 1}`} />
)}
{t.hero.scroll}
);
}
/* ============================================================
TICKER — small marquee under hero
============================================================ */
function Ticker({ t }) {
const items = [...t.ticker, ...t.ticker];
return (
{items.map((s, i) =>
{s}
)}
);
}
/* ============================================================
INTRO — "El Caserío" home block
============================================================ */
function Intro({ t, goTo }) {
return (
{t.intro.eyebrow}
{t.intro.h2}
{t.intro.body}
{[t.intro.feat1, t.intro.feat2, t.intro.feat3].map((f, i) =>
{f.split("\n").map((l, j) => {l}{j === 0 && } )}
)}
Desde 2008 · Itziar, Deba
);
}
/* ============================================================
HOST — Ainara, la anfitriona (va justo debajo de "El Caserío")
============================================================ */
function HostSection({ t }) {
const h = t.host;
if (!h) return null;
return (
{h.badge}
{h.eyebrow}
{h.h2_a} {h.h2_em}
{h.body1}
{h.body2}
{h.facts.map((f, i) =>
{f}
)}
);
}
/* ============================================================
HOME ALOJAMIENTO teaser — 3 cards
============================================================ */
function LodgingTeaser({ t, goTo, onBook }) {
return (
{t.aloj.eyebrow}
{t.aloj.head_a}{t.aloj.head_em}
{t.aloj.head_sub}
{t.aloj.cards.map((c, i) =>
document.getElementById("lodging")?.scrollIntoView({ behavior: "smooth" })} data-cursor="hover">
)}
{t.check_avail}→
);
}
/* ============================================================
ALOJAMIENTO page
============================================================ */
/* Iconos de los servicios (mismo orden que t.aloj.amenities en i18n):
0 habitaciones · 1 apartamentos · 2 personas · 3 desayuno · 4 wifi · 5 jardín · 6 perros */
const ALOJ_AMENITY_ICONS = [
/* cama */
,
/* edificio / apartamento */
,
/* personas */
,
/* desayuno (taza) */
,
/* wifi */
,
/* jardín (hoja) */
,
/* perro (huella) */
];
/* Categoría de la galería que abre cada tarjeta (mismo orden que t.aloj.cards):
0 habitaciones · 1 apartamentos · 2 casa completa → estancias comunes */
const ALOJ_CARD_FILTERS = ["habitaciones", "apartamentos", "estancias"];
function Lodging({ t, goTo, onBook }) {
return (
{t.aloj.eyebrow}
{t.aloj.page_title_a}{t.aloj.page_title_em}
{t.aloj.page_intro}
{t.check_avail}→
{t.aloj.cards.map((c, i) =>
goTo("galeria", ALOJ_CARD_FILTERS[i])}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); goTo("galeria", ALOJ_CARD_FILTERS[i]); } }}>
{c.num}
{c.title}
{c.body}
{t.gallery.see_photos || "Ver fotos"} →
)}
{t.aloj.body_text}
{t.aloj.amenities.map((a, i) =>
{ALOJ_AMENITY_ICONS[i % ALOJ_AMENITY_ICONS.length]}
{a}
)}
{t.check_avail}→
);
}
/* ============================================================
ENTORNO page
============================================================ */
/* Kilómetros desde el caserío para cada actividad de t.env.acts
(mismo orden en los 4 idiomas) */
const ENV_ACT_KMS = [6, 6, 2, 6, 6, 6, 6, 8, 10, 4, 6, 6, 5];
function Environment({ t, goTo, onBook }) {
// Búsqueda de experiencias en los alrededores del caserío (Itziar · Deba · Gipuzkoa)
const PLACE_QUERIES = [
"surf Zarautz escuela",
"Flysch Zumaia geoparque",
"asadores sidrerías Getaria Deba",
"senderismo Izarraitz rutas",
"pueblos marineros Getaria Mutriku Deba",
"San Sebastián Bilbao Guggenheim"];
const MAPS_LINKS = [
"https://www.google.com/maps/search/?api=1&query=Zarautz+Gipuzkoa",
"https://www.google.com/maps/search/?api=1&query=Flysch+Itzurun+Zumaia+Geoparque+UNESCO",
"https://www.google.com/maps/search/?api=1&query=Getaria+Gipuzkoa+restaurantes",
"https://www.google.com/maps/search/?api=1&query=Monte+Izarraitz+senderismo+Gipuzkoa",
"https://www.google.com/maps/search/?api=1&query=Deba+pueblo+Gipuzkoa",
"https://www.google.com/maps/search/?api=1&query=San+Sebastian+Donostia+Gipuzkoa"];
const ACT_QUERIES = [
"surf escuela playa",
"senderismo rutas",
"parapente vuelo",
"alquiler bicicletas BTT",
"hípica centro ecuestre",
"sidrería restaurante",
"Camino de Santiago del Norte",
"pesca río parque natural"];
const searchNearby = (q) => {
window.open(
"https://www.google.com/maps/search/" + encodeURIComponent(q + " cerca de Itziar, Deba, Gipuzkoa"),
"_blank", "noopener"
);
};
return (
{t.env.eyebrow}
{t.env.title_a}{t.env.title_em}
{t.env.hero_sub}
{t.env.dist.map(([n, l], i) =>
{n}
{l}
{i < t.env.dist.length - 1 && }
)}
{t.env.bidegorri && }
{t.env.natura_label}
{t.env.natura_h2_a}{t.env.natura_h2_em}
{t.env.natura_p1}
{t.env.natura_p2}
{t.env.natura_list.map((s, i) => ✓ {s} )}
{t.env.acts_eyebrow}
{t.env.acts_h3_a} {t.env.acts_h3_em}
{t.env.acts_lede}
{t.env.acts.map((a, i) => (
searchNearby(a.q || a.title)}>
{ENV_ACT_ICONS[i % ENV_ACT_ICONS.length]}
{a.title}
{ENV_ACT_KMS[i] || "—"} km
→
))}
{t.env.farm && (
{t.env.farm.eyebrow}
{t.env.farm.title_a} {t.env.farm.title_em}
{t.env.farm.lede}
{t.env.farm.cats.map((c, i) => (
{c.title}
{c.items.map((it, j) => (
goTo("galeria")}>
{it}
→
))}
))}
goTo("galeria")}>
{t.env.farm.see_in_gallery}→
)}
);
}
/* ============================================================
ENV CTA — closing call-to-action (rendered after the page footer)
============================================================ */
function EnvCTA({ t, onBook }) {
return (
{t.env.cta_eyebrow}
{t.env.cta_h2_a}{t.env.cta_h2_em}
{t.book_now}→
);
}
/* ============================================================
BIDEGORRI — carril bici Zubeltzu → Zumaia (7 km)
============================================================ */
/* Ciclista visto desde arriba (cenital), orientado hacia la derecha.
Para los que van hacia la izquierda se voltea con scaleX(-1) en CSS. */
function BidegorriRiderFig() {
return (
{/* ruedas (vistas en planta: elipses finas en el eje de avance) */}
{/* cuadro */}
{/* manillar */}
{/* brazos hacia el manillar */}
{/* piernas hacia atrás */}
{/* cabeza/torso */}
);
}
function BidegorriSection({ b }) {
const routeUrl = "https://www.google.com/maps/dir/?api=1&origin=43.26638,-2.30071&destination=43.2937,-2.2549&travelmode=bicycling";
// Ciclistas que cruzan el carril de vez en cuando (carril, sentido, velocidad y retardo distintos)
const riders = [
{ dir: "r", top: "20%", dur: "16s", delay: "0s" },
{ dir: "l", top: "80%", dur: "18s", delay: "2.5s" },
{ dir: "r", top: "78%", dur: "15s", delay: "5s" },
{ dir: "l", top: "22%", dur: "19s", delay: "7.5s" },
{ dir: "r", top: "79%", dur: "17s", delay: "10s" },
{ dir: "l", top: "21%", dur: "16s", delay: "12.5s" }
];
return (
{/* El propio carril: línea central discontinua + ciclistas cenitales */}
{riders.map((r, i) => (
))}
{/* Oculta los ciclistas en la zona derecha (a la altura del indicador) */}
{/* Texto, pintado sobre el camino */}
{b.eyebrow}
{b.title_a}{b.title_em}
{b.body}
{b.cta} →
{/* Indicador grande a la derecha, como una señal del carril */}
{b.km}{b.km_label}
{b.route}
);
}
/* ============================================================
ENV MAP — interactive real map (Leaflet)
============================================================ */
function EnvMap({ t }) {
const gimg = (q) => "https://www.google.com/search?tbm=isch&q=" + encodeURIComponent(q);
const points = [
{ id: "here", num: "00", title: "Zubeltzu Torre", dist: "Itziar · Deba", lat: 43.26638, lng: -2.30071, img: window.SITE.envImages.main, here: true, photos: gimg("Agroturismo Zubeltzu Torre Itziar caserío") },
{ id: "izarraitz", num: "01", title: "Izarraitz", dist: "5 min", lat: 43.2240, lng: -2.2940, img: window.SITE.envGridImages[1], photos: gimg("Monte Izarraitz Gipuzkoa") },
{ id: "deba", num: "02", title: "Deba · Puerto", dist: "20 min", lat: 43.2920, lng: -2.3531, img: window.SITE.envGridImages[0], photos: gimg("Deba Gipuzkoa puerto playa") },
{ id: "zumaia", num: "03", title: "Zumaia · Flysch", dist: "5 min", lat: 43.2937, lng: -2.2549, img: window.SITE.envGridImages[3], photos: gimg("Zumaia Flysch Itzurun acantilados") },
{ id: "zarautz", num: "04", title: "Zarautz · Surf", dist: "20 min", lat: 43.2855, lng: -2.1693, img: window.SITE.envGridImages[4], photos: gimg("Playa de Zarautz surf") },
{ id: "donosti", num: "05", title: "Donostia", dist: "20 min", lat: 43.3183, lng: -1.9812, img: window.SITE.envGridImages[5], photos: gimg("Donostia San Sebastián La Concha") },
{ id: "bilbao", num: "06", title: "Bilbao · Guggenheim", dist: "50 min", lat: 43.2680, lng: -2.9350, img: window.SITE.envGridImages[2], photos: gimg("Bilbao Guggenheim") }];
const [active, setActive] = useState2("here");
const mapEl = useRef2(null);
const mapRef = useRef2(null);
const markersRef = useRef2({});
const here = useMemo2(() => points.find((p) => p.id === active), [active]);
// initialize map once Leaflet is available
useEffect2(() => {
if (!mapEl.current || mapRef.current || !window.L) return;
const L = window.L;
const map = L.map(mapEl.current, {
zoomControl: false,
attributionControl: false,
scrollWheelZoom: false,
dragging: true,
doubleClickZoom: true
}).setView([43.2664, -2.3007], 11);
L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", {
maxZoom: 18,
attribution: "Esri"
}).addTo(map);
// Etiquetas de lugares. Antes eran las de Carto (dark_only_labels), que
// ahora exigen API key y pintan "API KEY REQUIRED" sobre el mapa; las de
// Esri son del mismo proveedor que el satélite y no piden clave.
L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}", {
maxZoom: 18,
opacity: 0.95
}).addTo(map);
// markers
points.forEach((p) => {
const isHere = !!p.here;
const html = `${p.title} `;
const icon = L.divIcon({
className: "env-marker " + (isHere ? "is-here" : ""),
html,
iconSize: [14, 14],
iconAnchor: [7, 7]
});
const m = L.marker([p.lat, p.lng], { icon, riseOnHover: true }).addTo(map);
m.on("click", () => setActive(p.id));
m.on("mouseover", () => setActive(p.id));
markersRef.current[p.id] = m;
});
mapRef.current = map;
// ensure tiles load correctly after layout settles
setTimeout(() => map.invalidateSize(), 200);
return () => {map.remove();mapRef.current = null;markersRef.current = {};};
}, []);
// fly to active marker when changed
useEffect2(() => {
const map = mapRef.current;
if (!map) return;
const p = points.find((x) => x.id === active);
if (!p) return;
map.flyTo([p.lat, p.lng], p.id === "here" ? 12 : 11, { duration: 0.9 });
// update marker classes
Object.entries(markersRef.current).forEach(([id, m]) => {
const el = m.getElement?.();
if (el) el.classList.toggle("is-active", id === active);
});
}, [active]);
return (
{points.map((p) =>
)}
{here?.dist}
{here?.title}
{here?.photos &&
▣ {t.env.see_photos} →
}
{/* Ruta en Google Maps: desde la casa (Zubeltzu Torre) hasta el lugar activo.
Si el lugar activo es la propia casa, la ruta parte de tu ubicación. */}
⊙ {t.env.how_to_get} →
{points.map((p) =>
setActive(p.id)}
onClick={() => setActive(p.id)}
data-cursor="hover">
{p.num}
{p.title}
{p.dist}
)}
);
}
/* ============================================================
ACTIVIDADES page
============================================================ */
const ACT_ICONS = [
/* 0 surf */
,
/* 1 senderismo */
,
/* 2 parapente */
,
/* 3 bicicleta */
,
/* 4 hípica */
,
/* 5 gastronomía */
,
/* 6 camino de santiago */
,
/* 7 pesca */
];
const ENV_ACT_ICONS = [
/* 0 paseos a caballo */ ACT_ICONS[4],
/* 1 kayak / piraguas */
,
/* 2 camino de santiago */ ACT_ICONS[6],
/* 3 playa */
,
/* 4 flysch */
,
/* 5 sidrería */ ACT_ICONS[5],
/* 6 cuevas */
,
/* 7 paintball */
,
/* 8 capea */
,
/* 9 frontón */
,
/* 10 piscina */
,
/* 11 polideportivo */
,
/* 12 campo de fútbol */
];
function Activities({ t, goTo, onBook }) {
const ACT_QUERIES = [
"surf escuela playa",
"senderismo rutas",
"parapente vuelo",
"alquiler bicicletas BTT",
"hípica centro ecuestre",
"sidrería restaurante",
"Camino de Santiago del Norte",
"pesca río parque natural"];
const searchNearby = (i) => {
const q = (ACT_QUERIES[i] || t.activities.cards[i].title) + " cerca de Itziar, Deba, Gipuzkoa";
window.open("https://www.google.com/maps/search/" + encodeURIComponent(q), "_blank", "noopener");
};
return (
{t.activities.page_label}
{t.activities.title_a}{t.activities.title_em}
{t.activities.lede}
{t.activities.cards.map((c, i) =>
searchNearby(i)} data-cursor="hover" role="link" tabIndex={0}
onKeyDown={(e) => {if (e.key === "Enter" || e.key === " ") {e.preventDefault();searchNearby(i);}}}>
{ACT_ICONS[i]}
{String(i + 1).padStart(2, "0")}
{c.title}
{c.body}
{t.cta.learn} ↗
)}
);
}
/* ============================================================
OPINIONES page
============================================================ */
function Reviews({ t, onBook, compact }) {
return (
{t.reviews.eyebrow}
{t.reviews.title_a}{t.reviews.title_em}
{window.SITE.contact.googleScore}/5,0
★ ★ ★ ★ ★
{window.SITE.contact.googleCount} {t.reviews.count_label}
G {t.reviews.score_label}
{t.reviews.quotes.map((q, i) =>
“
{q.q}
{initialsOf(q.name)}
{q.name}
G {t.reviews.source} · {q.by}
★ ★ ★ ★ ★
)}
);
}
/* ============================================================
CONTACTO page
============================================================ */
function Contact({ t, onBook }) {
return (
{t.contact.eyebrow}
{t.contact.title_a}{t.contact.title_em}
{t.contact.lede}
{t.contact.address_h}
{t.contact.address.map((l, i) => {l} )}
{t.contact.reg_h}
{t.contact.reg}
);
}
/* ============================================================
FOOTER
============================================================ */
function Footer({ t, goTo, page, onLegal }) {
const LEGAL_KEYS = ["aviso-legal", "privacidad", "cookies"];
return (
Agroturismo en el corazón de la costa vasca desde 2008.
{t.nav.lodging}
goTo("home")} data-cursor="hover">{t.aloj.cards[0].title}
goTo("home")} data-cursor="hover">{t.aloj.cards[1].title}
goTo("home")} data-cursor="hover">{t.aloj.cards[2].title}
{t.cta.learn}
goTo("entorno")} data-cursor="hover">{t.nav.env}
goTo("entorno")} data-cursor="hover">{t.nav.activities}
© 2026 Zubeltzu Torre · {t.contact.reg_h} {t.contact.reg}
{t.legal.map((l, i) => (
onLegal && onLegal(LEGAL_KEYS[i])}>
{l}
))}
);
}
/* ============================================================
FARM ACTIVITIES — agricultural life at the farmhouse
============================================================ */
const FARM_ICONS = {
agri: (
),
livestock: (
),
animals: (
)
};
function FarmActivities({ t }) {
const f = t.farm;
return (
{f.eyebrow}
{f.title}
{f.sub}
{f.cats.map((cat) => (
{FARM_ICONS[cat.icon]}
{cat.name}
{cat.items.map((item) => (
·
{item}
))}
))}
);
}
/* ============================================================
GALERÍA — "El carrete": filmstrip + muro editorial + lightbox
============================================================ */
// Forma de cada baldosa del muro para que la rejilla de 3 columnas cierre
// siempre sin huecos. Ciclo base de 6 = 12 celdas = 4 filas completas; las
// sobrantes al final usan combinaciones que también completan sus filas.
const WALL_CYCLE = ["big", "sq", "sq", "tall", "wide", "wide"];
const WALL_TAIL = {
1: ["w3"],
2: ["wide", "sq"],
3: ["sq", "sq", "sqw"], // sqw: 1×1 a 3 col, ancho completo a 2 col
4: ["big", "sq", "sq", "w3"],
5: ["big", "sq", "sq", "wide", "sq"]
};
function wallShape(n, total) {
const full = Math.floor(total / WALL_CYCLE.length) * WALL_CYCLE.length;
if (n < full) return WALL_CYCLE[n % WALL_CYCLE.length];
return WALL_TAIL[total - full][n - full];
}
function Gallery({ t, goTo, onBook, initialFilter }) {
const g = t.gallery;
const imgs = window.SITE.galleryImages;
const shots = useMemo2(() => g.shots.map((s, i) => {
const im = s.ph ? null : imgs[i] || imgs[i % imgs.length];
return {
...s,
i,
src: im ? im.src : null,
pos: im ? im.pos : "50% 50%",
no: String(i + 1).padStart(3, "0")
};
}), [g.shots, imgs]);
const [filter, setFilter] = useState2(initialFilter || "all");
// Si se llega con una categoría concreta (p. ej. desde una tarjeta de
// alojamiento), bajamos directamente al muro ya filtrado.
useEffect2(() => {
if (!initialFilter || initialFilter === "all") return;
const id = setTimeout(() => {
document.getElementById("gal-wall")?.scrollIntoView({ behavior: "smooth", block: "start" });
}, 120);
return () => clearTimeout(id);
}, [initialFilter]);
const [lb, setLb] = useState2(null); // index into shots or null
const reelRef = useRef2(null);
useDragScroll(reelRef);
const filtered = useMemo2(
() => filter === "all" ? shots : shots.filter((s) => s.cat === filter),
[filter, shots]
);
// lightbox keyboard nav
useEffect2(() => {
if (lb === null) return;
document.body.style.overflow = "hidden";
const onKey = (e) => {
if (e.key === "Escape") setLb(null);else
if (e.key === "ArrowRight") setLb((v) => (v + 1) % shots.length);else
if (e.key === "ArrowLeft") setLb((v) => (v - 1 + shots.length) % shots.length);
};
window.addEventListener("keydown", onKey);
return () => {window.removeEventListener("keydown", onKey);document.body.style.overflow = "";};
}, [lb, shots.length]);
const catLabel = (c) => g.filters[c] || c;
const filterKeys = ["all", "casa", "habitaciones", "apartamentos", "estancias", "entorno", "animales"];
return (
{/* HERO — fotografía del caserío + título superpuesto */}
{g.eyebrow}
{g.title_a}{g.title_em}
{g.intro}
{/* INTRO — contador de fotogramas (el título y el intro ya están en el hero) */}
{/* Cifra fija a petición del cliente; las fotos reales son shots.length */}
50
{g.frame}s
{/* FILMSTRIP — el carrete */}
◉ {g.reel_label}
{g.drag} →
{shots.map((s) =>
setLb(s.i)}
style={{ "--pos": s.pos }}>
{s.ph ?
+ {s.ph} {g.farm_drop} :
}
ZUB·{s.no}
{catLabel(s.cat)}
{s.cap}
)}
{/* WALL — muro editorial */}
{g.wall_eyebrow}
{g.wall_title_a} {g.wall_title_em}
{filterKeys.map((k) =>
setFilter(k)}>
{catLabel(k)}
{filter === k && }
)}
{filtered.map((s, n) =>
setLb(s.i)}>
{s.ph ?
+ {s.ph} {g.farm_drop} :
}
ZUB·{s.no}
{s.cap}
+
)}
{filtered.length === 0 &&
{g.empty}
}
{/* LA GRANJA — animales */}
{g.farm_eyebrow}
{g.farm_title_a} {g.farm_title_em}
{g.farm_lede}
{shots.filter((s) => s.cat === "animales").map((s) =>
setLb(s.i)}>
{s.src ?
:
+
{s.ph}
{g.farm_drop}
}
ZUB·{s.no} {s.cap}
)}
{g.farm_note}
{/* CTA */}
{g.cta_h2_a}{g.cta_h2_em}
{t.book_now}→
{/* LIGHTBOX */}
{lb !== null &&
setLb(null)} role="dialog" aria-modal="true">
e.stopPropagation()}>
ZUB·{shots[lb].no}
{String(lb + 1).padStart(2, "0")} / {String(shots.length).padStart(2, "0")}
setLb(null)} aria-label="Cerrar">×
{e.stopPropagation();setLb((v) => (v - 1 + shots.length) % shots.length);}}
aria-label="Anterior">
←
e.stopPropagation()}>
{shots[lb].ph ?
+ {shots[lb].ph} {g.farm_drop}
:
}
{catLabel(shots[lb].cat)}
{shots[lb].cap}
{e.stopPropagation();setLb((v) => (v + 1) % shots.length);}}
aria-label="Siguiente">
→
{g.lb_hint}
}
);
}
Object.assign(window, { Hero, Ticker, Intro, HostSection, LodgingTeaser, Lodging, Environment, BidegorriSection, Activities, Reviews, Contact, Footer, Gallery, FarmActivities });