/* ============================================================ BARESPACE TEMPLATE ENGINE Pours a config object into the page. Powers BOTH live customer sites (window.BARESPACE_CONFIG) and the portal's live preview (config received via postMessage). Don't edit per-customer. ============================================================ */ /* Apply a prebuilt theme: fonts, palette, hero style, corner shape */ function applyTheme(themeId) { const themes = window.BARESPACE_THEMES || []; const t = themes.find((x) => x.id === themeId) || themes[0]; if (!t) return null; let link = document.getElementById("theme-fonts"); if (!link) { link = document.createElement("link"); link.id = "theme-fonts"; link.rel = "stylesheet"; document.head.appendChild(link); } if (link.getAttribute("href") !== t.fontsUrl) link.setAttribute("href", t.fontsUrl); const r = document.documentElement.style; r.setProperty("--serif", t.heading); r.setProperty("--sans", t.body); r.setProperty("--accent", t.accent); r.setProperty("--ink", t.ink); r.setProperty("--warm", t.warm); r.setProperty("--radius", t.radius); /* surface tokens — let a theme go fully dark, default to light */ r.setProperty("--bg", t.bg || "#ffffff"); r.setProperty("--soft", t.soft || `color-mix(in srgb, ${t.accent} 7%, #ffffff)`); r.setProperty("--card", t.card || "#ffffff"); r.setProperty("--muted", t.muted || "#6f6a66"); r.setProperty("--line", t.line || "#ece7e1"); document.body.classList.remove("hero-image", "hero-solid", "hero-light", "caps"); document.body.classList.add("hero-" + (t.hero || "image")); if (t.caps) document.body.classList.add("caps"); return t; } var SVC_LIMIT = 8; // services shown before "View all" collapse /* Prices on the service menu always read "from £X" */ function priceLabel(p) { p = (p == null ? "" : String(p)).trim(); if (!p || /^free$/i.test(p) || /^from\b/i.test(p)) return p; return "from " + p; } /* Load Instagram's embed script once, then (re)process blockquotes */ function loadInstagramEmbed() { if (window.instgrm && window.instgrm.Embeds) { window.instgrm.Embeds.process(); return; } if (!document.getElementById("ig-embed-js")) { const s = document.createElement("script"); s.id = "ig-embed-js"; s.async = true; s.src = "https://www.instagram.com/embed.js"; document.body.appendChild(s); } } /* Placeholder imagery — used whenever the customer hasn't supplied photos, so a site is NEVER shown with empty or broken image areas. Real professional stock. */ var BARESPACE_STOCK = { beauty: [ "https://images.unsplash.com/photo-1560066984-138dadb4c035?w=800&q=80", "https://images.unsplash.com/photo-1522335789203-aabd1fc54bc9?w=800&q=80", "https://images.unsplash.com/photo-1487412947147-5cebf100ffc2?w=800&q=80", "https://images.unsplash.com/photo-1633681926035-ec1ac984418a?w=800&q=80", ], barber: [ "https://images.unsplash.com/photo-1503951914875-452162b0f3f1?w=800&q=80", "https://images.unsplash.com/photo-1599351431202-1e0f0137899a?w=800&q=80", "https://images.unsplash.com/photo-1517832606299-7ae9b720a186?w=800&q=80", "https://images.unsplash.com/photo-1521590832167-7bcbfaa6381f?w=800&q=80", ], }; var BARESPACE_HERO = { beauty: "https://images.unsplash.com/photo-1521590832167-7bcbfaa6381f?w=1600&q=80", barber: "https://images.unsplash.com/photo-1503951914875-452162b0f3f1?w=1600&q=80", }; function isBarber(c) { return c && (c.theme === "bold-barber" || c.schemaType === "BarberShop"); } function fallbackGallery(c) { return isBarber(c) ? BARESPACE_STOCK.barber : BARESPACE_STOCK.beauty; } function fallbackHero(c) { return isBarber(c) ? BARESPACE_HERO.barber : BARESPACE_HERO.beauty; } var STOCK_ONE = BARESPACE_STOCK.beauty[0]; var TEAM_FALLBACK = [ "https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=500&q=80", "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=500&q=80", "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=500&q=80", "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&q=80", ]; function renderBarespace(c, opts) { c = c || {}; opts = opts || {}; const bind = (name) => document.querySelectorAll(`[data-bind="${name}"]`); const setText = (name, value) => bind(name).forEach((el) => (el.textContent = value || "")); /* Theme first; then the salon's brand colour can override the accent */ const theme = applyTheme(c.theme); if (c.brandColor) document.documentElement.style.setProperty("--accent", c.brandColor); /* Text */ setText("businessName", c.businessName); setText("tagline", c.tagline); setText("about", c.about); setText("phone", c.phone); setText("email", c.email); setText("address", c.address); /* Hero background photo — with focal point + zoom */ const hero = document.getElementById("hero"); if (hero) { const heroSrc = c.heroImage || fallbackHero(c); // always a photo behind the hero hero.style.backgroundImage = heroSrc ? `url("${heroSrc}")` : ""; hero.style.backgroundPosition = c.heroPosition || "center"; hero.style.backgroundSize = c.heroZoom && +c.heroZoom !== 100 ? +c.heroZoom + "%" : "cover"; } setText("heroLocation", c.heroLocation || (c.address || "").toUpperCase()); setText("heroHeadline", c.heroHeadline || c.businessName); setText("heroAccent", c.heroAccent || ""); bind("heroRating").forEach((el) => { el.innerHTML = c.googleRating ? `★★★★★ ${c.googleRating} on Google${c.googleReviewCount ? " · " + c.googleReviewCount + " reviews" : ""}` : ""; }); /* Logo */ bind("logo").forEach((el) => { el.innerHTML = c.logoImage ? `${c.businessName || ` : (c.logoText || c.businessName || ""); }); /* Booking buttons */ bind("bookLink").forEach((el) => { if (c.bookingUrl) el.setAttribute("href", c.bookingUrl); }); /* Services */ bind("services").forEach((list) => { const items = c.services || []; list.innerHTML = items.map((s, i) => `
${s.name || ""}${s.desc ? `${s.desc}` : ""} ${priceLabel(s.price)}
`).join(""); list.classList.toggle("collapsed", items.length > SVC_LIMIT); }); bind("svcToggle").forEach((wrap) => { const list = document.querySelector('[data-bind="services"]'); const total = (c.services || []).length; if (total > SVC_LIMIT && list) { wrap.innerHTML = ``; wrap.querySelector("button").onclick = function () { const collapsed = list.classList.toggle("collapsed"); this.textContent = collapsed ? `View all ${total} services` : "Show fewer"; }; } else { wrap.innerHTML = ""; } }); /* Gallery (each photo wrapped for the hover-zoom) */ bind("gallery").forEach((grid) => { const imgs = (c.gallery && c.gallery.length) ? c.gallery : fallbackGallery(c); // never empty grid.innerHTML = imgs .map((url) => `
${c.businessName ||
`) .join(""); }); /* Kinetic marquee band (scrolling outlined type) */ bind("marquee").forEach((box) => { const phrase = c.businessName || "Welcome"; const items = Array.from({ length: 8 }, (_, i) => `${phrase} —`).join(""); box.innerHTML = `
${items}${items}
`; }); /* Meet the team */ bind("team").forEach((grid) => { grid.innerHTML = (c.team || []).map((m, i) => `

${m.name || ""}

${m.role || ""}

`).join(""); }); /* Social links */ bind("socials").forEach((box) => { const links = []; if (c.instagramUrl) links.push(`Instagram`); if (c.facebookUrl) links.push(`Facebook`); if (c.googleBusinessUrl) links.push(`Google`); box.innerHTML = links.join(""); }); /* Reviews stat block */ bind("reviewStat").forEach((el) => { el.innerHTML = c.googleRating ? `${c.googleRating}★★★★★ ${c.googleReviewCount ? c.googleReviewCount + " " : ""}Google reviews` : ""; }); /* Individual review cards (pulled from Google) */ bind("reviewsList").forEach((box) => { box.innerHTML = (c.reviews || []).slice(0, 6).map((r) => `
${"★".repeat(Math.round(r.rating || 5))}

${r.text ? '"' + String(r.text).slice(0, 280) + '"' : ""}

— ${r.author || "Google review"}
`).join(""); }); /* Opening hours */ bind("hours").forEach((box) => { const rows = (c.hours || []).map((h) => `
${h.label || ""}${h.value || ""}
`).join(""); box.innerHTML = rows; box.style.display = (c.hours && c.hours.length) ? "" : "none"; }); /* Google Map (no API key) */ bind("mapFrame").forEach((box) => { if (c.address) { const q = encodeURIComponent(((c.businessName || "") + " " + c.address).trim()); box.innerHTML = ``; box.style.display = ""; } else { box.style.display = "none"; } }); /* Instagram — official blockquote embeds */ bind("instagramPosts").forEach((grid) => { grid.innerHTML = (c.instagramPosts || []).map((u) => `
View on Instagram
` ).join(""); if ((c.instagramPosts || []).length) loadInstagramEmbed(); }); /* Section visibility — never show an empty box */ const showHide = (sectionName, visible) => { const s = bind(sectionName)[0]; if (s) s.style.display = visible ? "" : "none"; }; bind("instagramWidget").forEach((slot) => (slot.innerHTML = c.instagramWidgetEmbed || "")); bind("reviewsWidget").forEach((slot) => (slot.innerHTML = c.googleReviewsWidgetEmbed || "")); showHide("teamSection", !!(c.team && c.team.length)); showHide("instagramSection", !!((c.instagramPosts && c.instagramPosts.length) || c.instagramWidgetEmbed)); const hasReviews = c.googleRating || (c.reviews && c.reviews.length) || c.googleReviewsWidgetEmbed; showHide("reviewsSection", c.showReviews !== false && !!hasReviews); // opt-out respected /* Reveal-on-scroll: in preview, just show everything */ if (opts.preview) document.querySelectorAll(".reveal").forEach((el) => el.classList.add("in")); } /* Marquee speed reacts to scroll velocity (speeds up / reverses as you scroll) */ function setupMarquee() { const tracks = [...document.querySelectorAll(".marquee-track")]; if (!tracks.length) return; let last = window.scrollY, vel = 0; window.addEventListener("scroll", () => { const y = window.scrollY; vel += y - last; last = y; }, { passive: true }); const state = tracks.map((t) => { t.style.animation = "none"; return { t, x: 0, w: (t.scrollWidth / 2) || 1 }; }); (function frame() { vel *= 0.9; state.forEach((s) => { s.x -= 0.6 + vel * 0.6; if (s.x <= -s.w) s.x += s.w; else if (s.x > 0) s.x -= s.w; s.t.style.transform = "translateX(" + s.x + "px)"; }); requestAnimationFrame(frame); })(); } /* A custom cursor that grows over interactive elements (fine-pointer devices only) */ function setupCursor() { const c = document.createElement("div"); c.id = "bs-cursor"; document.body.appendChild(c); document.body.classList.add("has-cursor"); let x = innerWidth / 2, y = innerHeight / 2, cx = x, cy = y; window.addEventListener("mousemove", (e) => { x = e.clientX; y = e.clientY; }, { passive: true }); (function loop() { cx += (x - cx) * 0.2; cy += (y - cy) * 0.2; c.style.transform = "translate(" + cx + "px," + cy + "px) translate(-50%,-50%)"; requestAnimationFrame(loop); })(); const sel = "a,button,.gal,.svc,.svc-toggle"; document.addEventListener("mouseover", (e) => { if (e.target.closest(sel)) c.classList.add("hover"); }); document.addEventListener("mouseout", (e) => { if (e.target.closest(sel)) c.classList.remove("hover"); }); } /* Per-section parallax — headings drift against the scroll */ function setupSectionParallax() { const els = [...document.querySelectorAll(".section-head h2, .section-head .section-eyebrow")]; if (!els.length) return; const upd = () => { const vh = window.innerHeight; els.forEach((el) => { const r = el.getBoundingClientRect(); const off = (r.top + r.height / 2 - vh / 2) / vh; el.style.transform = "translateY(" + (off * -26) + "px)"; }); }; window.addEventListener("scroll", upd, { passive: true }); upd(); } /* Scroll behaviours for the live site */ function setupScrollEffects() { const nav = document.getElementById("nav"); const onScroll = () => { if (nav) nav.classList.toggle("scrolled", window.scrollY > 40); }; window.addEventListener("scroll", onScroll); onScroll(); /* Hero parallax — the photo drifts slower than the scroll (skipped if reduced-motion) */ const hero = document.getElementById("hero"); const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (hero && !reduce) { window.addEventListener("scroll", () => { const y = window.scrollY; if (y < window.innerHeight) hero.style.backgroundPositionY = (50 + y * 0.04) + "%"; }, { passive: true }); } if (!reduce) { setupMarquee(); setupSectionParallax(); } if (new URLSearchParams(location.search).get("reveal") === "all") { document.querySelectorAll(".reveal").forEach((el) => el.classList.add("in")); const h = document.getElementById("hero"); if (h) h.style.minHeight = "600px"; // capture aid return; } if ("IntersectionObserver" in window) { const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }); }, { threshold: 0.12 }); document.querySelectorAll(".reveal").forEach((el) => io.observe(el)); } else { document.querySelectorAll(".reveal").forEach((el) => el.classList.add("in")); } } /* 1. Live site */ document.addEventListener("DOMContentLoaded", () => { renderBarespace(window.BARESPACE_CONFIG); setupScrollEffects(); }); /* 2. Portal live preview (+ click-to-edit mode) */ function parentEdit(p) { try { parent.postMessage(Object.assign({ type: "bs-edit" }, p), "*"); } catch (e) {} } function enableEditMode() { document.body.classList.add("bs-edit"); const wire = (el, onSave, multiline) => { if (!el || el.dataset.wired) return; el.dataset.wired = "1"; el.setAttribute("contenteditable", "true"); el.classList.add("bs-editable"); el.addEventListener("blur", () => onSave(el.textContent.trim())); el.addEventListener("keydown", (ev) => { if (ev.key === "Enter" && !multiline) { ev.preventDefault(); el.blur(); } }); }; ["heroHeadline", "heroAccent", "tagline", "heroLocation", "about", "phone", "email", "address"].forEach((f) => { document.querySelectorAll(`[data-bind="${f}"]`).forEach((el) => wire(el, (v) => parentEdit({ field: f, value: v }), f === "about")); }); document.querySelectorAll(".svc").forEach((row) => { const i = +row.dataset.i; wire(row.querySelector(".svc-n"), (v) => parentEdit({ field: "service", index: i, key: "name", value: v })); wire(row.querySelector(".svc-price"), (v) => parentEdit({ field: "service", index: i, key: "price", value: v })); }); document.querySelectorAll(".team-card").forEach((card) => { const i = +card.dataset.i; wire(card.querySelector("h3"), (v) => parentEdit({ field: "team", index: i, key: "name", value: v })); wire(card.querySelector("p"), (v) => parentEdit({ field: "team", index: i, key: "role", value: v })); }); } window.addEventListener("message", (e) => { if (e.data && e.data.type === "barespace-config") { renderBarespace(e.data.config, { preview: true }); if (e.data.edit) enableEditMode(); } });