That runs in the , before this script, and wins. Partial — set
only the keys you're changing. Source of truth for the snippet:
ghl-location-config.html. The values below are the fallback for when
no override is present (local dev, or the snippet gets cleared).
Never put a Private Integration Token here or in the override. Both
ship to every visitor's browser. The inbound-webhook URL is write-only
and safe to expose; an API key is not.
Booking runs through book.html (GHL calendar widget) and needs no key.
============================================================ */
var GHL = Object.assign({
locationId: '3vvpKzglpNUKboHBEgoq',
calendarId: '4t9ijLLKlIJx1kfHGwHU',
leadWebhook: 'https://services.leadconnectorhq.com/hooks/3vvpKzglpNUKboHBEgoq/webhook-trigger/7793e770-9e88-487b-8463-4559e93c36b2',
chatWidgetId: '6a69032aecf1f59fc8882679' // Conversation AI webchat
}, window.QK_CONFIG || {});
/* ---------- GoHighLevel chat widget (Conversation AI) ----------
Enable in GHL: Sites → Chat Widget → configure → copy the widget ID,
then set it via the QK_CONFIG override above — not by editing this file.
Conversation AI is switched on separately under Settings →
Conversation AI and attaches to this same widget.
Loads once, on every page, after the rest of the page is interactive. */
if (GHL.chatWidgetId) {
window.addEventListener('load', function () {
var t = setTimeout(function () {
var s = document.createElement('script');
s.src = 'https://widgets.leadconnectorhq.com/loader.js';
s.setAttribute('data-resources-url', 'https://widgets.leadconnectorhq.com/chat-widget/loader.js');
s.setAttribute('data-widget-id', GHL.chatWidgetId);
s.async = true;
document.body.appendChild(s);
}, 1200);
window.addEventListener('pagehide', function () { clearTimeout(t); });
});
}
var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/* ---------- Current year ---------- */
var yearEl = document.getElementById('year');
if (yearEl) yearEl.textContent = new Date().getFullYear();
/* ---------- Header shadow on scroll ---------- */
var header = document.getElementById('header');
var bookBar = document.getElementById('bookBar');
var ticking = false;
function onScroll() {
var y = window.scrollY || window.pageYOffset;
if (header) header.classList.toggle('header--scrolled', y > 12);
if (bookBar) bookBar.setAttribute('data-visible', y > 400 ? 'true' : 'false');
ticking = false;
}
window.addEventListener('scroll', function () {
if (!ticking) { window.requestAnimationFrame(onScroll); ticking = true; }
}, { passive: true });
onScroll();
/* ---------- Mobile nav ---------- */
var mobileNav = document.getElementById('mobileNav');
var openBtn = document.querySelector('[data-menu-open]');
var closeBtn = document.querySelector('[data-menu-close]');
function setMenu(open) {
if (!mobileNav) return;
mobileNav.setAttribute('data-open', open ? 'true' : 'false');
mobileNav.setAttribute('aria-hidden', open ? 'false' : 'true');
if (openBtn) openBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
document.body.style.overflow = open ? 'hidden' : '';
if (open) {
var first = mobileNav.querySelector('a');
if (first) first.focus();
} else if (openBtn) {
openBtn.focus();
}
}
if (openBtn) openBtn.addEventListener('click', function () { setMenu(true); });
if (closeBtn) closeBtn.addEventListener('click', function () { setMenu(false); });
if (mobileNav) {
mobileNav.addEventListener('click', function (e) {
if (e.target.tagName === 'A') setMenu(false);
});
}
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
if (mobileNav && mobileNav.getAttribute('data-open') === 'true') setMenu(false);
document.querySelectorAll('[data-nav-group][data-open="true"]').forEach(function (g) {
closeGroup(g);
});
});
/* ---------- Desktop dropdown ---------- */
function closeGroup(group) {
group.setAttribute('data-open', 'false');
var btn = group.querySelector('button');
if (btn) btn.setAttribute('aria-expanded', 'false');
}
function openGroup(group) {
group.setAttribute('data-open', 'true');
var btn = group.querySelector('button');
if (btn) btn.setAttribute('aria-expanded', 'true');
}
document.querySelectorAll('[data-nav-group]').forEach(function (group) {
var btn = group.querySelector('button');
var timer;
if (btn) {
btn.addEventListener('click', function (e) {
e.stopPropagation();
var isOpen = group.getAttribute('data-open') === 'true';
document.querySelectorAll('[data-nav-group]').forEach(closeGroup);
if (!isOpen) openGroup(group);
});
}
group.addEventListener('mouseenter', function () {
clearTimeout(timer);
openGroup(group);
});
group.addEventListener('mouseleave', function () {
timer = setTimeout(function () { closeGroup(group); }, 180);
});
group.addEventListener('focusout', function (e) {
if (!group.contains(e.relatedTarget)) closeGroup(group);
});
});
document.addEventListener('click', function (e) {
document.querySelectorAll('[data-nav-group][data-open="true"]').forEach(function (g) {
if (!g.contains(e.target)) closeGroup(g);
});
});
/* ---------- Scroll reveal ----------
The reveal is decoration. It must never be able to hide content for good,
so there are three ways out: the observer, a visibility-change retry (a
tab loaded in the background gets no rAF/IO callbacks until it is looked
at), and a hard timeout backstop. */
var revealables = document.querySelectorAll('.reveal');
function showAll() {
revealables.forEach(function (el) { el.setAttribute('data-shown', 'true'); });
}
if (reduceMotion || !('IntersectionObserver' in window)) {
showAll();
} else {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (entry, i) {
if (!entry.isIntersecting) return;
var el = entry.target;
setTimeout(function () { el.setAttribute('data-shown', 'true'); }, Math.min(i * 45, 220));
io.unobserve(el);
});
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.06 });
revealables.forEach(function (el) { io.observe(el); });
// Background tabs get throttled callbacks — re-check once the page is looked at.
document.addEventListener('visibilitychange', function () {
if (document.visibilityState !== 'visible') return;
revealables.forEach(function (el) {
if (el.getAttribute('data-shown') === 'true') return;
var r = el.getBoundingClientRect();
if (r.top < window.innerHeight && r.bottom > 0) {
el.setAttribute('data-shown', 'true');
io.unobserve(el);
}
});
});
// Last resort: if anything is still hidden after 3s of visible time, show it.
setTimeout(function () {
if (document.visibilityState === 'visible') showAll();
}, 3000);
}
/* ---------- Contact form -> GoHighLevel ---------- */
var form = document.getElementById('contactForm');
if (form) {
var status = document.getElementById('formStatus');
var submitBtn = form.querySelector('button[type="submit"]');
function setInvalid(input, invalid) {
var field = input.closest('.field');
if (field) field.setAttribute('data-invalid', invalid ? 'true' : 'false');
}
function say(state, msg) {
if (!status) return;
status.setAttribute('data-state', state);
status.textContent = msg;
}
form.querySelectorAll('input, textarea').forEach(function (input) {
input.addEventListener('blur', function () {
if (input.hasAttribute('required') || input.value) {
setInvalid(input, !input.checkValidity());
}
});
});
// Attribution: where this visit came from, carried through to the CRM.
function attribution() {
var q = new URLSearchParams(location.search);
var out = {
page: location.pathname,
pageUrl: location.href,
referrer: document.referrer || ''
};
['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid']
.forEach(function (k) { if (q.get(k)) out[k] = q.get(k); });
return out;
}
form.addEventListener('submit', function (e) {
e.preventDefault();
var firstInvalid = null;
form.querySelectorAll('input, textarea').forEach(function (input) {
var ok = input.checkValidity();
setInvalid(input, !ok);
if (!ok && !firstInvalid) firstInvalid = input;
});
if (firstInvalid) { firstInvalid.focus(); return; }
var data = Object.assign({
first_name: (form.name && form.name.value || '').trim().split(/\s+/)[0] || '',
full_name: (form.name && form.name.value || '').trim(),
email: (form.email && form.email.value || '').trim(),
phone: (form.phone && form.phone.value || '').trim(),
treatment_of_interest: (form.interest && form.interest.value) || '',
website_message: (form.message && form.message.value || '').trim(),
source: 'Website — queensandkingswellnesslounge.com',
locationId: GHL.locationId
}, attribution());
// No webhook configured yet: don't pretend it was sent.
if (!GHL.leadWebhook) {
say('warn', 'Message form is not connected yet — please call (954) 663-2092 or use the Book page. ');
var a = document.createElement('a');
a.href = 'tel:+19546632092'; a.textContent = 'Call now';
a.style.textDecoration = 'underline';
if (status) status.appendChild(a);
return;
}
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Sending…'; }
say('pending', 'Sending…');
fetch(GHL.leadWebhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
say('ok', 'Thank you — your message is on its way. We’ll be in touch shortly.');
form.reset();
})
.catch(function () {
say('err', 'Something went wrong sending that. Please call (954) 663-2092 and we’ll take care of you.');
})
.finally(function () {
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; }
});
});
}
})();