setOpen(false)} />
);
}
/* ===================== CLUB ATUKASA (membresía por teléfono) ===================== */
const DELIVERY_DISCOUNT = 100;
function readClub() {
try { return JSON.parse(localStorage.getItem("atukasa_club") || "null"); }
catch (e) { return null; }
}
function saveClub(data) {
localStorage.setItem("atukasa_club", JSON.stringify(data));
window.dispatchEvent(new Event("atukasa:club"));
}
function clearClub() {
localStorage.removeItem("atukasa_club");
window.dispatchEvent(new Event("atukasa:club"));
}
function useClub() {
const [user, setUser] = useState(readClub);
useEffect(() => {
const upd = () => setUser(readClub());
window.addEventListener("atukasa:club", upd);
return () => window.removeEventListener("atukasa:club", upd);
}, []);
return { user };
}
/* alias de compatibilidad */
const useLoyalty = () => { const { user } = useClub(); return { user, login: () => {}, logout: clearClub }; };
function LoyaltyChip({ user, onOpen }) {
if (user) {
return (
Delivery gratis
);
}
return (
Delivery gratis
);
}
function LoyaltyModal({ onClose }) {
const [phone, setPhone] = useState("");
const [checking, setChecking] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
useEffect(() => {
const onKey = (e) => e.key === "Escape" && onClose();
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, []);
const activate = async () => {
if (checking) return;
const digits = phone.replace(/\D/g, "");
if (digits.length < 10) { setError("Escribe el teléfono con el que hiciste tu pedido (10 dígitos)"); return; }
setChecking(true);
setError(null);
try {
const res = await fetch("/atukasa-club.php?phone=" + encodeURIComponent(digits));
const data = await res.json().catch(() => ({}));
if (data.member) {
saveClub({ name: data.name || "Miembro", phone: digits, since: Date.now() });
setSuccess(data.name || "Miembro");
setTimeout(onClose, 2600);
} else {
setError("Ese número aún no está en el Club. Únete marcando la casilla al confirmar tu próximo pedido. 🍣");
}
} catch (err) {
setError("No se pudo verificar ahora. Inténtalo de nuevo.");
} finally {
setChecking(false);
}
};
return (
e.stopPropagation()}>
{success ? (
🎉
¡Hola de nuevo, {success}!
Tu beneficio del Club está activo en este dispositivo. El delivery de RD$ {fmt(DELIVERY_DISCOUNT)} es gratis en tus pedidos.
Beneficio activo en tu carrito
) : (
Club Atukasa
Delivery gratis, siempre.
Únete al Club Atukasa y recibe ofertas exclusivas por WhatsApp + delivery gratis en la zona de cobertura de San Cristóbal.
−RD$ {fmt(DELIVERY_DISCOUNT)}
en cada pedido, aplicado automáticamente
¿Cómo me uno? Marca la casilla "Únete al Club Atukasa" al confirmar tu pedido. Sin formularios, sin contraseñas.
¿Ya eres miembro?
Activa tu beneficio en este dispositivo con tu teléfono:
setPhone(e.target.value)} />
{checking ? "..." : "Activar"}
{error &&
{error}
}
Al unirte aceptas recibir ofertas y novedades de Atukasa Sushi por WhatsApp. Puedes salir del Club cuando quieras respondiendo "BAJA". Beneficio válido en zona de cobertura de San Cristóbal, no acumulable con otros cupones.
)}
);
}
/* ===================== INDICADOR DE SESIÓN (header) ===================== */
function AccountChip({ memberUser, onOpenLoyalty }) {
const read = () => {
try { return JSON.parse(localStorage.getItem("atukasa_customer") || "null"); }
catch (e) { return null; }
};
const [cust, setCust] = useState(read);
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
const upd = () => setCust(read());
window.addEventListener("atukasa:customer", upd);
return () => window.removeEventListener("atukasa:customer", upd);
}, []);
useEffect(() => {
if (!open) return;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
const onKey = (e) => e.key === "Escape" && setOpen(false);
document.addEventListener("mousedown", onDoc);
document.addEventListener("keydown", onKey);
return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); };
}, [open]);
const isClubMember = !!(memberUser && memberUser.name);
/* Sin membresía del Club → ícono genérico (aunque haya datos de autollenado) */
if (!isClubMember) {
return (
);
}
/* Miembro del Club → inicial en círculo rojo + popover con sus datos */
const initial = memberUser.name.trim().charAt(0).toUpperCase();
return (
setOpen(!open)} aria-label={"Cuenta de " + memberUser.name} title={memberUser.name}>
{initial}
{open && (
{memberUser.name}
{cust && cust.phone &&
📞 {cust.phone}
}
{cust && cust.address &&
📍 {cust.address}
}
Club Atukasa · Delivery gratis
{
localStorage.removeItem("atukasa_customer");
clearClub();
setCust(null); setOpen(false);
}}>
Salir del Club en este dispositivo
)}
);
}
Object.assign(window, {
fmt, DishImage, Ph, Stepper, CategoryRail, ProductCard, ProductModal, CartDrawer, CartLine, SiteMenu,
useLoyalty, useClub, saveClub, clearClub, LoyaltyChip, LoyaltyModal,
DELIVERY_DISCOUNT, DELIVERY_FEE, CheckoutPanel, AccountChip,
});
/* ── Cobertura de delivery ──────────────────────────────────────────
Un solo boton. El GPS del telefono se afina solo (como Google Maps)
y la respuesta sale sola. Si hace falta corregir, el pin se queda
FIJO en el centro y el cliente mueve el mapa con el dedo — nadie
tiene que arrastrar un pin diminuto.
El mapa de zonas NO viaja al navegador: de aqui sale un punto y
vuelve un si o un no. La fuente unica es n8n. ---------------------- */
var COB_CENTRO = [18.4125, -70.1024];
var COB_WHATSAPP = "https://wa.me/18097986648";
function cobEstilos() {
if (document.getElementById("atukasa-cobertura-css")) return;
var s = document.createElement("style");
s.id = "atukasa-cobertura-css";
s.textContent = [
".cob{margin:14px 0 4px}",
".cob__wrap{position:relative;width:100%;height:260px;border-radius:12px;overflow:hidden;margin:10px 0;background:#e8e8e8}",
".cob__mapa{position:absolute;inset:0}",
".cob__pin{position:absolute;left:50%;top:50%;transform:translate(-50%,-100%);pointer-events:none;z-index:500;filter:drop-shadow(0 2px 3px rgba(0,0,0,.4))}",
".cob__pulso{position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;background:rgba(30,110,255,.35);pointer-events:none;z-index:499}",
".cob__btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;border:0;border-radius:12px;padding:15px 18px;font:inherit;font-size:16px;font-weight:700;cursor:pointer;background:#e11d2e;color:#fff}",
".cob__btn:disabled{opacity:.6;cursor:default}",
".cob__btn--alt{background:#fff;color:#111;border:1px solid #d5d5d5;margin-top:10px;font-weight:600}",
".cob__btn--mini{width:auto;padding:9px 14px;font-size:14px;font-weight:600;background:#fff;color:#111;border:1px solid #d5d5d5;margin-top:8px}",
".cob__aviso{border-radius:12px;padding:13px 15px;margin-top:10px;line-height:1.45;font-size:14.5px}",
".cob__aviso--si{background:#e9f7ee;color:#12603a}",
".cob__aviso--no{background:#fdecec;color:#8a1c1c}",
".cob__aviso--err{background:#fff5e5;color:#7a4a00}",
".cob__aviso p{margin:6px 0 0;font-size:14px}",
".cob__hint{font-size:13px;opacity:.85;margin-top:8px;line-height:1.4}",
".cob__estado{display:flex;align-items:center;gap:9px;font-size:14.5px;margin-top:10px;font-weight:600}",
".cob__spin{width:16px;height:16px;border:2px solid rgba(150,150,150,.35);border-top-color:#e11d2e;border-radius:50%;animation:cobGira .8s linear infinite}",
"@keyframes cobGira{to{transform:rotate(360deg)}}",
".cob__wa{display:inline-block;margin-top:10px;font-weight:700;color:#128c7e;text-decoration:none}",
".cob__fila{display:flex;gap:8px;margin-top:6px}",
".cob__input{flex:1;min-width:0;border:1px solid #d5d5d5;border-radius:10px;padding:11px 12px;font:inherit;font-size:15px;background:#fff;color:#111}",
".cob__sug{margin-top:8px;border:1px solid #e2e2e2;border-radius:10px;overflow:hidden;background:#fff}",
".cob__sugitem{display:block;width:100%;text-align:left;border:0;border-bottom:1px solid #eee;background:#fff;color:#111;font:inherit;font-size:13.5px;line-height:1.35;padding:11px 12px;cursor:pointer}",
".cob__sugitem:last-child{border-bottom:0}"
].join("");
document.head.appendChild(s);
}
/* El zoom se ajusta a lo bueno que sea el GPS. Centrar a zoom maximo
una lectura de +-800 m deja al cliente perdido en un mapa vacio. */
function cobZoom(ac) {
if (!ac || ac <= 30) return 18;
if (ac <= 80) return 17;
if (ac <= 200) return 16;
if (ac <= 600) return 15;
if (ac <= 2000) return 14;
return 13;
}
/* Distancia aproximada en metros entre dos puntos cercanos. */
function cobCerca(a, b, metros) {
if (!a || !b) return false;
var dLat = (a[0] - b[0]) * 111320;
var dLng = (a[1] - b[1]) * 111320 * Math.cos(a[0] * Math.PI / 180);
return Math.sqrt(dLat * dLat + dLng * dLng) < metros;
}
function cobCargarLeaflet() {
if (window.__cobLeaflet) return window.__cobLeaflet;
window.__cobLeaflet = new Promise(function (resolve, reject) {
if (window.L && window.L.map) { resolve(window.L); return; }
var css = document.createElement("link");
css.rel = "stylesheet";
css.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
css.crossOrigin = "anonymous";
document.head.appendChild(css);
var js = document.createElement("script");
js.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
js.crossOrigin = "anonymous";
js.onload = function () { resolve(window.L); };
js.onerror = function () { reject(new Error("mapa")); };
document.head.appendChild(js);
});
return window.__cobLeaflet;
}
function CoberturaBox(props) {
var valor = props.valor, onResult = props.onResult, onPickup = props.onPickup;
var e = React.useState(valor && valor.pase ? "ok" : "inicio"); var estado = e[0], setEstado = e[1];
var z = React.useState(valor ? valor.zona : null); var zona = z[0], setZona = z[1];
var m = React.useState(null); var msg = m[0], setMsg = m[1];
var pr = React.useState(null); var precision = pr[0], setPrecision = pr[1];
var mm = React.useState(false); var hayMapa = mm[0], setHayMapa = mm[1];
var qq = React.useState(""); var q = qq[0], setQ = qq[1];
var sg = React.useState(null); var sug = sg[0], setSug = sg[1];
var bs = React.useState(false); var buscando = bs[0], setBuscando = bs[1];
var cajaRef = React.useRef(null);
var mapaRef = React.useRef(null);
var vivoRef = React.useRef(true);
var watchRef = React.useRef(null);
var timerRef = React.useRef(null);
var moviendoRef = React.useRef(false);
var debounceRef = React.useRef(null);
var prevRef = React.useRef(null);
var verificadoRef = React.useRef(null);
var vigilanteRef = React.useRef(null);
var circuloRef = React.useRef(null);
React.useEffect(function () {
cobEstilos();
return function () {
vivoRef.current = false;
if (watchRef.current !== null && navigator.geolocation) navigator.geolocation.clearWatch(watchRef.current);
if (timerRef.current) clearTimeout(timerRef.current);
if (debounceRef.current) clearTimeout(debounceRef.current);
if (vigilanteRef.current) clearInterval(vigilanteRef.current);
circuloRef.current = null;
if (mapaRef.current) { try { mapaRef.current.remove(); } catch (err) {} mapaRef.current = null; }
};
}, []);
function consultar(lat, lng) {
setEstado("cargando");
fetch("/atukasa-cobertura.php", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lat: lat, lng: lng })
}).then(function (r) { return r.json(); }).then(function (j) {
if (!vivoRef.current) return;
if (j && j.ok && j.cubierto) {
setZona(j.zona); setMsg(null); setEstado("ok");
onResult && onResult({ cubierto: true, zona: j.zona, pase: j.pase, lat: lat, lng: lng, fee: j.delivery_fee });
} else if (j && j.ok) {
setZona(null); setMsg(null); setEstado("fuera");
onResult && onResult(null);
} else {
setEstado("error");
setMsg(j && j.error === "validador_no_disponible"
? "No pudimos verificar la zona ahora mismo. Espera un momento y toca de nuevo."
: "No pudimos verificar esa ubicación.");
onResult && onResult(null);
}
}).catch(function () {
if (!vivoRef.current) return;
setEstado("error");
setMsg("No pudimos verificar la zona. Revisa tu conexión.");
onResult && onResult(null);
});
}
function quitarCirculo() {
if (circuloRef.current && mapaRef.current) { try { mapaRef.current.removeLayer(circuloRef.current); } catch (e) {} }
circuloRef.current = null;
}
/* Circulo azul = margen de error del GPS. Que el cliente VEA la duda. */
function marcarPrecision(lat, lng, ac) {
var mp = mapaRef.current;
if (!mp || !window.L) return;
if (!ac || ac < 20) { quitarCirculo(); return; }
if (circuloRef.current) { circuloRef.current.setLatLng([lat, lng]); circuloRef.current.setRadius(ac); }
else circuloRef.current = window.L.circle([lat, lng], { radius: ac, color: "#1e6eff", weight: 1, fillColor: "#1e6eff", fillOpacity: 0.12, interactive: false }).addTo(mp);
}
function buscar() {
var t = (q || "").trim();
if (t.length < 3) { setSug([]); return; }
setBuscando(true);
fetch("/atukasa-cobertura.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ buscar: t }) })
.then(function (r) { return r.json(); })
.then(function (j) { if (!vivoRef.current) return; setBuscando(false); setSug((j && j.resultados) ? j.resultados : []); })
.catch(function () { if (!vivoRef.current) return; setBuscando(false); setSug([]); });
}
function irA(lat, lng) {
setSug(null); setHayMapa(true); quitarCirculo();
if (!mapaRef.current) crearMapa(lat, lng, 17); else centrar(lat, lng, 17);
verificadoRef.current = [lat, lng];
prevRef.current = verificadoRef.current;
consultar(lat, lng);
}
function centrar(lat, lng, zoom) {
var mp = mapaRef.current;
if (!mp) return;
moviendoRef.current = true;
mp.setView([lat, lng], zoom || mp.getZoom());
setTimeout(function () { moviendoRef.current = false; }, 450);
}
function crearMapa(lat, lng, zoom) {
return cobCargarLeaflet().then(function (L) {
if (!vivoRef.current || !cajaRef.current || mapaRef.current) return;
var mp = L.map(cajaRef.current, {
zoomControl: true, attributionControl: true,
// Sin animaciones: dentro del panel deslizante no terminan y dejan
// el zoom a medias (setView pedia 17 y el mapa se quedaba en 14).
zoomAnimation: false, fadeAnimation: false, markerZoomAnimation: false, inertia: false
}).setView([lat, lng], zoom || 17);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap" }).addTo(mp);
mapaRef.current = mp;
// Verificacion automatica al mover el mapa. NO dependemos solo del
// evento moveend de Leaflet: dentro del panel deslizante dispara
// movestart pero nunca moveend. Vigilamos el centro cada 700 ms y,
// cuando se queda quieto sobre un punto nuevo, se verifica solo.
var revisar = function () {
if (!vivoRef.current || !mapaRef.current || moviendoRef.current) return;
var c = mapaRef.current.getCenter();
var p = [c.lat, c.lng];
var anterior = prevRef.current;
prevRef.current = p;
if (cobCerca(p, verificadoRef.current, 12)) return; // ya lo preguntamos
if (!cobCerca(p, anterior, 3)) return; // todavia se mueve
verificadoRef.current = p;
setPrecision(null);
quitarCirculo();
consultar(c.lat, c.lng);
};
mp.on("moveend", function () {
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(revisar, 350);
});
vigilanteRef.current = setInterval(revisar, 700);
setTimeout(function () { if (mapaRef.current) mapaRef.current.invalidateSize(); }, 150);
setTimeout(function () { if (mapaRef.current) mapaRef.current.invalidateSize(); }, 600);
});
}
function terminarBusqueda(lat, lng) {
if (watchRef.current !== null && navigator.geolocation) { navigator.geolocation.clearWatch(watchRef.current); watchRef.current = null; }
if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
verificadoRef.current = [lat, lng];
prevRef.current = verificadoRef.current;
consultar(lat, lng);
}
function ubicar() {
if (!navigator.geolocation) {
setEstado("sinpermiso");
setMsg("Este teléfono no nos deja leer la ubicación.");
return;
}
setEstado("ubicando"); setPrecision(null); setHayMapa(true);
var mejor = null;
var arranca = function (lat, lng, ac) {
var z = cobZoom(ac);
if (!mapaRef.current) crearMapa(lat, lng, z).then(function () { marcarPrecision(lat, lng, ac); });
else { centrar(lat, lng, z); marcarPrecision(lat, lng, ac); }
};
watchRef.current = navigator.geolocation.watchPosition(function (pos) {
if (!vivoRef.current) return;
var la = pos.coords.latitude, ln = pos.coords.longitude;
var ac = pos.coords.accuracy || 999;
if (!mejor || ac < mejor.ac) mejor = { la: la, ln: ln, ac: ac };
setPrecision(Math.round(mejor.ac));
arranca(mejor.la, mejor.ln, mejor.ac);
if (mejor.ac <= 25) terminarBusqueda(mejor.la, mejor.ln);
}, function (err) {
if (!vivoRef.current) return;
if (watchRef.current !== null) { navigator.geolocation.clearWatch(watchRef.current); watchRef.current = null; }
if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
setHayMapa(false);
setEstado("sinpermiso");
setMsg(err && err.code === 1
? "No nos diste permiso para ver tu ubicación. Actívalo en el candado de la barra de direcciones, o escríbenos por WhatsApp y lo hacemos contigo."
: "No pudimos leer el GPS. Asegúrate de tener la ubicación encendida, o escríbenos por WhatsApp y lo hacemos contigo.");
}, { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 });
timerRef.current = setTimeout(function () {
if (!vivoRef.current) return;
if (mejor) terminarBusqueda(mejor.la, mejor.ln);
else {
if (watchRef.current !== null) { navigator.geolocation.clearWatch(watchRef.current); watchRef.current = null; }
setHayMapa(false); setEstado("sinpermiso");
setMsg("El GPS está tardando demasiado. Enciende la ubicación del teléfono, o busca tu calle o sector aquí abajo.");
}
}, 22000);
}
var pinSVG = (
);
return (
④ Confirma tu ubicación
{estado === "inicio" && (
Repartimos solo en algunas zonas de San Cristóbal. Toca el botón y tu teléfono
nos dice dónde estás — no tienes que escribir nada.
📍 Ubicarme automáticamente
)}
{estado === "ubicando" &&
}
{pinSVG}
¿No encuentras tu casa en el mapa? Escribe tu calle o tu sector:
Buscar
{buscando &&
Buscando…
}
{sug && sug.length > 0 && (
{sug.map(function (it, i) {
return (
📍 {String(it.nombre || "").split(",").slice(0, 3).join(",")}
);
})}
)}
{sug && sug.length === 0 && !buscando && (
No encontramos ese lugar. Prueba con el nombre de la calle o del sector.
)}
{estado === "ubicando" && (
Buscando tu ubicación{precision ? " · precisión " + precision + " m" : "…"}
)}
{estado === "cargando" && (
Verificando la zona…
)}
{estado === "ok" && (
✅ Sí llegamos hasta ahí.
Zona confirmada: {zona}.
)}
{estado === "fuera" && (
Todavía no repartimos en esa zona.
Puedes recoger tu pedido en el local, en General Cabral #90, sin costo de envío.
🏪 Cambiar a recoger en el local
)}
{(estado === "ok" || estado === "fuera") && hayMapa && (
¿El punto rojo no está sobre tu casa? Mueve el mapa con el dedo hasta
que quede encima. Se verifica solo.
📍 Volver a ubicarme
)}
{(estado === "sinpermiso" || estado === "error") && (
)}
);
}