/* Prospect Advisory Survey v2 */
(function () {
'use strict';
if (window.__PAS_SURVEY_LOADED__) return;
window.__PAS_SURVEY_LOADED__ = true;
// === DATA ===
// ==========================================================================
// CONFIG
// ==========================================================================
//
// WEBHOOK_URL is the Google Apps Script web app /exec URL. It must be a
// deployment owned by austin.sandmeyer@sendoso.com, bound to the sheet
// "Prospect Advisory Project: Survey Results (sendoso.com)"
// (1ZvLY77wUcMLO_UIJDlkuWYKIrVrJmnMBSg0vkso74Xs), and deployed with
// "Who has access: Anyone".
//
// Not Hannah's original sheet -- that one keeps the Netlify test data.
//
// build/build.py refuses to build while this is still the placeholder,
// unless you pass --allow-placeholder for local testing.
//
// REPORT_WHEN is the single place the report timing is expressed. It is
// written to slot into the sentence "...the experiments we're launching
// ___." Set it to 'on September 15' when the date is real; leave it as
// 'when it publishes' while the date is TBD. Do not hardcode a date
// anywhere else.
// --------------------------------------------------------------------------
var CONFIG = {
WEBHOOK_URL: 'https://script.google.com/macros/s/AKfycbz_yJIuns-HWnQDzkjZeXDLaU8J1uWztHp3tgjsHRho-gDw2HJCsxLztFtdgCeWsx2F/exec',
MOUNT_ID: 'pas-survey-root',
REPORT_WHEN: 'when it publishes',
MIN_SAMPLE: 5,
MAX_OPEN_CHARS: 2000
};
// ==========================================================================
// SURVEY MODEL
// ==========================================================================
//
// DO NOT CHANGE the section `key` values or the option `letter` values.
// They are the data model: they are what gets written to the sheet, what
// the tally endpoint counts, and what any downstream Zap keys on.
// Renaming a letter silently invalidates every response already collected.
//
// Option `label` and `text` are display strings and safe to edit.
// --------------------------------------------------------------------------
var SECTIONS = [
{
key: 'cta',
title: 'THE CTA',
intro: [
"Every outbound playbook ends with an ask. Too few earn it.",
"So we're pressure-testing one of outbound's biggest assumptions: the CTA.",
"Before we run the experiment, we want to hear from the people on the receiving end. What actually earns engagement today?"
],
questions: [
{
id: 'cta_choice',
type: 'single',
text: 'Which ask would be most likely to earn a reply from you?',
options: [
{ letter: 'A', label: 'Learn Something', text: '"I put together a gifting playbook for teams like yours. Want me to send it over?"' },
{ letter: 'B', label: 'Give Feedback', text: '"Can I ask you an honest question? What\'s the biggest reason you haven\'t considered Sendoso?"' },
{ letter: 'C', label: 'Try Something', text: '"We built a free tool that scores your gifting program in under 4 minutes. Curious how you\'d stack up?"' },
{ letter: 'D', label: 'Receive Something', text: '"Saw this and thought of you. Happy to send it your way if you\'re interested."' }
]
},
{
id: 'cta_open',
type: 'open',
text: "Tell us about the last outbound CTA that actually made you stop and engage. What made it work?",
placeholder: 'What was it, and why did it land?'
}
]
},
{
key: 'personalization',
title: 'EARNING ATTENTION',
intro: [
"Anyone can personalize at scale. What we're trying to understand is what actually earns your attention."
],
questions: [
{
id: 'personalization_choice',
type: 'single',
text: 'What earns your attention more than personalization?',
options: [
{ letter: 'A', label: 'Relevance', text: "It speaks directly to a challenge I'm dealing with." },
{ letter: 'B', label: 'Insight', text: "It teaches me something I didn't know." },
{ letter: 'C', label: 'Effort', text: 'Someone clearly invested real time in creating it.' },
{ letter: 'D', label: 'Generosity', text: 'It delivers value before asking me to do anything.' }
]
},
{
id: 'personalization_open',
type: 'open',
text: "What's the most thoughtful piece of outbound you've received in the last year? What made it stand out?",
placeholder: 'What happened, and why did it stick with you?'
}
]
},
{
key: 'events',
title: 'EXPERIENCES',
intro: [
"Everyone is fighting for the same limited calendar space.",
"We're testing a simple question: What type of experience actually earns a \"yes\" from GTM leaders like yourself?"
],
questions: [
{
id: 'events_choice',
type: 'single',
text: 'Which experience would you be most likely to make time for?',
options: [
{ letter: 'A', label: 'Build Something', text: 'A hands-on workshop where I leave with something I can use.' },
{ letter: 'B', label: 'Learn Something', text: "Exclusive research and data I haven't seen anywhere else." },
{ letter: 'C', label: 'Meet Someone', text: 'A small dinner with peers facing similar challenges.' },
{ letter: 'D', label: 'Debate Something', text: "A live discussion where GTM leaders vote on and debate today's biggest outbound assumptions." }
]
},
{
id: 'events_open',
type: 'open',
text: "Tell us about the last vendor event or experience that felt genuinely worth your time. What made it different?",
placeholder: 'Company, format, whatever you remember...'
}
]
},
{
key: 'content',
title: 'CONTENT',
intro: [
"Great content doesn't just get read. It gets shared.",
"We're testing what actually earns a forward inside today's buying committee."
],
questions: [
{
id: 'content_choice',
type: 'single',
text: "When you forward vendor content internally, what's usually the reason?",
options: [
{ letter: 'A', label: null, text: "It supports a decision we're already considering." },
{ letter: 'B', label: null, text: 'It introduces a new idea we should discuss.' },
{ letter: 'C', label: null, text: 'It shows how another company solved a similar problem.' },
{ letter: 'D', label: null, text: 'It gives us something we can immediately put into practice.' }
]
},
{
id: 'content_open',
type: 'open',
text: "What's one report, framework, or resource you'd immediately download if it existed?",
placeholder: "This is your chance to ask for something that doesn't exist yet..."
}
]
}
];
// ==========================================================================
// RESULTS PAGE CARD LABELS
// ==========================================================================
//
// These are short restatements of the choice question in each section.
// They must stay faithful to the question actually asked -- v1 shipped a
// card labelled "What makes outbound feel like someone actually tried?"
// above the tally for "What earns your attention more than
// personalization?", which is a different question.
// --------------------------------------------------------------------------
var RESULT_CARDS = [
{ key: 'cta', question: 'Which ask would earn a reply?' },
{ key: 'personalization', question: 'What earns attention more than personalization?' },
{ key: 'events', question: 'Which experience would you make time for?' },
{ key: 'content', question: 'Why do you forward vendor content internally?' }
];
// ==========================================================================
// STATIC COPY
// ==========================================================================
// Every visitor-facing string that is not part of a question lives here.
// The Webflow page owns no copy at all -- it is a mount container only.
// --------------------------------------------------------------------------
var COPY = {
intro: {
eyebrow: 'From Kris Rudeegraap, CEO of Sendoso',
headline: "OUTBOUND ISN'T DEAD.
IT JUST STOPPED EARNING ATTENTION.",
body: [
"Everyone has an opinion about why outbound isn't working. Very few are asking buyers directly.",
"This quarter, we're testing the assumptions behind today's GTM playbook, starting with the CTA and building our experiments from real executive feedback."
],
commitment: "8 questions. About 8 minutes. Submit your responses to see live peer data, then get the full report and the experiments we're launching " + CONFIG.REPORT_WHEN + ".",
cta: "Let's go, 8 minutes"
},
contact: {
eyebrow: 'Last thing (optional)',
headline: 'WANT TO HEAR HOW THIS TURNS OUT?',
body: "Leave your name and email if you'd like the full summary and a look at the experiments we're launching " + CONFIG.REPORT_WHEN + ". Either way, continue to see how your responses compare with everyone who's participated so far.",
namePlaceholder: 'Name (optional)',
emailPlaceholder: 'Email (optional)',
cta: 'See how you compare'
},
loading: {
eyebrow: 'Submitting',
headline: 'TALLYING YOUR ANSWERS...',
note: "One second while we compare you to everyone else who's answered so far."
},
results: {
eyebrow: "Here's what your peers said",
body: "Here's a live look at how everyone who's responded so far compares. Your selections are highlighted.",
onListTitle: "You're on the list.",
onList: "The full summary and what we're experimenting with lands in your inbox " + CONFIG.REPORT_WHEN + ".",
gateHeading: 'Want the full results ' + CONFIG.REPORT_WHEN + '?',
gateBody: 'Enter your email below to receive the complete summary and a look at the experiments we are launching.',
gateCta: 'Add me',
gateSuccess: "You're on the list.",
gateError: "That didn't save. Try again, or let your Sendoso contact know.",
earlyData: function (n) {
return 'Early days: this is based on ' + n + (n === 1 ? ' response' : ' responses') + ' so far, so treat the percentages as directional.';
},
sampleNote: function (n) {
return 'Based on ' + n + ' responses so far.';
},
saveFailed: "We couldn't save your response, so it isn't counted below and we don't have a record of it. Please let your Sendoso contact know.",
talliesFailed: "Your response was saved. We couldn't load the live peer comparison right now."
},
ribbon: {
almost: 'Almost there',
done: "You're in"
}
};
// The mount container is the scope for this bundle. The script is applied
// to a single Webflow page, and gating on the container rather than on
// location.pathname means a slug change degrades to a console warning
// instead of a blank page.
var mount = document.getElementById(CONFIG.MOUNT_ID);
if (!mount) {
console.warn('[pas-survey] No #' + CONFIG.MOUNT_ID + ' element on this page. Nothing rendered.');
return;
}
if (location.pathname.indexOf('advisory-report-26') === -1) {
console.warn('[pas-survey] Mounted outside the expected path: ' + location.pathname);
}
// === STYLES ===
(function () {
if (document.getElementById('pas-survey-styles')) return;
var s = document.createElement('style');
s.id = 'pas-survey-styles';
s.textContent = `/* ==========================================================================
Prospect Advisory Survey -- styles
Every selector is scoped under #pas-survey-root and every class is
prefixed pas-. This stylesheet is injected into
' + esc(label) + '
' + '' + esc(COPY.intro.body[i]) + '
'; } return '' + esc(COPY.intro.eyebrow) + '
' + '' + esc(COPY.intro.commitment) + '
' + signature() + 'Kris RudeegraapCEO, Sendoso
' + '' + esc(s.intro[i]) + '
'; } var blocks = ''; for (var qi = 0; qi < s.questions.length; qi++) { var q = s.questions[qi]; var num = 'Question ' + (idx + 1) + '.' + (qi + 1); if (q.type === 'single') { var opts = ''; for (var oi = 0; oi < q.options.length; oi++) { var o = q.options[oi]; var inputId = 'pas-' + q.id + '-' + o.letter; var copy = o.label ? '' + esc(o.label) + ': ' + esc(o.text) : esc(o.text); opts += ''; } blocks += ''; } else { var taId = 'pas-' + q.id; blocks += '' + esc(COPY.contact.eyebrow) + '
' + '' + esc(COPY.contact.body) + '
' + '' + '' + '' + '' + '' + '' + '' + esc(COPY.loading.eyebrow) + '
' + '' + esc(COPY.loading.note) + '
' + '' + esc(card.question) + '
' + rows + '' + esc(COPY.results.eyebrow) + '
' + '' + esc(COPY.results.saveFailed) + '
' + signature() + '' + esc(COPY.results.talliesFailed) + '
' + emailBlock() + signature() + '' + esc(COPY.results.earlyData(result.total)) + '
'; } return '' + esc(COPY.results.body) + '
' + early + renderTallyCards() + (result.total >= CONFIG.MIN_SAMPLE ? '' + esc(COPY.results.sampleNote(result.total)) + '
' : '') + emailBlock() + signature() + '' + esc(COPY.results.gateHeading) + '
' + '' + esc(COPY.results.gateBody) + '
' + '' + esc(COPY.results.onListTitle) + '
' + '' + esc(COPY.results.onList) + '
'; } // ========================================================================== // SUBMIT // ========================================================================== function submitAndTally() { step = STEP_RESULTS; result = null; render(); if (!endpointReady()) { result = { tallies: {}, total: 0, saveOk: false, talliesOk: false }; render(); return; } var payload = {}; for (var k in answers) { if (Object.prototype.hasOwnProperty.call(answers, k)) payload[k] = answers[k]; } payload.submissionId = submissionId; payload.submittedAt = new Date().toISOString(); post(payload).then(function (data) { result = { tallies: (data && data.tallies) || {}, total: (data && Number(data.total)) || 0, saveOk: !!(data && data.ok), talliesOk: !!(data && data.tallies) }; render(); }).catch(function () { result = { tallies: {}, total: 0, saveOk: false, talliesOk: false }; render(); }); } function addLateEmail() { var input = document.getElementById('pas-late-email'); var errEl = document.getElementById('pas-gate-error'); var btn = document.querySelector('[data-action="late-email"]'); if (!input || !errEl) return; var value = input.value.trim(); errEl.hidden = true; if (!isEmail(value)) { errEl.textContent = 'That does not look like an email address.'; errEl.hidden = false; return; } if (!endpointReady()) { errEl.textContent = COPY.results.gateError; errEl.hidden = false; return; } if (btn) btn.disabled = true; // A real CORS request, not mode:'no-cors'. v1 used no-cors, which makes // the response opaque -- a silent failure was indistinguishable from a // success, so nothing ever proved this path worked. post({ lateEmailAdd: true, submissionId: submissionId, email: value, name: answers.name || '', submittedAt: new Date().toISOString() }).then(function (data) { if (!(data && data.ok)) throw new Error('not ok'); answers.email = value; var gate = document.getElementById('pas-gate'); if (gate) { gate.innerHTML = onListPanel() + '' + esc(COPY.results.gateSuccess) + '
'; } }).catch(function () { if (btn) btn.disabled = false; errEl.textContent = COPY.results.gateError; errEl.hidden = false; }); } // ========================================================================== // NAVIGATION // ========================================================================== function sectionComplete(idx) { var s = SECTIONS[idx]; for (var i = 0; i < s.questions.length; i++) { var q = s.questions[i]; if (q.type === 'single' && !answers[q.id]) return false; } return true; } function syncNextButton() { var btn = document.querySelector('[data-action="next"]'); if (!btn) return; var idx = Number(btn.getAttribute('data-section')); btn.disabled = !sectionComplete(idx); } function handleSubmitStep() { var nameEl = document.getElementById('pas-name'); var emailEl = document.getElementById('pas-email'); var hpEl = document.getElementById('pas-company-url'); var errEl = document.getElementById('pas-contact-error'); var email = emailEl ? emailEl.value.trim() : ''; if (email && !isEmail(email)) { if (errEl) { errEl.textContent = 'That does not look like an email address. Clear it to continue without one.'; errEl.hidden = false; } if (emailEl) emailEl.focus(); return; } answers.name = nameEl ? nameEl.value.trim() : ''; answers.email = email; answers.hp = hpEl ? hpEl.value.trim() : ''; submitAndTally(); } // ========================================================================== // RENDER + HYDRATE // ========================================================================== function screen() { if (step === 0) return renderIntro(); if (step >= 1 && step <= LAST_SECTION) return renderSection(step - 1); if (step === STEP_CONTACT) return renderContact(); return result ? renderResults() : renderLoading(); } function render() { mount.innerHTML = renderRibbon() + screen(); hydrate(); if (!firstPaint) { try { mount.scrollIntoView({ block: 'start', behavior: 'smooth' }); } catch (e) { window.scrollTo(0, mount.offsetTop || 0); } var heading = document.getElementById('pas-step-heading'); if (heading) heading.focus({ preventScroll: true }); } firstPaint = false; } function hydrate() { // Restore radio state from `answers` rather than interpolating a checked // attribute, and restore textarea content via .value rather than // innerHTML -- no user-supplied string is ever parsed as markup. var radios = mount.querySelectorAll('.pas-option-input'); var i; for (i = 0; i < radios.length; i++) { var radio = radios[i]; radio.checked = answers[radio.getAttribute('data-qid')] === radio.value; syncOptionClass(radio); } var areas = mount.querySelectorAll('.pas-open'); for (i = 0; i < areas.length; i++) { var area = areas[i]; area.value = answers[area.getAttribute('data-qid')] || ''; } var nameEl = document.getElementById('pas-name'); if (nameEl) nameEl.value = answers.name || ''; var emailEl = document.getElementById('pas-email'); if (emailEl) emailEl.value = answers.email || ''; syncNextButton(); } function syncOptionClass(radio) { var label = radio.closest ? radio.closest('.pas-option') : radio.parentNode; if (!label) return; if (radio.checked) label.classList.add('pas-is-selected'); else label.classList.remove('pas-is-selected'); } // ========================================================================== // EVENTS // ========================================================================== // Delegated, bound once. Webflow strips inline onclick attributes on // publish, so every handler has to be addEventListener. // -------------------------------------------------------------------------- mount.addEventListener('click', function (ev) { var el = ev.target.closest ? ev.target.closest('[data-action]') : null; if (!el || !mount.contains(el)) return; var action = el.getAttribute('data-action'); if (action === 'start') { step = 1; render(); return; } if (action === 'back') { step = Math.max(0, step - 1); render(); return; } if (action === 'next') { var idx = Number(el.getAttribute('data-section')); if (!sectionComplete(idx)) return; step = (idx === SECTIONS.length - 1) ? STEP_CONTACT : step + 1; render(); return; } if (action === 'submit') { handleSubmitStep(); return; } if (action === 'late-email') { addLateEmail(); return; } }); // Selecting an option updates state in place. v1 re-rendered the whole // screen on every option click, which wiped any open-text already typed in // that section and yanked the page back to the top. mount.addEventListener('change', function (ev) { var target = ev.target; if (!target.classList || !target.classList.contains('pas-option-input')) return; var qid = target.getAttribute('data-qid'); answers[qid] = target.value; var group = mount.querySelectorAll('.pas-option-input[data-qid="' + qid + '"]'); for (var i = 0; i < group.length; i++) syncOptionClass(group[i]); syncNextButton(); }); // Open text is written to state on every keystroke, so Back, Next and // option clicks can never lose it. mount.addEventListener('input', function (ev) { var target = ev.target; if (!target.classList || !target.classList.contains('pas-open')) return; answers[target.getAttribute('data-qid')] = target.value.slice(0, CONFIG.MAX_OPEN_CHARS); }); render(); })();