/* global React, ReactDOM */
const { useState, useEffect, useRef, useCallback } = React;
/* ===================== Custom cursor ===================== */
function CustomCursor() {
const dotRef = useRef(null);
const ringRef = useRef(null);
const target = useRef({ x: -100, y: -100 });
const ring = useRef({ x: -100, y: -100 });
const isDown = useRef(false);
useEffect(() => {
// Only render on devices with a fine pointer
const mq = window.matchMedia('(hover: hover) and (pointer: fine)');
if (!mq.matches) return;
let raf;
function loop() {
const t = target.current;
const r = ring.current;
r.x += (t.x - r.x) * 0.18;
r.y += (t.y - r.y) * 0.18;
if (dotRef.current) dotRef.current.style.transform = `translate3d(${t.x}px, ${t.y}px, 0) translate(-50%, -50%)`;
if (ringRef.current) ringRef.current.style.transform = `translate3d(${r.x}px, ${r.y}px, 0) translate(-50%, -50%) scale(${isDown.current ? 0.85 : 1})`;
raf = requestAnimationFrame(loop);
}
function onMove(e) {
target.current = { x: e.clientX, y: e.clientY };
// dark backgrounds: figure out element under cursor
const el = document.elementFromPoint(e.clientX, e.clientY);
const onDark = !!(el && el.closest && el.closest('.on-dark, [data-dark]'));
ringRef.current && ringRef.current.classList.toggle('is-dark', onDark);
dotRef.current && dotRef.current .classList.toggle('is-dark', onDark);
}
function onOver(e) {
const interactive = e.target.closest && e.target.closest('a, button, [role="button"], input, select, textarea, label, summary');
ringRef.current && ringRef.current.classList.toggle('is-hover', !!interactive);
}
function onOut(e) {
if (!e.relatedTarget) {
ringRef.current && ringRef.current.classList.remove('is-hover');
}
}
function onDown(){ isDown.current = true; }
function onUp() { isDown.current = false; }
window.addEventListener('mousemove', onMove, { passive: true });
document.addEventListener('mouseover', onOver, { passive: true });
document.addEventListener('mouseout', onOut, { passive: true });
window.addEventListener('mousedown', onDown);
window.addEventListener('mouseup', onUp);
raf = requestAnimationFrame(loop);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseover', onOver);
document.removeEventListener('mouseout', onOut);
window.removeEventListener('mousedown', onDown);
window.removeEventListener('mouseup', onUp);
};
}, []);
return (
<>
>
);
}
/* ===================== Media slots (image / video) ===================== */
function ImgSlot({ src, alt, label, badge, className = "", aspect = "aspect-[3/4]" }) {
return (
{ e.currentTarget.style.display = "none"; }} />
{(label || badge) && (
{label && {label}}
{badge && {badge}}
)}
);
}
function VideoSlot({ label, srcs = [], poster, aspect = "aspect-[3/4]", className = "" }) {
return (
{/* Poster image as fallback while there is no real video */}
{poster && (
{ e.currentTarget.style.display = "none"; }} />
)}
{label}
Reel
);
}
/* ===================== Gallery (Resultados Reales) ===================== */
function Gallery({ onNavigate, dark = false }) {
const tiles = (window.siteMedia && window.siteMedia.gallery) || [];
const bg = dark ? "bg-charcoal text-ivory on-dark" : "bg-ivory";
const muteText = dark ? "text-ivory/60" : "text-mute";
const heading = dark ? "text-ivory" : "text-ink";
return (
Resultados reales
Tu mejor versión,
en imagen y movimiento.
Una selección del trabajo del atelier — fotografía editorial y reels capturados en cabina. Lo que ves es real, sin filtros.
Ver más en Instagram
{tiles.map((t, i) => (
t.kind === "video"
?
:
))}
Atelier Vibelami · Castellón / Valencia
);
}
/* ===================== Brand mark ===================== */
/* Returns a real URL for a page id when running in multipage mode
(window.VIBELAMI_LINKS is defined by each page's ); otherwise null. */
function pageHref(id) {
return (typeof window !== "undefined" && window.VIBELAMI_LINKS) ? window.VIBELAMI_LINKS[id] : null;
}
function Logo({ className = "", onClick }) {
const inner = (
VIBELAMI.
);
const href = pageHref("inicio");
if (href) {
return (
{inner}
);
}
return (
);
}
/* ===================== Top utility strip ===================== */
function TopStrip() {
return (
Castellón · Valencia
Agenda 2026 abierta — Citas y formaciones disponibles
Atelier privado · Por cita previa
);
}
/* ===================== Navigation ===================== */
function Nav({ currentPage, onNavigate }) {
const [open, setOpen] = useState(false);
const items = [
{ id: "inicio", label: "Inicio" },
{ id: "servicios", label: "Servicios" },
{ id: "academia", label: "Academia" },
{ id: "promocion", label: "Promoción" },
{ id: "contacto", label: "Contacto" },
];
function go(id) {
setOpen(false);
onNavigate(id);
}
return (
go("inicio")} />
ES / EN
{pageHref("contacto") ? (
Reservar
) : (
)}
{open && (
)}
);
}
/* ===================== Glyphs for "para ti" ===================== */
function Glyph({ kind }) {
const common = { width: 56, height: 56, viewBox: "0 0 56 56", fill: "none", "aria-hidden": true, stroke: "currentColor" };
if (kind === "lift") {
return (
);
}
if (kind === "diamond") {
return (
);
}
return (
);
}
/* ===================== Contact ===================== */
function Contact({ defaultService }) {
const [form, setForm] = useState({ nombre: "", telefono: "", servicio: defaultService || "", mensaje: "", rgpd: false });
const [sent, setSent] = useState(false);
const [touched, setTouched] = useState(false);
const handle = (k) => (e) => setForm({ ...form, [k]: e.target.type === "checkbox" ? e.target.checked : e.target.value });
const required = form.nombre.trim() && form.telefono.trim() && form.servicio && form.rgpd;
function submit(e) {
e.preventDefault();
setTouched(true);
if (!required) return;
setSent(true);
}
const inputBase = "w-full bg-transparent border-0 border-b border-line/80 focus:border-ink py-3 text-[15px] text-ink placeholder:text-mute/70 transition-colors";
const labelBase = "block text-[10px] tracking-widest-2 uppercase text-mute mb-2";
return (
);
}
/* ===================== Footer ===================== */
function Footer({ onNavigate }) {
const cols = [
{ title: "Navegación", links: [
{ label: "Inicio", page: "inicio" },
{ label: "Servicios", page: "servicios" },
{ label: "Academia", page: "academia" },
{ label: "Contacto", page: "contacto" },
]},
{ title: "Servicios", links: [
{ label: "VIBROWS & Powder Brows", page: "servicios" },
{ label: "Micropigmentación Labios", page: "servicios" },
{ label: "Densificación Capilar", page: "servicios" },
{ label: "Correcciones & Remoción", page: "servicios" },
]},
{ title: "Legal", links: [
{ label: "Aviso Legal", href: "#aviso-legal" },
{ label: "Política de Privacidad", href: "#privacidad" },
{ label: "Política de Cookies", href: "#cookies" },
{ label: "RGPD / LOPDGDD", href: "#rgpd" },
]},
];
function clickLink(l) {
if (l.page) onNavigate(l.page);
}
return (
);
}
/* ===================== Chat Widget (premium) ===================== */
function ChatWidget() {
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
const [history, setHistory] = useState([]); // local messages user has typed
const inputRef = useRef(null);
const scrollRef = useRef(null);
const WA = "https://wa.me/34642606395?text=";
useEffect(() => {
if (open) setTimeout(() => inputRef.current && inputRef.current.focus(), 220);
}, [open]);
useEffect(() => {
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [history, open]);
// Esc to close
useEffect(() => {
function onKey(e) { if (e.key === "Escape" && open) setOpen(false); }
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
function send(text) {
const t = (text || "").trim();
if (!t) return;
setHistory((h) => [...h, { who: "me", text: t }]);
const url = WA + encodeURIComponent(t);
// small delay so the user sees their message in the chat before the tab opens
setTimeout(() => window.open(url, "_blank", "noopener,noreferrer"), 220);
}
function onSubmit(e) {
e.preventDefault();
send(input);
setInput("");
}
const chips = [
{ label: "Info Cejas", msg: "¡Hola Vibelami! Me gustaría más información sobre las cejas (VIBROWS / Powder Brows)." },
{ label: "Info Labios", msg: "¡Hola Vibelami! Me gustaría más información sobre la micropigmentación de labios." },
{ label: "Academia", msg: "¡Hola Vibelami! Me interesa la Masterclass. ¿Podríais darme detalles?" },
];
return (
<>
{/* Header */}
V
Vibelami
En línea · Bela o Vivian te responderán pronto
{/* Body */}
{/* Bot intro */}
V
¡Hola! 👋 ¿En qué podemos ayudarte hoy? ¿Buscas información sobre tratamientos o sobre la academia?
Equipo Vibelami
{/* Chips */}
{chips.map((c) => (
))}
{/* User messages */}
{history.map((m, i) => (
{m.text}
Enviado · abriendo WhatsApp
))}
{/* Input */}
Tu mensaje se abrirá en WhatsApp
{!open && (
¡Hablemos!
)}
>
);
}
/* Keep WhatsappFab as a backward-compatible alias */
const WhatsappFab = ChatWidget;
/* Expose globals for the other babel scripts */
Object.assign(window, {
CustomCursor, Logo, TopStrip, Nav, Glyph, Contact, Footer, WhatsappFab,
ChatWidget, ImgSlot, VideoSlot, Gallery
});