MemberScripts
Una solución basada en atributos para añadir funciones a su sitio Webflow.
Simplemente copie algo de código, añada algunos atributos y listo.
Todos los clientes de Memberstack pueden solicitar asistencia en el Slack 2.0. Tenga en cuenta que no se trata de funciones oficiales y que no se puede garantizar la asistencia.

#178 - Rewrite Text When User Logged In
Rewrite text on your Webflow site to show different messages for logged-in and logged-out users.
<!-- 💙 MEMBERSCRIPT #178 v0.1 REWRITE TEXT WHEN USER IS LOGGED IN 💙 -->
<script>
async function getMemberData() {
if (!window.$memberstackDom) {
return null;
}
try {
const member = await window.$memberstackDom.getCurrentMember();
return member;
} catch (error) {
return null;
}
}
function updateText(member) {
const textElements = document.querySelectorAll('[data-ms-code="text-rewrite"]');
textElements.forEach((el) => {
if (!el.hasAttribute("data-ms-original-text")) {
el.setAttribute("data-ms-original-text", el.textContent.trim());
}
const originalText = el.getAttribute("data-ms-original-text");
const loggedInText = el.getAttribute("data-ms-logged-in-text");
const isLoggedIn = member && member.data && member.data.id;
if (isLoggedIn) {
if (loggedInText) {
el.textContent = loggedInText;
el.classList.add("ms-logged-in");
}
} else {
el.textContent = originalText;
el.classList.remove("ms-logged-in");
}
});
}
async function initialize() {
let attempts = 0;
while (!window.$memberstackDom && attempts < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
const member = await getMemberData();
updateText(member);
}
function tryInitialize() {
initialize();
setTimeout(initialize, 500);
setTimeout(initialize, 1000);
setTimeout(initialize, 2000);
}
tryInitialize();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', tryInitialize);
} else {
tryInitialize();
}
document.addEventListener('msLogin', async () => {
setTimeout(async () => {
const member = await getMemberData();
updateText(member);
}, 200);
});
document.addEventListener('msLogout', () => {
updateText(null);
});
</script>
<!-- 💙 MEMBERSCRIPT #178 v0.1 REWRITE TEXT WHEN USER IS LOGGED IN 💙 -->
<script>
async function getMemberData() {
if (!window.$memberstackDom) {
return null;
}
try {
const member = await window.$memberstackDom.getCurrentMember();
return member;
} catch (error) {
return null;
}
}
function updateText(member) {
const textElements = document.querySelectorAll('[data-ms-code="text-rewrite"]');
textElements.forEach((el) => {
if (!el.hasAttribute("data-ms-original-text")) {
el.setAttribute("data-ms-original-text", el.textContent.trim());
}
const originalText = el.getAttribute("data-ms-original-text");
const loggedInText = el.getAttribute("data-ms-logged-in-text");
const isLoggedIn = member && member.data && member.data.id;
if (isLoggedIn) {
if (loggedInText) {
el.textContent = loggedInText;
el.classList.add("ms-logged-in");
}
} else {
el.textContent = originalText;
el.classList.remove("ms-logged-in");
}
});
}
async function initialize() {
let attempts = 0;
while (!window.$memberstackDom && attempts < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
const member = await getMemberData();
updateText(member);
}
function tryInitialize() {
initialize();
setTimeout(initialize, 500);
setTimeout(initialize, 1000);
setTimeout(initialize, 2000);
}
tryInitialize();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', tryInitialize);
} else {
tryInitialize();
}
document.addEventListener('msLogin', async () => {
setTimeout(async () => {
const member = await getMemberData();
updateText(member);
}, 200);
});
document.addEventListener('msLogout', () => {
updateText(null);
});
</script>

#177 - Disable Auth Buttons Until required fields are completed
Automatically disables your form’s sign-up or login buttons until all required fields are filled.
<!-- 💙 MEMBERSCRIPT #177 v0.1 DISABLE AUTH BUTTONS UNTIL REQUIRED FIELDS ARE COMPLETED 💙 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const AUTH_BUTTON_SELECTORS = [
'[data-ms-code="auth-button"]',
'[data-ms-auth-provider]',
'button[type="submit"]',
'input[type="submit"]',
'.ms-auth-button',
'.auth-submit'
];
const REQUIRED_FIELD_SELECTORS = [
'input[required]',
'textarea[required]',
'select[required]',
'[data-ms-member][required]',
'[data-ms-code="required-field"]',
'[data-ms-required]'
];
const authButtons = [];
const requiredFields = [];
AUTH_BUTTON_SELECTORS.forEach(selector => {
document.querySelectorAll(selector).forEach(button => authButtons.push(button));
});
REQUIRED_FIELD_SELECTORS.forEach(selector => {
document.querySelectorAll(selector).forEach(field => requiredFields.push(field));
});
const uniqueAuthButtons = [...new Set(authButtons)];
const uniqueRequiredFields = [...new Set(requiredFields)];
function checkRequiredFields() {
let allFilled = true;
uniqueRequiredFields.forEach(field => {
if (field.type === 'checkbox' || field.type === 'radio') {
if (!field.checked) allFilled = false;
} else if (field.type === 'select-one') {
if (!field.value || field.value === '' || field.value === field.querySelector('option[value=""]')?.value) {
allFilled = false;
}
} else {
if (!field.value || field.value.trim() === '') allFilled = false;
}
});
uniqueAuthButtons.forEach(button => {
if (allFilled) {
button.disabled = false;
button.style.opacity = '1';
button.style.cursor = 'pointer';
button.classList.remove('disabled', 'ms-disabled');
} else {
button.disabled = true;
button.style.opacity = '0.5';
button.style.cursor = 'not-allowed';
button.classList.add('disabled', 'ms-disabled');
}
});
}
uniqueRequiredFields.forEach(field => {
field.addEventListener('input', checkRequiredFields);
field.addEventListener('change', checkRequiredFields);
field.addEventListener('paste', () => setTimeout(checkRequiredFields, 10));
});
checkRequiredFields();
const style = document.createElement('style');
style.textContent = `
.ms-disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
pointer-events: none !important;
}
.ms-disabled:hover {
transform: none !important;
box-shadow: none !important;
}
`;
document.head.appendChild(style);
});
</script>
<!-- 💙 MEMBERSCRIPT #177 v0.1 DISABLE AUTH BUTTONS UNTIL REQUIRED FIELDS ARE COMPLETED 💙 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const AUTH_BUTTON_SELECTORS = [
'[data-ms-code="auth-button"]',
'[data-ms-auth-provider]',
'button[type="submit"]',
'input[type="submit"]',
'.ms-auth-button',
'.auth-submit'
];
const REQUIRED_FIELD_SELECTORS = [
'input[required]',
'textarea[required]',
'select[required]',
'[data-ms-member][required]',
'[data-ms-code="required-field"]',
'[data-ms-required]'
];
const authButtons = [];
const requiredFields = [];
AUTH_BUTTON_SELECTORS.forEach(selector => {
document.querySelectorAll(selector).forEach(button => authButtons.push(button));
});
REQUIRED_FIELD_SELECTORS.forEach(selector => {
document.querySelectorAll(selector).forEach(field => requiredFields.push(field));
});
const uniqueAuthButtons = [...new Set(authButtons)];
const uniqueRequiredFields = [...new Set(requiredFields)];
function checkRequiredFields() {
let allFilled = true;
uniqueRequiredFields.forEach(field => {
if (field.type === 'checkbox' || field.type === 'radio') {
if (!field.checked) allFilled = false;
} else if (field.type === 'select-one') {
if (!field.value || field.value === '' || field.value === field.querySelector('option[value=""]')?.value) {
allFilled = false;
}
} else {
if (!field.value || field.value.trim() === '') allFilled = false;
}
});
uniqueAuthButtons.forEach(button => {
if (allFilled) {
button.disabled = false;
button.style.opacity = '1';
button.style.cursor = 'pointer';
button.classList.remove('disabled', 'ms-disabled');
} else {
button.disabled = true;
button.style.opacity = '0.5';
button.style.cursor = 'not-allowed';
button.classList.add('disabled', 'ms-disabled');
}
});
}
uniqueRequiredFields.forEach(field => {
field.addEventListener('input', checkRequiredFields);
field.addEventListener('change', checkRequiredFields);
field.addEventListener('paste', () => setTimeout(checkRequiredFields, 10));
});
checkRequiredFields();
const style = document.createElement('style');
style.textContent = `
.ms-disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
pointer-events: none !important;
}
.ms-disabled:hover {
transform: none !important;
box-shadow: none !important;
}
`;
document.head.appendChild(style);
});
</script>

#176 - Save & Display Last Used Auth Method
Displays a popup showing the last login method a member used to make logging in easier.
Código de la cabeza
Place this in your page <head>
<style>
.ms-popup-badge { /* CHANGE THE STYLE OF THE BADGE*/
position: absolute;
background: #2d62ff;
color: white;
padding: 8px 16px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
z-index: 999;
display: flex;
align-items: center;
gap: 8px;
pointer-events: none;
user-select: none;
}
.ms-popup-badge::before {
font-size: 14px;
font-weight: bold;
}
.ms-popup-badge .ms-popup-text {
font-size: 12px;
font-weight: 600;
}
/* Animation keyframes */
@keyframes ms-badge-fade-in {
from {
opacity: 0;
transform: translateY(10px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes ms-badge-fade-out {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.ms-popup-badge {
font-size: 11px;
padding: 6px 12px;
}
.ms-popup-badge .ms-popup-text {
font-size: 11px;
}
}
</style>
Código del cuerpo
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #176 v0.1 💙 - SAVE AND DISPLAY LAST AUTH METHOD -->
<script>
(function() {
'use strict';
const STORAGE_KEY = 'ms_last_auth_method';
// Auth method display names
const AUTH_METHOD_NAMES = {
'email': 'Email & Password',
'google': 'Google',
'facebook': 'Facebook',
'github': 'GitHub',
'linkedin': 'LinkedIn',
'twitter': 'Twitter',
'apple': 'Apple',
'microsoft': 'Microsoft',
'discord': 'Discord',
'spotify': 'Spotify',
'dribbble': 'Dribbble'
};
function getAuthMethodDisplayName(method) {
return AUTH_METHOD_NAMES[method] || method.charAt(0).toUpperCase() + method.slice(1);
}
function saveAuthMethod(method) {
if (method) localStorage.setItem(STORAGE_KEY, method);
}
function getLastAuthMethod() {
return localStorage.getItem(STORAGE_KEY);
}
function showPopupTag(method) {
if (!method) return;
document.querySelectorAll('.ms-popup-badge').forEach(badge => badge.remove());
let targetElement;
if (method === 'email') {
targetElement = document.querySelector('[data-ms-member="email"]') ||
document.querySelector('input[type="email"]') ||
document.querySelector('input[name="email"]');
} else {
targetElement = document.querySelector(`[data-ms-auth-provider="${method}"]`);
}
if (!targetElement) {
console.log('Memberstack: Target element not found for method:', method);
return;
}
const authMethodName = getAuthMethodDisplayName(method);
const badge = document.createElement('div');
badge.className = 'ms-popup-badge';
badge.innerHTML = `<span class="ms-popup-text">Last Auth Method Used: ${authMethodName}</span>`;
const elementRect = targetElement.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
document.body.appendChild(badge);
badge.style.position = 'absolute';
badge.style.top = (elementRect.top + scrollTop - 40) + 'px';
badge.style.left = (elementRect.right + scrollLeft - 200) + 'px';
badge.style.opacity = '0';
badge.style.transform = 'translateY(10px) scale(0.9)';
requestAnimationFrame(() => {
badge.style.transition = 'all 0.3s ease-out';
badge.style.opacity = '1';
badge.style.transform = 'translateY(0) scale(1)';
});
setTimeout(() => {
badge.style.transition = 'all 0.3s ease-in';
badge.style.opacity = '0';
badge.style.transform = 'translateY(-10px) scale(0.9)';
setTimeout(() => {
if (badge.parentNode) {
badge.parentNode.removeChild(badge);
}
}, 300);
}, 8000);
}
function handleEmailPasswordLogin() {
const emailForm = document.querySelector('[data-ms-form="login"]');
if (emailForm) {
emailForm.addEventListener('submit', () => {
setTimeout(() => saveAuthMethod('email'), 100);
});
}
}
function handleSocialAuthClicks() {
document.querySelectorAll('[data-ms-auth-provider]').forEach(button => {
button.addEventListener('click', function() {
const provider = this.getAttribute('data-ms-auth-provider');
if (provider) saveAuthMethod(provider);
});
});
document.addEventListener('ms:auth:start', e => {
const provider = e.detail?.provider || e.detail?.authMethod;
if (provider) saveAuthMethod(provider);
});
}
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}
handleEmailPasswordLogin();
handleSocialAuthClicks();
const lastMethod = getLastAuthMethod();
if (lastMethod) showPopupTag(lastMethod);
document.addEventListener('ms:auth:success', e => {
const method = e.detail?.method || e.detail?.provider || 'email';
saveAuthMethod(method);
showPopupTag(method);
});
}
init();
})();
</script>
Código de la cabeza
Place this in your page <head>
<style>
.ms-popup-badge { /* CHANGE THE STYLE OF THE BADGE*/
position: absolute;
background: #2d62ff;
color: white;
padding: 8px 16px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
z-index: 999;
display: flex;
align-items: center;
gap: 8px;
pointer-events: none;
user-select: none;
}
.ms-popup-badge::before {
font-size: 14px;
font-weight: bold;
}
.ms-popup-badge .ms-popup-text {
font-size: 12px;
font-weight: 600;
}
/* Animation keyframes */
@keyframes ms-badge-fade-in {
from {
opacity: 0;
transform: translateY(10px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes ms-badge-fade-out {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.ms-popup-badge {
font-size: 11px;
padding: 6px 12px;
}
.ms-popup-badge .ms-popup-text {
font-size: 11px;
}
}
</style>
Código del cuerpo
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #176 v0.1 💙 - SAVE AND DISPLAY LAST AUTH METHOD -->
<script>
(function() {
'use strict';
const STORAGE_KEY = 'ms_last_auth_method';
// Auth method display names
const AUTH_METHOD_NAMES = {
'email': 'Email & Password',
'google': 'Google',
'facebook': 'Facebook',
'github': 'GitHub',
'linkedin': 'LinkedIn',
'twitter': 'Twitter',
'apple': 'Apple',
'microsoft': 'Microsoft',
'discord': 'Discord',
'spotify': 'Spotify',
'dribbble': 'Dribbble'
};
function getAuthMethodDisplayName(method) {
return AUTH_METHOD_NAMES[method] || method.charAt(0).toUpperCase() + method.slice(1);
}
function saveAuthMethod(method) {
if (method) localStorage.setItem(STORAGE_KEY, method);
}
function getLastAuthMethod() {
return localStorage.getItem(STORAGE_KEY);
}
function showPopupTag(method) {
if (!method) return;
document.querySelectorAll('.ms-popup-badge').forEach(badge => badge.remove());
let targetElement;
if (method === 'email') {
targetElement = document.querySelector('[data-ms-member="email"]') ||
document.querySelector('input[type="email"]') ||
document.querySelector('input[name="email"]');
} else {
targetElement = document.querySelector(`[data-ms-auth-provider="${method}"]`);
}
if (!targetElement) {
console.log('Memberstack: Target element not found for method:', method);
return;
}
const authMethodName = getAuthMethodDisplayName(method);
const badge = document.createElement('div');
badge.className = 'ms-popup-badge';
badge.innerHTML = `<span class="ms-popup-text">Last Auth Method Used: ${authMethodName}</span>`;
const elementRect = targetElement.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
document.body.appendChild(badge);
badge.style.position = 'absolute';
badge.style.top = (elementRect.top + scrollTop - 40) + 'px';
badge.style.left = (elementRect.right + scrollLeft - 200) + 'px';
badge.style.opacity = '0';
badge.style.transform = 'translateY(10px) scale(0.9)';
requestAnimationFrame(() => {
badge.style.transition = 'all 0.3s ease-out';
badge.style.opacity = '1';
badge.style.transform = 'translateY(0) scale(1)';
});
setTimeout(() => {
badge.style.transition = 'all 0.3s ease-in';
badge.style.opacity = '0';
badge.style.transform = 'translateY(-10px) scale(0.9)';
setTimeout(() => {
if (badge.parentNode) {
badge.parentNode.removeChild(badge);
}
}, 300);
}, 8000);
}
function handleEmailPasswordLogin() {
const emailForm = document.querySelector('[data-ms-form="login"]');
if (emailForm) {
emailForm.addEventListener('submit', () => {
setTimeout(() => saveAuthMethod('email'), 100);
});
}
}
function handleSocialAuthClicks() {
document.querySelectorAll('[data-ms-auth-provider]').forEach(button => {
button.addEventListener('click', function() {
const provider = this.getAttribute('data-ms-auth-provider');
if (provider) saveAuthMethod(provider);
});
});
document.addEventListener('ms:auth:start', e => {
const provider = e.detail?.provider || e.detail?.authMethod;
if (provider) saveAuthMethod(provider);
});
}
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}
handleEmailPasswordLogin();
handleSocialAuthClicks();
const lastMethod = getLastAuthMethod();
if (lastMethod) showPopupTag(lastMethod);
document.addEventListener('ms:auth:success', e => {
const method = e.detail?.method || e.detail?.provider || 'email';
saveAuthMethod(method);
showPopupTag(method);
});
}
init();
})();
</script>

#175 - Create a Stripe payment link from Webflow Form Submission
Allow your members to submit a product through a Webflow form and automatically create a Stripe product.

#174 - Simple Referral Program
Create a simple referral program that generates unique links, adds copy & share buttons, and tracks clicks.
<!-- 💙 MEMBERSCRIPT #174 v0.1 SIMPLE REFERRAL PROGRAM 💙 -->
<script>
(function() {
'use strict';
const CONFIG = {
baseUrl: window.location.origin,
referralParam: 'ref',
trackingParam: 'utm_source',
trackingValue: 'member_referral',
webhookUrl: 'https://hook.eu2.make.com/1mfnxnmrkbl4e8tsyh8ob7kxuuauoc61' // REPLACE WITH YOUR WEBHOOK
};
let member = null;
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', init)
: init();
async function init() {
await loadMember();
if (member?.id) {
const referralUrl = generateReferralUrl(member.id);
// Populate the input field
const input = document.querySelector('[data-ms-code="referral-url-input"]');
if (input) {
input.value = referralUrl;
input.setAttribute('readonly', 'readonly');
}
// Attach to all buttons inside the container
const buttons = document.querySelectorAll('[data-ms-code="referral-link"] a');
buttons.forEach((btn) => {
if (btn.dataset.msAction === 'copy') {
btn.addEventListener('click', (e) => {
e.preventDefault();
copyToClipboard(referralUrl, btn);
});
}
if (btn.dataset.msAction === 'share') {
btn.addEventListener('click', (e) => {
e.preventDefault();
shareLink(referralUrl);
});
}
});
}
trackReferralClick();
}
async function loadMember() {
try {
if (window.$memberstackDom) {
const { data } = await window.$memberstackDom.getCurrentMember();
member = data;
}
} catch {}
}
function generateReferralUrl(memberId) {
const url = new URL(CONFIG.baseUrl);
url.searchParams.set(CONFIG.referralParam, memberId);
url.searchParams.set(CONFIG.trackingParam, CONFIG.trackingValue);
return url.toString();
}
function copyToClipboard(text, btn) {
navigator.clipboard.writeText(text).then(() => {
showFeedback(btn, 'Copied!');
}).catch(() => {
showFeedback(btn, 'Failed to copy');
});
}
function showFeedback(btn, msg) {
const original = btn.textContent;
btn.textContent = msg;
setTimeout(() => {
btn.textContent = original;
}, 2000);
}
function shareLink(url) {
if (navigator.share) {
navigator.share({
title: 'Join me!',
text: 'Use my referral link:',
url: url
});
} else {
navigator.clipboard.writeText(url);
alert('Referral link copied: ' + url);
}
}
function trackReferralClick() {
const urlParams = new URLSearchParams(window.location.search);
const referrerId = urlParams.get(CONFIG.referralParam);
if (!referrerId) return;
const visitorId = 'visitor_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const referralData = {
referrerId,
visitorId,
timestamp: Date.now(),
userAgent: navigator.userAgent,
referrer: document.referrer || null,
landingPage: window.location.href
};
fetch(CONFIG.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(referralData)
}).catch(() => {});
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #174 v0.1 SIMPLE REFERRAL PROGRAM 💙 -->
<script>
(function() {
'use strict';
const CONFIG = {
baseUrl: window.location.origin,
referralParam: 'ref',
trackingParam: 'utm_source',
trackingValue: 'member_referral',
webhookUrl: 'https://hook.eu2.make.com/1mfnxnmrkbl4e8tsyh8ob7kxuuauoc61' // REPLACE WITH YOUR WEBHOOK
};
let member = null;
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', init)
: init();
async function init() {
await loadMember();
if (member?.id) {
const referralUrl = generateReferralUrl(member.id);
// Populate the input field
const input = document.querySelector('[data-ms-code="referral-url-input"]');
if (input) {
input.value = referralUrl;
input.setAttribute('readonly', 'readonly');
}
// Attach to all buttons inside the container
const buttons = document.querySelectorAll('[data-ms-code="referral-link"] a');
buttons.forEach((btn) => {
if (btn.dataset.msAction === 'copy') {
btn.addEventListener('click', (e) => {
e.preventDefault();
copyToClipboard(referralUrl, btn);
});
}
if (btn.dataset.msAction === 'share') {
btn.addEventListener('click', (e) => {
e.preventDefault();
shareLink(referralUrl);
});
}
});
}
trackReferralClick();
}
async function loadMember() {
try {
if (window.$memberstackDom) {
const { data } = await window.$memberstackDom.getCurrentMember();
member = data;
}
} catch {}
}
function generateReferralUrl(memberId) {
const url = new URL(CONFIG.baseUrl);
url.searchParams.set(CONFIG.referralParam, memberId);
url.searchParams.set(CONFIG.trackingParam, CONFIG.trackingValue);
return url.toString();
}
function copyToClipboard(text, btn) {
navigator.clipboard.writeText(text).then(() => {
showFeedback(btn, 'Copied!');
}).catch(() => {
showFeedback(btn, 'Failed to copy');
});
}
function showFeedback(btn, msg) {
const original = btn.textContent;
btn.textContent = msg;
setTimeout(() => {
btn.textContent = original;
}, 2000);
}
function shareLink(url) {
if (navigator.share) {
navigator.share({
title: 'Join me!',
text: 'Use my referral link:',
url: url
});
} else {
navigator.clipboard.writeText(url);
alert('Referral link copied: ' + url);
}
}
function trackReferralClick() {
const urlParams = new URLSearchParams(window.location.search);
const referrerId = urlParams.get(CONFIG.referralParam);
if (!referrerId) return;
const visitorId = 'visitor_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const referralData = {
referrerId,
visitorId,
timestamp: Date.now(),
userAgent: navigator.userAgent,
referrer: document.referrer || null,
landingPage: window.location.href
};
fetch(CONFIG.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(referralData)
}).catch(() => {});
}
})();
</script>

#173 - CMS Chatbot Assistant
Build a Webflow chatbot that dynamically loads help articles from collection lists and updates in real time.
<!-- 💙 MEMBERSCRIPT #173 v0.1 💙 - CMS BASED CHATBOT ASSISTANT -->
<script>
(function() {
'use strict';
const CONFIG = {
primary: '#2d62ff', // CHANGE THIS
maxResults: 3, // CHANGE THIS
helpPath: '/post/' // CHANGE THIS
};
let kb = [], member = null, open = false, history = [];
let conversationContext = { lastQuery: '', topics: [] };
document.readyState === 'loading' ?
document.addEventListener('DOMContentLoaded', init) : init();
async function init() {
await loadMember();
createUI();
setupEvents();
await loadKB();
}
async function loadMember() {
try {
if (window.$memberstackDom) {
const { data } = await window.$memberstackDom.getCurrentMember();
member = data;
}
} catch {}
}
async function loadKB() {
const articles = document.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) return loadArticlesFromElements(articles);
if (!window.location.pathname.includes('/help')) {
return loadKnowledgeBaseFromHelpPage();
}
const wrappers = Array.from(document.querySelectorAll('[data-ms-code="kb-article"]'))
.map(el => el.parentElement)
.filter((v,i,a) => v && a.indexOf(v) === i);
if (wrappers.length === 0) return [];
const kbSet = new Set();
wrappers.forEach(wrapper => {
const observer = new MutationObserver(() => {
const articles = wrapper.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) {
Array.from(articles).forEach(el => {
const titleEl = el.querySelector('[data-ms-code="kb-title"]');
const contentEl = el.querySelector('[data-ms-code="kb-content"]');
const categoriesEl = el.querySelector('[data-ms-code="kb-categories"]');
const slugEl = el.querySelector('[data-ms-code="kb-slug"]');
const title = titleEl?.textContent?.trim() || '';
const content = contentEl?.textContent?.trim() || '';
const categoriesText = categoriesEl?.textContent?.trim() || '';
const slug = slugEl?.textContent?.trim() || `article-${kb.length}`;
if (!title || kbSet.has(title)) return;
const categories = categoriesText ? categoriesText.split(',').map(c => c.trim().toLowerCase()).filter(c => c) : [];
kb.push({ id: kb.length, title, content, slug, categories });
kbSet.add(title);
});
conversationContext.topics = kb.map(a => a.title);
updateWelcomeMessage(kb.length);
}
});
observer.observe(wrapper, { childList: true, subtree: true });
});
return kb;
}
async function loadKnowledgeBaseFromHelpPage() {
try {
const response = await fetch('/help');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const articles = doc.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) {
const kbData = loadArticlesFromElements(articles);
return kbData;
} else return [];
} catch {
return [];
}
}
function loadArticlesFromElements(articles) {
const uniqueArticles = new Map();
Array.from(articles).forEach((el, i) => {
const titleEl = el.querySelector('[data-ms-code="kb-title"]');
const contentEl = el.querySelector('[data-ms-code="kb-content"]');
const categoriesEl = el.querySelector('[data-ms-code="kb-categories"]');
const slugEl = el.querySelector('[data-ms-code="kb-slug"]');
const title = titleEl?.textContent?.trim() || '';
const content = contentEl?.textContent?.trim() || '';
const categoriesText = categoriesEl?.textContent?.trim() || '';
const slug = slugEl?.textContent?.trim() || `article-${i}`;
const categories = categoriesText ? categoriesText.split(',').map(c => c.trim().toLowerCase()).filter(c => c) : [];
if (uniqueArticles.has(title)) return;
uniqueArticles.set(title, {
id: uniqueArticles.size,
title,
content,
slug,
categories
});
});
kb = Array.from(uniqueArticles.values()).filter(a => a.title && a.content);
conversationContext.topics = kb.map(a => a.title);
updateWelcomeMessage(kb.length);
return kb;
}
function updateWelcomeMessage(articleCount) {
setTimeout(() => {
const messages = document.getElementById('ms-messages');
if (messages) {
const firstBubble = messages.querySelector('div');
if (firstBubble) {
firstBubble.innerHTML = `👋 Ask me anything! I can help with ${articleCount} topics.`;
}
}
}, 100);
}
function createUI() {
const trigger = document.createElement('div');
trigger.id = 'ms-chatbot';
trigger.innerHTML = `<div id="ms-chat-button" onclick="MemberscriptChat.toggle()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg></div>`;
const widget = document.createElement('div');
widget.id = 'ms-chat-window';
widget.innerHTML = `... (truncated for brevity) ...`; // Keep full inner HTML here as in original
document.body.appendChild(trigger);
document.body.appendChild(widget);
}
function setupEvents() {}
function toggle() { ... }
function close() { ... }
function send() { ... }
function search(query) { ... }
function generateFollowUpSuggestions(results) { ... }
function generateIntelligentFallback(query) { ... }
function addMsg(sender,text){ ... }
window.MemberscriptChat={
toggle,
close,
send,
ask: q => { document.getElementById('ms-input').value = q; send(); },
history: () => history,
reloadFromHelp: async () => {
const kbData = await loadKnowledgeBaseFromHelpPage();
if (kbData.length > 0) updateWelcomeMessage(kbData.length);
return kbData;
}
};
})();
</script>
<!-- 💙 MEMBERSCRIPT #173 v0.1 💙 - CMS BASED CHATBOT ASSISTANT -->
<script>
(function() {
'use strict';
const CONFIG = {
primary: '#2d62ff', // CHANGE THIS
maxResults: 3, // CHANGE THIS
helpPath: '/post/' // CHANGE THIS
};
let kb = [], member = null, open = false, history = [];
let conversationContext = { lastQuery: '', topics: [] };
document.readyState === 'loading' ?
document.addEventListener('DOMContentLoaded', init) : init();
async function init() {
await loadMember();
createUI();
setupEvents();
await loadKB();
}
async function loadMember() {
try {
if (window.$memberstackDom) {
const { data } = await window.$memberstackDom.getCurrentMember();
member = data;
}
} catch {}
}
async function loadKB() {
const articles = document.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) return loadArticlesFromElements(articles);
if (!window.location.pathname.includes('/help')) {
return loadKnowledgeBaseFromHelpPage();
}
const wrappers = Array.from(document.querySelectorAll('[data-ms-code="kb-article"]'))
.map(el => el.parentElement)
.filter((v,i,a) => v && a.indexOf(v) === i);
if (wrappers.length === 0) return [];
const kbSet = new Set();
wrappers.forEach(wrapper => {
const observer = new MutationObserver(() => {
const articles = wrapper.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) {
Array.from(articles).forEach(el => {
const titleEl = el.querySelector('[data-ms-code="kb-title"]');
const contentEl = el.querySelector('[data-ms-code="kb-content"]');
const categoriesEl = el.querySelector('[data-ms-code="kb-categories"]');
const slugEl = el.querySelector('[data-ms-code="kb-slug"]');
const title = titleEl?.textContent?.trim() || '';
const content = contentEl?.textContent?.trim() || '';
const categoriesText = categoriesEl?.textContent?.trim() || '';
const slug = slugEl?.textContent?.trim() || `article-${kb.length}`;
if (!title || kbSet.has(title)) return;
const categories = categoriesText ? categoriesText.split(',').map(c => c.trim().toLowerCase()).filter(c => c) : [];
kb.push({ id: kb.length, title, content, slug, categories });
kbSet.add(title);
});
conversationContext.topics = kb.map(a => a.title);
updateWelcomeMessage(kb.length);
}
});
observer.observe(wrapper, { childList: true, subtree: true });
});
return kb;
}
async function loadKnowledgeBaseFromHelpPage() {
try {
const response = await fetch('/help');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const articles = doc.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) {
const kbData = loadArticlesFromElements(articles);
return kbData;
} else return [];
} catch {
return [];
}
}
function loadArticlesFromElements(articles) {
const uniqueArticles = new Map();
Array.from(articles).forEach((el, i) => {
const titleEl = el.querySelector('[data-ms-code="kb-title"]');
const contentEl = el.querySelector('[data-ms-code="kb-content"]');
const categoriesEl = el.querySelector('[data-ms-code="kb-categories"]');
const slugEl = el.querySelector('[data-ms-code="kb-slug"]');
const title = titleEl?.textContent?.trim() || '';
const content = contentEl?.textContent?.trim() || '';
const categoriesText = categoriesEl?.textContent?.trim() || '';
const slug = slugEl?.textContent?.trim() || `article-${i}`;
const categories = categoriesText ? categoriesText.split(',').map(c => c.trim().toLowerCase()).filter(c => c) : [];
if (uniqueArticles.has(title)) return;
uniqueArticles.set(title, {
id: uniqueArticles.size,
title,
content,
slug,
categories
});
});
kb = Array.from(uniqueArticles.values()).filter(a => a.title && a.content);
conversationContext.topics = kb.map(a => a.title);
updateWelcomeMessage(kb.length);
return kb;
}
function updateWelcomeMessage(articleCount) {
setTimeout(() => {
const messages = document.getElementById('ms-messages');
if (messages) {
const firstBubble = messages.querySelector('div');
if (firstBubble) {
firstBubble.innerHTML = `👋 Ask me anything! I can help with ${articleCount} topics.`;
}
}
}, 100);
}
function createUI() {
const trigger = document.createElement('div');
trigger.id = 'ms-chatbot';
trigger.innerHTML = `<div id="ms-chat-button" onclick="MemberscriptChat.toggle()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg></div>`;
const widget = document.createElement('div');
widget.id = 'ms-chat-window';
widget.innerHTML = `... (truncated for brevity) ...`; // Keep full inner HTML here as in original
document.body.appendChild(trigger);
document.body.appendChild(widget);
}
function setupEvents() {}
function toggle() { ... }
function close() { ... }
function send() { ... }
function search(query) { ... }
function generateFollowUpSuggestions(results) { ... }
function generateIntelligentFallback(query) { ... }
function addMsg(sender,text){ ... }
window.MemberscriptChat={
toggle,
close,
send,
ask: q => { document.getElementById('ms-input').value = q; send(); },
history: () => history,
reloadFromHelp: async () => {
const kbData = await loadKnowledgeBaseFromHelpPage();
if (kbData.length > 0) updateWelcomeMessage(kbData.length);
return kbData;
}
};
})();
</script>

#172 - Capture Stripe Checkout Session
Track Memberstack Stripe checkouts and sends member + transaction data to your webhook.
<!-- 💙 MEMBERSCRIPT #172 v0.1 💙 - CAPTURE STRIPE CHECKOUT SESSION -->
<script>
(function() {
'use strict';
// Configuration object for webhook URL and debugging options
const CONFIG = {
WEBHOOK_URL: 'https://hook.eu2.make.com/ld2ovhwaw6fo9ufvq20lfcocmsjhr6zc', // REPLACE THIS WITH YOUR WEBHOOK URL
TRACK_FAILURES: true,
DEBUG: true
};
// Event listener to execute code once the DOM content is fully loaded
document.addEventListener('DOMContentLoaded', async () => {
if (CONFIG.DEBUG) console.log('Webhook-only checkout tracker initialized');
// Fetch current member data from Memberstack
const member = await getCurrentMember();
// Check if the checkout was successful
if (isCheckoutSuccess()) {
onCheckoutSuccess(member);
} else {
onCheckoutFailure(member);
}
});
// Function to retrieve current member data from Memberstack
async function getCurrentMember() {
try {
const { data } = await window.$memberstackDom.getCurrentMember();
if (!data) return {};
const fn = data.customFields?.['first-name'] || data.customFields?.['first_name'] || '';
return {
ms_member_id: data.id || '',
ms_email: data.auth?.email || '',
ms_first_name: fn
};
} catch (e) {
if (CONFIG.DEBUG) console.warn('Memberstack fetch error', e);
return {};
}
}
// Function to determine if the checkout was successful based on URL parameters
function isCheckoutSuccess() {
const p = new URLSearchParams(window.location.search);
return p.get('fromCheckout') === 'true' && (p.has('msPriceId') || p.has('stripePriceId'));
}
// Function to handle successful checkout
function onCheckoutSuccess(member) {
if (CONFIG.DEBUG) console.log('Checkout success detected');
const data = extractUrlData();
sendToWebhook({ ...data, ...member }, 'checkout_success');
setTimeout(cleanUrl, 2000);
}
// Function to generate or retrieve GA4 client ID
function getGA4ClientId() {
// Try to get existing client ID from localStorage
let clientId = localStorage.getItem('ga4_client_id');
if (!clientId) {
// Generate new client ID if none exists
clientId = 'GA1.1.' + Math.random().toString(36).substring(2, 15) + '.' + Date.now();
localStorage.setItem('ga4_client_id', clientId);
}
return clientId;
}
// Function to extract relevant data from the URL query parameters
function extractUrlData() {
const p = new URLSearchParams(window.location.search);
return {
fromCheckout: p.get('fromCheckout'),
msPriceId: p.get('msPriceId'),
stripePriceId: p.get('stripePriceId'),
planId: p.get('planId'),
memberId: p.get('memberId'),
transactionId: `ms_checkout_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,
timestamp: new Date().toISOString(),
successUrl: window.location.href,
checkout_session_id: p.get('checkout_session_id'),
payment_intent: p.get('payment_intent'),
amount: p.get('amount'),
currency: p.get('currency') || 'USD',
email: p.get('customer_email') || p.get('email'),
subscription_id: p.get('subscription_id'),
customer_id: p.get('customer_id'),
payment_status: p.get('payment_status'),
ga4_client_id: getGA4ClientId() // Add GA4 client ID
};
}
// Function to send the collected data to a specified webhook URL
async function sendToWebhook(data, type) {
const fd = new FormData();
fd.append('event_type', type);
Object.entries(data).forEach(([k,v]) => v != null && fd.append(k, v));
try {
await fetch(CONFIG.WEBHOOK_URL, { method: 'POST', body: fd });
if (CONFIG.DEBUG) console.log(`Data sent: ${type}`, data);
} catch (e) {
console.error('Webhook error', e);
}
}
// Function to clean up the URL by removing specific query parameters
function cleanUrl() {
const url = new URL(window.location);
['fromCheckout','msPriceId','stripePriceId','planId','memberId','amount','currency'].forEach(p => url.searchParams.delete(p));
window.history.replaceState({}, document.title, url.toString());
if (CONFIG.DEBUG) console.log('URL cleaned');
}
// Function to handle checkout failure
function onCheckoutFailure(member) {
if (!CONFIG.TRACK_FAILURES) return;
const p = new URLSearchParams(window.location.search);
const failed = p.get('payment_status') === 'failed' || p.get('error');
if (!failed) return;
const data = {
failure_url: window.location.href,
payment_status: p.get('payment_status'),
error: p.get('error'),
msPriceId: p.get('msPriceId'),
stripePriceId: p.get('stripePriceId'),
timestamp: new Date().toISOString()
};
sendToWebhook({ ...data, ...member }, 'checkout_failure');
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #172 v0.1 💙 - CAPTURE STRIPE CHECKOUT SESSION -->
<script>
(function() {
'use strict';
// Configuration object for webhook URL and debugging options
const CONFIG = {
WEBHOOK_URL: 'https://hook.eu2.make.com/ld2ovhwaw6fo9ufvq20lfcocmsjhr6zc', // REPLACE THIS WITH YOUR WEBHOOK URL
TRACK_FAILURES: true,
DEBUG: true
};
// Event listener to execute code once the DOM content is fully loaded
document.addEventListener('DOMContentLoaded', async () => {
if (CONFIG.DEBUG) console.log('Webhook-only checkout tracker initialized');
// Fetch current member data from Memberstack
const member = await getCurrentMember();
// Check if the checkout was successful
if (isCheckoutSuccess()) {
onCheckoutSuccess(member);
} else {
onCheckoutFailure(member);
}
});
// Function to retrieve current member data from Memberstack
async function getCurrentMember() {
try {
const { data } = await window.$memberstackDom.getCurrentMember();
if (!data) return {};
const fn = data.customFields?.['first-name'] || data.customFields?.['first_name'] || '';
return {
ms_member_id: data.id || '',
ms_email: data.auth?.email || '',
ms_first_name: fn
};
} catch (e) {
if (CONFIG.DEBUG) console.warn('Memberstack fetch error', e);
return {};
}
}
// Function to determine if the checkout was successful based on URL parameters
function isCheckoutSuccess() {
const p = new URLSearchParams(window.location.search);
return p.get('fromCheckout') === 'true' && (p.has('msPriceId') || p.has('stripePriceId'));
}
// Function to handle successful checkout
function onCheckoutSuccess(member) {
if (CONFIG.DEBUG) console.log('Checkout success detected');
const data = extractUrlData();
sendToWebhook({ ...data, ...member }, 'checkout_success');
setTimeout(cleanUrl, 2000);
}
// Function to generate or retrieve GA4 client ID
function getGA4ClientId() {
// Try to get existing client ID from localStorage
let clientId = localStorage.getItem('ga4_client_id');
if (!clientId) {
// Generate new client ID if none exists
clientId = 'GA1.1.' + Math.random().toString(36).substring(2, 15) + '.' + Date.now();
localStorage.setItem('ga4_client_id', clientId);
}
return clientId;
}
// Function to extract relevant data from the URL query parameters
function extractUrlData() {
const p = new URLSearchParams(window.location.search);
return {
fromCheckout: p.get('fromCheckout'),
msPriceId: p.get('msPriceId'),
stripePriceId: p.get('stripePriceId'),
planId: p.get('planId'),
memberId: p.get('memberId'),
transactionId: `ms_checkout_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,
timestamp: new Date().toISOString(),
successUrl: window.location.href,
checkout_session_id: p.get('checkout_session_id'),
payment_intent: p.get('payment_intent'),
amount: p.get('amount'),
currency: p.get('currency') || 'USD',
email: p.get('customer_email') || p.get('email'),
subscription_id: p.get('subscription_id'),
customer_id: p.get('customer_id'),
payment_status: p.get('payment_status'),
ga4_client_id: getGA4ClientId() // Add GA4 client ID
};
}
// Function to send the collected data to a specified webhook URL
async function sendToWebhook(data, type) {
const fd = new FormData();
fd.append('event_type', type);
Object.entries(data).forEach(([k,v]) => v != null && fd.append(k, v));
try {
await fetch(CONFIG.WEBHOOK_URL, { method: 'POST', body: fd });
if (CONFIG.DEBUG) console.log(`Data sent: ${type}`, data);
} catch (e) {
console.error('Webhook error', e);
}
}
// Function to clean up the URL by removing specific query parameters
function cleanUrl() {
const url = new URL(window.location);
['fromCheckout','msPriceId','stripePriceId','planId','memberId','amount','currency'].forEach(p => url.searchParams.delete(p));
window.history.replaceState({}, document.title, url.toString());
if (CONFIG.DEBUG) console.log('URL cleaned');
}
// Function to handle checkout failure
function onCheckoutFailure(member) {
if (!CONFIG.TRACK_FAILURES) return;
const p = new URLSearchParams(window.location.search);
const failed = p.get('payment_status') === 'failed' || p.get('error');
if (!failed) return;
const data = {
failure_url: window.location.href,
payment_status: p.get('payment_status'),
error: p.get('error'),
msPriceId: p.get('msPriceId'),
stripePriceId: p.get('stripePriceId'),
timestamp: new Date().toISOString()
};
sendToWebhook({ ...data, ...member }, 'checkout_failure');
}
})();
</script>

#171 - Multi-Step Onboarding with Auto Tab Navigation
Automatically advances users through multi-step tabbed onboarding steps.
<!-- 💙 MEMBERSCRIPT #171 v0.1 💙 - MULTI-STEP ONBOARDING WITH AUTO TAB NAVIGATION -->
<script>
(function() {
'use strict';
// Configuration
const CONFIG = {
TABS_SELECTOR: '[data-ms-code="onboarding-tabs"]',
FORM_SELECTOR: '[data-ms-code="profile-form"]',
SUCCESS_SELECTOR: '[data-ms-message="success"]',
WEBFLOW_SUCCESS_SELECTOR: '.w-form-done',
TAB_BUTTON_SELECTOR: '[data-w-tab]',
TAB_PANE_SELECTOR: '.w-tab-pane',
DEFAULT_DELAY: 600 //Customize this delay between tabs
};
// Wait for Memberstack to be ready
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackDom.getCurrentMember) {
resolve();
return;
}
document.addEventListener('memberstack.ready', resolve, { once: true });
const checkInterval = setInterval(() => {
if (window.$memberstackDom && window.$memberstackDom.getCurrentMember) {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
resolve();
}, 10000);
});
}
let isAdvancing = false;
function handleFormSuccess(form, tabButtons, tabPanes, tabsContainer) {
if (isAdvancing) return;
isAdvancing = true;
const currentPane = form.closest('.w-tab-pane');
if (!currentPane) {
isAdvancing = false;
return;
}
const activeTabButton = tabButtons.find(btn => btn.classList.contains('w--current'));
const actualCurrentIndex = activeTabButton ? tabButtons.indexOf(activeTabButton) : -1;
const delay = parseInt(tabsContainer.dataset.msDelay) || CONFIG.DEFAULT_DELAY;
const shouldReset = form.dataset.msReset !== 'false';
setTimeout(() => {
const webflowSuccess = form.parentElement.querySelector('.w-form-done');
if (webflowSuccess) webflowSuccess.style.display = 'none';
if (shouldReset) form.reset();
if (actualCurrentIndex >= 0) {
const nextTabButton = tabButtons[actualCurrentIndex + 1];
if (nextTabButton) {
nextTabButton.click();
} else {
const finalRedirect = currentPane.dataset.msFinalRedirect || tabsContainer.dataset.msFinalRedirect;
if (finalRedirect) {
window.location.href = finalRedirect;
} else {
tabsContainer.dispatchEvent(new CustomEvent('onboarding:complete', {
detail: { totalSteps: tabPanes.length }
}));
}
}
}
setTimeout(() => { isAdvancing = false; }, 1000);
}, delay);
}
function setupSuccessDetection(form, tabButtons, tabPanes, tabsContainer) {
const formWrapper = form.parentElement;
const webflowSuccess = formWrapper.querySelector('.w-form-done');
let hasTriggered = false;
function triggerSuccess() {
if (hasTriggered || isAdvancing) return;
hasTriggered = true;
clearAllTimers();
handleFormSuccess(form, tabButtons, tabPanes, tabsContainer);
}
if (window.$memberstackDom) {
const profileUpdateHandler = () => triggerSuccess();
document.addEventListener('ms:profile:updated', profileUpdateHandler);
document.addEventListener('memberstack:profile-updated', profileUpdateHandler);
document.addEventListener('ms:member:updated', profileUpdateHandler);
const originalUpdateMember = window.$memberstackDom.updateMember;
if (originalUpdateMember) {
window.$memberstackDom.updateMember = function(...args) {
return originalUpdateMember.apply(this, args).then((result) => {
setTimeout(() => triggerSuccess(), 100);
return result;
}).catch((error) => { throw error; });
};
}
}
let webflowObserver, formObserver, fallbackTimer, memberStackTimer;
if (webflowSuccess) {
webflowObserver = new MutationObserver(() => {
const successStyle = window.getComputedStyle(webflowSuccess);
const isSuccessVisible = successStyle.display !== 'none' && webflowSuccess.offsetParent !== null;
if (isSuccessVisible) triggerSuccess();
});
webflowObserver.observe(webflowSuccess, { attributes: true, attributeFilter: ['style','tabindex','class'] });
}
formObserver = new MutationObserver(() => {
const hasSuccessClass = formWrapper.classList.contains('w-form-done') ||
formWrapper.classList.contains('w--success') ||
formWrapper.classList.contains('ms-success');
if (hasSuccessClass) triggerSuccess();
});
formObserver.observe(formWrapper, { attributes: true, attributeFilter: ['class'] });
function clearAllTimers() {
if (fallbackTimer) clearTimeout(fallbackTimer);
if (memberStackTimer) clearTimeout(memberStackTimer);
if (webflowObserver) webflowObserver.disconnect();
if (formObserver) formObserver.disconnect();
}
form.addEventListener('submit', () => {
fallbackTimer = setTimeout(() => {
const submitButton = form.querySelector('[type="submit"]');
const isSubmitting = submitButton && (
submitButton.value.includes('wait') ||
submitButton.disabled ||
submitButton.classList.contains('w--current')
);
if (!isSubmitting) triggerSuccess();
}, 2000);
});
window[`triggerTabAdvance_${form.id || 'form'}`] = () => triggerSuccess();
}
function initializeTabNavigator(tabsContainer) {
const tabButtons = Array.from(tabsContainer.querySelectorAll(CONFIG.TAB_BUTTON_SELECTOR));
const tabPanes = Array.from(tabsContainer.querySelectorAll(CONFIG.TAB_PANE_SELECTOR));
const forms = Array.from(tabsContainer.querySelectorAll(CONFIG.FORM_SELECTOR));
if (!tabButtons.length || !tabPanes.length || !forms.length) return;
forms.forEach((form) => setupSuccessDetection(form, tabButtons, tabPanes, tabsContainer));
tabsContainer.dispatchEvent(new CustomEvent('onboarding:initialized', {
detail: { totalSteps: tabPanes.length, formsCount: forms.length }
}));
}
async function init() {
await waitForMemberstack();
const tabsContainers = document.querySelectorAll(CONFIG.TABS_SELECTOR);
if (!tabsContainers.length) return;
tabsContainers.forEach(initializeTabNavigator);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.MemberScript171 = { init, CONFIG, version: '1.0' };
})();
</script>
<!-- 💙 MEMBERSCRIPT #171 v0.1 💙 - MULTI-STEP ONBOARDING WITH AUTO TAB NAVIGATION -->
<script>
(function() {
'use strict';
// Configuration
const CONFIG = {
TABS_SELECTOR: '[data-ms-code="onboarding-tabs"]',
FORM_SELECTOR: '[data-ms-code="profile-form"]',
SUCCESS_SELECTOR: '[data-ms-message="success"]',
WEBFLOW_SUCCESS_SELECTOR: '.w-form-done',
TAB_BUTTON_SELECTOR: '[data-w-tab]',
TAB_PANE_SELECTOR: '.w-tab-pane',
DEFAULT_DELAY: 600 //Customize this delay between tabs
};
// Wait for Memberstack to be ready
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackDom.getCurrentMember) {
resolve();
return;
}
document.addEventListener('memberstack.ready', resolve, { once: true });
const checkInterval = setInterval(() => {
if (window.$memberstackDom && window.$memberstackDom.getCurrentMember) {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
resolve();
}, 10000);
});
}
let isAdvancing = false;
function handleFormSuccess(form, tabButtons, tabPanes, tabsContainer) {
if (isAdvancing) return;
isAdvancing = true;
const currentPane = form.closest('.w-tab-pane');
if (!currentPane) {
isAdvancing = false;
return;
}
const activeTabButton = tabButtons.find(btn => btn.classList.contains('w--current'));
const actualCurrentIndex = activeTabButton ? tabButtons.indexOf(activeTabButton) : -1;
const delay = parseInt(tabsContainer.dataset.msDelay) || CONFIG.DEFAULT_DELAY;
const shouldReset = form.dataset.msReset !== 'false';
setTimeout(() => {
const webflowSuccess = form.parentElement.querySelector('.w-form-done');
if (webflowSuccess) webflowSuccess.style.display = 'none';
if (shouldReset) form.reset();
if (actualCurrentIndex >= 0) {
const nextTabButton = tabButtons[actualCurrentIndex + 1];
if (nextTabButton) {
nextTabButton.click();
} else {
const finalRedirect = currentPane.dataset.msFinalRedirect || tabsContainer.dataset.msFinalRedirect;
if (finalRedirect) {
window.location.href = finalRedirect;
} else {
tabsContainer.dispatchEvent(new CustomEvent('onboarding:complete', {
detail: { totalSteps: tabPanes.length }
}));
}
}
}
setTimeout(() => { isAdvancing = false; }, 1000);
}, delay);
}
function setupSuccessDetection(form, tabButtons, tabPanes, tabsContainer) {
const formWrapper = form.parentElement;
const webflowSuccess = formWrapper.querySelector('.w-form-done');
let hasTriggered = false;
function triggerSuccess() {
if (hasTriggered || isAdvancing) return;
hasTriggered = true;
clearAllTimers();
handleFormSuccess(form, tabButtons, tabPanes, tabsContainer);
}
if (window.$memberstackDom) {
const profileUpdateHandler = () => triggerSuccess();
document.addEventListener('ms:profile:updated', profileUpdateHandler);
document.addEventListener('memberstack:profile-updated', profileUpdateHandler);
document.addEventListener('ms:member:updated', profileUpdateHandler);
const originalUpdateMember = window.$memberstackDom.updateMember;
if (originalUpdateMember) {
window.$memberstackDom.updateMember = function(...args) {
return originalUpdateMember.apply(this, args).then((result) => {
setTimeout(() => triggerSuccess(), 100);
return result;
}).catch((error) => { throw error; });
};
}
}
let webflowObserver, formObserver, fallbackTimer, memberStackTimer;
if (webflowSuccess) {
webflowObserver = new MutationObserver(() => {
const successStyle = window.getComputedStyle(webflowSuccess);
const isSuccessVisible = successStyle.display !== 'none' && webflowSuccess.offsetParent !== null;
if (isSuccessVisible) triggerSuccess();
});
webflowObserver.observe(webflowSuccess, { attributes: true, attributeFilter: ['style','tabindex','class'] });
}
formObserver = new MutationObserver(() => {
const hasSuccessClass = formWrapper.classList.contains('w-form-done') ||
formWrapper.classList.contains('w--success') ||
formWrapper.classList.contains('ms-success');
if (hasSuccessClass) triggerSuccess();
});
formObserver.observe(formWrapper, { attributes: true, attributeFilter: ['class'] });
function clearAllTimers() {
if (fallbackTimer) clearTimeout(fallbackTimer);
if (memberStackTimer) clearTimeout(memberStackTimer);
if (webflowObserver) webflowObserver.disconnect();
if (formObserver) formObserver.disconnect();
}
form.addEventListener('submit', () => {
fallbackTimer = setTimeout(() => {
const submitButton = form.querySelector('[type="submit"]');
const isSubmitting = submitButton && (
submitButton.value.includes('wait') ||
submitButton.disabled ||
submitButton.classList.contains('w--current')
);
if (!isSubmitting) triggerSuccess();
}, 2000);
});
window[`triggerTabAdvance_${form.id || 'form'}`] = () => triggerSuccess();
}
function initializeTabNavigator(tabsContainer) {
const tabButtons = Array.from(tabsContainer.querySelectorAll(CONFIG.TAB_BUTTON_SELECTOR));
const tabPanes = Array.from(tabsContainer.querySelectorAll(CONFIG.TAB_PANE_SELECTOR));
const forms = Array.from(tabsContainer.querySelectorAll(CONFIG.FORM_SELECTOR));
if (!tabButtons.length || !tabPanes.length || !forms.length) return;
forms.forEach((form) => setupSuccessDetection(form, tabButtons, tabPanes, tabsContainer));
tabsContainer.dispatchEvent(new CustomEvent('onboarding:initialized', {
detail: { totalSteps: tabPanes.length, formsCount: forms.length }
}));
}
async function init() {
await waitForMemberstack();
const tabsContainers = document.querySelectorAll(CONFIG.TABS_SELECTOR);
if (!tabsContainers.length) return;
tabsContainers.forEach(initializeTabNavigator);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.MemberScript171 = { init, CONFIG, version: '1.0' };
})();
</script>

#170 - Pre-fill Stripe Payment Links with Member Email
Redirect logged-in members to a Stripe Payment Link with their email address prefilled
<!-- 💙 MEMBERSCRIPT #170 v0.1 💙 - PREFILL STRIPE PAYMENT LINK WITH A MEMBERS EMAIL -->
<script>
(function() {
const STRIPE_PAYMENT_LINK = "https://buy.stripe.com/test_00wfZg0W43drdOobsJgIo03";
function redirectToStripeWithEmail(memberEmail) {
if (!memberEmail) return;
try {
const paymentUrl = `${STRIPE_PAYMENT_LINK}?prefilled_email=${encodeURIComponent(memberEmail)}`;
window.location.href = paymentUrl;
} catch (error) {
console.error("Memberscript #170: Error creating payment URL:", error);
}
}
function handleButtonClick(event) {
event.preventDefault();
if (!window.$memberstackReady) return;
window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
const userEmail = member?.email || member?.auth?.email || member?.profile?.email || null;
if (userEmail) {
redirectToStripeWithEmail(userEmail);
}
}).catch(() => {
// Silent fail if no member data
});
}
function setupButtonListeners() {
document.querySelectorAll('[data-ms-code="prefill-link"]').forEach((button) => {
button.addEventListener('click', handleButtonClick);
});
}
function initializeScript() {
if (window.$memberstackReady && window.$memberstackDom) {
setTimeout(setupButtonListeners, 500);
} else {
document.addEventListener("memberstack.ready", () => {
setTimeout(setupButtonListeners, 500);
});
}
}
initializeScript();
})();
</script>
<!-- 💙 MEMBERSCRIPT #170 v0.1 💙 - PREFILL STRIPE PAYMENT LINK WITH A MEMBERS EMAIL -->
<script>
(function() {
const STRIPE_PAYMENT_LINK = "https://buy.stripe.com/test_00wfZg0W43drdOobsJgIo03";
function redirectToStripeWithEmail(memberEmail) {
if (!memberEmail) return;
try {
const paymentUrl = `${STRIPE_PAYMENT_LINK}?prefilled_email=${encodeURIComponent(memberEmail)}`;
window.location.href = paymentUrl;
} catch (error) {
console.error("Memberscript #170: Error creating payment URL:", error);
}
}
function handleButtonClick(event) {
event.preventDefault();
if (!window.$memberstackReady) return;
window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
const userEmail = member?.email || member?.auth?.email || member?.profile?.email || null;
if (userEmail) {
redirectToStripeWithEmail(userEmail);
}
}).catch(() => {
// Silent fail if no member data
});
}
function setupButtonListeners() {
document.querySelectorAll('[data-ms-code="prefill-link"]').forEach((button) => {
button.addEventListener('click', handleButtonClick);
});
}
function initializeScript() {
if (window.$memberstackReady && window.$memberstackDom) {
setTimeout(setupButtonListeners, 500);
} else {
document.addEventListener("memberstack.ready", () => {
setTimeout(setupButtonListeners, 500);
});
}
}
initializeScript();
})();
</script>

#169 - Autoplay slider with an optional manual selection.
Add on scroll autoplay to your Webflow sliders with optional pause-on-hover, custom slider dots navigation.
<!-- 💙 MEMBERSCRIPT #169 v0.1 💙 - AUTOPLAY SLIDER WITH OPTIONAL MANUAL SELECTION -->
<script>
(function() {
'use strict';
// Wait for DOM to be ready
function initSliders() {
const sliders = document.querySelectorAll('[data-ms-code="auto-slider"]');
sliders.forEach(slider => {
new AutoSlider(slider);
});
}
class AutoSlider {
constructor(element) {
this.slider = element;
this.track = this.slider.querySelector('[data-ms-code="slider-track"]');
this.slides = this.slider.querySelectorAll('[data-ms-code="slider-slide"]');
this.dotsContainer = this.slider.querySelector('[data-ms-code="slider-dots"]');
this.dots = this.slider.querySelectorAll('[data-ms-code="slider-dot"]');
this.collectDots();
// Configuration
this.currentSlide = 0;
this.interval = parseInt(this.slider.dataset.msInterval) || 3000;
this.pauseOnHover = this.slider.dataset.msPauseOnHover !== 'false';
this.resumeDelay = parseInt(this.slider.dataset.msResumeDelay) || 3000;
this.autoplayOnVisible = this.slider.dataset.msAutoplayOnVisible === 'true';
this.visibleThreshold = Number.isNaN(parseFloat(this.slider.dataset.msVisibleThreshold))
? 0.3
: Math.min(1, Math.max(0, parseFloat(this.slider.dataset.msVisibleThreshold)));
this.dotsActiveClass = this.dotsContainer?.dataset.msDotActiveClass || '';
this.dotsInactiveClass = this.dotsContainer?.dataset.msDotInactiveClass || '';
this.defaultActiveClass = '';
this.defaultInactiveClass = '';
// State
this.autoplayTimer = null;
this.resumeTimer = null;
this.isUserInteracting = false;
this.isPaused = false;
this.isInView = false;
this.visibilityObserver = null;
// Validate required elements
if (!this.track || this.slides.length === 0) {
console.warn('AutoSlider: Required elements not found');
return;
}
this.init();
}
init() {
// Set up initial styles
this.setupStyles();
// Detect default dot classes from markup if no attributes provided
this.detectDotClasses();
// Sync with Webflow's current state
this.syncWithWebflow();
// Bind event listeners
this.bindEvents();
// Start autoplay (optionally only when visible)
if (this.autoplayOnVisible) {
this.setupVisibilityObserver();
} else {
this.startAutoplay();
}
console.log('AutoSlider initialized with', this.slides.length, 'slides');
}
setupStyles() {
// No CSS modifications - work with existing Webflow slider styles
// Only add data attributes to slides for tracking
this.slides.forEach((slide, index) => {
slide.dataset.slideIndex = index;
});
// Improve accessibility for custom dots without altering styles
if (this.dots && this.dots.length) {
this.dots.forEach((dot, index) => {
if (!dot.hasAttribute('role')) dot.setAttribute('role', 'button');
if (!dot.hasAttribute('tabindex')) dot.setAttribute('tabindex', '0');
if (!dot.hasAttribute('aria-label')) dot.setAttribute('aria-label', `Show slide ${index + 1}`);
// If data-ms-slide is missing, infer from position
if (!dot.dataset.msSlide) dot.dataset.msSlide = String(index);
});
}
}
bindEvents() {
// No custom prev/next controls
// Custom dot navigation (data-ms-code dots). Use event delegation if container exists.
if (this.dotsContainer) {
this.dotsContainer.addEventListener('click', (e) => {
const dot = e.target.closest('[data-ms-code="slider-dot"]');
if (!dot || !this.dotsContainer.contains(dot)) return;
e.preventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
// Recollect in case DOM changed
this.dots = this.dotsContainer.querySelectorAll('[data-ms-code="slider-dot"]');
slideIndex = Array.from(this.dots).indexOf(dot);
}
if (!Number.isNaN(slideIndex)) {
this.handleUserInteraction();
this.goToSlide(slideIndex);
}
});
}
// Also attach direct listeners to cover cases without a container
this.dots.forEach(dot => {
dot.addEventListener('click', (e) => {
e.preventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
slideIndex = Array.from(this.dots).indexOf(dot);
}
if (!Number.isNaN(slideIndex)) {
this.handleUserInteraction();
this.goToSlide(slideIndex);
}
});
// Keyboard support for custom dots
dot.addEventListener('keydown', (e) => {
const key = e.key;
if (key === 'Enter' || key === ' ') {
e.preventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
slideIndex = Array.from(this.dots).indexOf(dot);
}
if (!Number.isNaN(slideIndex)) {
this.handleUserInteraction();
this.goToSlide(slideIndex);
}
}
});
});
// Listen for Webflow slider interactions to pause autoplay
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
const webflowArrows = this.slider.querySelectorAll('.w-slider-arrow-left, .w-slider-arrow-right');
webflowDots.forEach(dot => {
dot.addEventListener('click', () => {
this.handleUserInteraction();
// Update our current slide based on Webflow's active dot
const activeDotIndex = Array.from(webflowDots).indexOf(dot);
if (activeDotIndex !== -1) {
this.currentSlide = activeDotIndex;
this.updateActiveStates();
}
// Schedule resume after inactivity
clearTimeout(this.resumeTimer);
this.resumeTimer = setTimeout(() => {
this.isUserInteracting = false;
this.resumeAutoplay();
}, this.resumeDelay);
});
});
webflowArrows.forEach(arrow => {
arrow.addEventListener('click', () => {
this.handleUserInteraction();
// Let Webflow handle the navigation, then sync our state
setTimeout(() => {
this.syncWithWebflow();
// Schedule resume after inactivity
clearTimeout(this.resumeTimer);
this.resumeTimer = setTimeout(() => {
this.isUserInteracting = false;
this.resumeAutoplay();
}, this.resumeDelay);
}, 100);
});
});
// Hover events
if (this.pauseOnHover) {
this.slider.addEventListener('mouseenter', () => {
this.pauseAutoplay();
});
this.slider.addEventListener('mouseleave', () => {
this.isUserInteracting = false;
this.resumeAutoplay();
});
}
// Touch/swipe support (only if not handled by Webflow)
this.setupTouchEvents();
// Keyboard navigation
this.slider.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
this.handleUserInteraction();
this.previousSlide();
} else if (e.key === 'ArrowRight') {
this.handleUserInteraction();
this.nextSlide();
}
});
// Focus management
this.slider.addEventListener('focus', () => {
this.pauseAutoplay();
}, true);
this.slider.addEventListener('blur', () => {
if (!this.isUserInteracting) {
this.resumeAutoplay();
}
}, true);
}
setupTouchEvents() {
let startX = 0;
let currentX = 0;
let isDragging = false;
this.slider.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
isDragging = true;
this.pauseAutoplay();
});
this.slider.addEventListener('touchmove', (e) => {
if (!isDragging) return;
currentX = e.touches[0].clientX;
});
this.slider.addEventListener('touchend', () => {
if (!isDragging) return;
isDragging = false;
const deltaX = startX - currentX;
const threshold = 50;
if (Math.abs(deltaX) > threshold) {
this.handleUserInteraction();
if (deltaX > 0) {
this.nextSlide();
} else {
this.previousSlide();
}
}
});
}
handleUserInteraction() {
this.isUserInteracting = true;
this.pauseAutoplay();
// Clear any existing resume timer
clearTimeout(this.resumeTimer);
// Set timer to resume autoplay after period of inactivity
this.resumeTimer = setTimeout(() => {
this.isUserInteracting = false;
this.resumeAutoplay();
}, this.resumeDelay);
}
startAutoplay() {
if (this.slides.length <= 1) return;
if (this.autoplayTimer) return; // already running
if (this.autoplayOnVisible && !this.isInView) return; // respect visibility
this.autoplayTimer = setInterval(() => {
this.nextSlide();
}, this.interval);
this.isPaused = false;
}
pauseAutoplay() {
if (this.autoplayTimer) {
clearInterval(this.autoplayTimer);
this.autoplayTimer = null;
}
this.isPaused = true;
}
resumeAutoplay() {
if (!this.isPaused || this.isUserInteracting) return;
this.startAutoplay();
}
goToSlide(index) {
// Ensure index is within bounds
if (index < 0) {
this.currentSlide = this.slides.length - 1;
} else if (index >= this.slides.length) {
this.currentSlide = 0;
} else {
this.currentSlide = index;
}
// Use Webflow's native slider navigation if available
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
const webflowRight = this.slider.querySelector('.w-slider-arrow-right');
const webflowLeft = this.slider.querySelector('.w-slider-arrow-left');
if (webflowDots.length > 0) {
// Clicking dots is reliable for direct index navigation
const target = webflowDots[this.currentSlide];
if (target) target.click();
} else if (webflowRight && webflowLeft) {
// Fallback: use arrows to move stepwise toward target
const direction = this.currentSlide > 0 ? 1 : -1;
(direction > 0 ? webflowRight : webflowLeft).click();
} else {
// Fallback: calculate slide position manually
const slideWidth = this.slides[0].offsetWidth;
const translateX = -(this.currentSlide * slideWidth);
this.track.style.transform = `translateX(${translateX}px)`;
}
// Update active states
this.updateActiveStates();
// Trigger custom event
this.slider.dispatchEvent(new CustomEvent('slideChanged', {
detail: {
currentSlide: this.currentSlide,
totalSlides: this.slides.length
}
}));
// If the user has left the dots/nav area, ensure autoplay resumes after delay
if (!this.pauseOnHover && !this.isUserInteracting && !this.autoplayTimer) {
this.startAutoplay();
}
}
nextSlide() {
this.goToSlide(this.currentSlide + 1);
// If autoplay is running, keep it seamless after manual advance
if (!this.autoplayTimer && !this.isUserInteracting) {
this.startAutoplay();
}
}
previousSlide() {
this.goToSlide(this.currentSlide - 1);
if (!this.autoplayTimer && !this.isUserInteracting) {
this.startAutoplay();
}
}
updateActiveStates() {
// Update slides
this.slides.forEach((slide, index) => {
slide.classList.toggle('active', index === this.currentSlide);
slide.setAttribute('aria-hidden', index !== this.currentSlide);
});
// Update dots
this.dots.forEach((dot, index) => {
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) slideIndex = index;
const isActive = slideIndex === this.currentSlide;
// ARIA and data state
dot.setAttribute('aria-pressed', String(isActive));
if (isActive) {
dot.setAttribute('data-active', 'true');
} else {
dot.removeAttribute('data-active');
}
// Generic active class toggle (if they style it)
dot.classList.toggle('active', isActive);
// Optional custom classes provided via attributes
const activeClass = dot.dataset.msActiveClass || this.dotsActiveClass || this.defaultActiveClass;
const inactiveClass = dot.dataset.msInactiveClass || this.dotsInactiveClass || this.defaultInactiveClass;
if (activeClass) dot.classList.toggle(activeClass, isActive);
if (inactiveClass) dot.classList.toggle(inactiveClass, !isActive);
});
}
detectDotClasses() {
if (!this.dots || this.dots.length === 0) return;
// If classes are already provided via attributes, skip detection
if (this.dotsActiveClass || this.dotsInactiveClass) return;
// Find a class containing 'active' and 'inactive' among dot elements
const classCounts = new Map();
this.dots.forEach((dot) => {
dot.classList.forEach((cls) => {
classCounts.set(cls, (classCounts.get(cls) || 0) + 1);
});
});
// Prefer classes that explicitly include 'active'/'inactive'
const allClasses = Array.from(classCounts.keys());
const activeCandidate = allClasses.find((c) => /active/i.test(c));
const inactiveCandidate = allClasses.find((c) => /inactive/i.test(c));
if (activeCandidate) this.defaultActiveClass = activeCandidate;
if (inactiveCandidate) this.defaultInactiveClass = inactiveCandidate;
}
syncWithWebflow() {
// Sync our current slide with Webflow's active slide
const activeWebflowDot = this.slider.querySelector('.w-slider-dot.w-active');
if (activeWebflowDot) {
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
const activeIndex = Array.from(webflowDots).indexOf(activeWebflowDot);
if (activeIndex !== -1 && activeIndex !== this.currentSlide) {
this.currentSlide = activeIndex;
this.updateActiveStates();
}
}
}
collectDots() {
// If dots are not inside the slider, look for a sibling container in the same wrapper
if (!this.dots || this.dots.length === 0) {
let container = this.dotsContainer;
if (!container && this.slider.parentElement) {
container = this.slider.parentElement.querySelector('[data-ms-code="slider-dots"]');
}
if (!container) {
// Try the closest ancestor wrapper then find dots within it that are siblings
const wrapper = this.slider.closest('[data-ms-slider-wrapper], .feature-slider-wrapper, section, div');
if (wrapper) {
// Prefer immediate sibling dots container
const siblingDots = Array.from(wrapper.querySelectorAll('[data-ms-code="slider-dots"]'))
.find((el) => el !== this.slider);
if (siblingDots) container = siblingDots;
}
}
if (container) {
this.dotsContainer = container;
this.dots = container.querySelectorAll('[data-ms-code="slider-dot"]');
}
}
}
// No custom nav buttons
// Public methods for external control
pause() {
this.pauseAutoplay();
}
resume() {
this.isUserInteracting = false;
this.resumeAutoplay();
}
destroy() {
this.pauseAutoplay();
clearTimeout(this.resumeTimer);
if (this.visibilityObserver) {
try { this.visibilityObserver.disconnect(); } catch (e) {}
this.visibilityObserver = null;
}
// Remove event listeners would go here if needed
// This is a simplified version
}
setupVisibilityObserver() {
if (!('IntersectionObserver' in window)) {
// Fallback: start immediately
this.startAutoplay();
return;
}
const thresholds = [];
const step = 0.1;
for (let t = 0; t <= 1; t += step) thresholds.push(parseFloat(t.toFixed(1)));
if (!thresholds.includes(this.visibleThreshold)) thresholds.push(this.visibleThreshold);
thresholds.sort((a, b) => a - b);
this.visibilityObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
this.isInView = entry.isIntersecting && entry.intersectionRatio >= this.visibleThreshold;
if (this.isInView) {
// Resume/start autoplay only if not interacting
if (!this.isUserInteracting) {
this.startAutoplay();
}
} else {
this.pauseAutoplay();
}
});
}, { threshold: thresholds });
this.visibilityObserver.observe(this.slider);
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSliders);
} else {
initSliders();
}
// Expose the class globally for external access if needed
window.MemberScript169 = {
AutoSlider: AutoSlider,
init: initSliders
};
})();
</script>
<!-- 💙 MEMBERSCRIPT #169 v0.1 💙 - AUTOPLAY SLIDER WITH OPTIONAL MANUAL SELECTION -->
<script>
(function() {
'use strict';
// Wait for DOM to be ready
function initSliders() {
const sliders = document.querySelectorAll('[data-ms-code="auto-slider"]');
sliders.forEach(slider => {
new AutoSlider(slider);
});
}
class AutoSlider {
constructor(element) {
this.slider = element;
this.track = this.slider.querySelector('[data-ms-code="slider-track"]');
this.slides = this.slider.querySelectorAll('[data-ms-code="slider-slide"]');
this.dotsContainer = this.slider.querySelector('[data-ms-code="slider-dots"]');
this.dots = this.slider.querySelectorAll('[data-ms-code="slider-dot"]');
this.collectDots();
// Configuration
this.currentSlide = 0;
this.interval = parseInt(this.slider.dataset.msInterval) || 3000;
this.pauseOnHover = this.slider.dataset.msPauseOnHover !== 'false';
this.resumeDelay = parseInt(this.slider.dataset.msResumeDelay) || 3000;
this.autoplayOnVisible = this.slider.dataset.msAutoplayOnVisible === 'true';
this.visibleThreshold = Number.isNaN(parseFloat(this.slider.dataset.msVisibleThreshold))
? 0.3
: Math.min(1, Math.max(0, parseFloat(this.slider.dataset.msVisibleThreshold)));
this.dotsActiveClass = this.dotsContainer?.dataset.msDotActiveClass || '';
this.dotsInactiveClass = this.dotsContainer?.dataset.msDotInactiveClass || '';
this.defaultActiveClass = '';
this.defaultInactiveClass = '';
// State
this.autoplayTimer = null;
this.resumeTimer = null;
this.isUserInteracting = false;
this.isPaused = false;
this.isInView = false;
this.visibilityObserver = null;
// Validate required elements
if (!this.track || this.slides.length === 0) {
console.warn('AutoSlider: Required elements not found');
return;
}
this.init();
}
init() {
// Set up initial styles
this.setupStyles();
// Detect default dot classes from markup if no attributes provided
this.detectDotClasses();
// Sync with Webflow's current state
this.syncWithWebflow();
// Bind event listeners
this.bindEvents();
// Start autoplay (optionally only when visible)
if (this.autoplayOnVisible) {
this.setupVisibilityObserver();
} else {
this.startAutoplay();
}
console.log('AutoSlider initialized with', this.slides.length, 'slides');
}
setupStyles() {
// No CSS modifications - work with existing Webflow slider styles
// Only add data attributes to slides for tracking
this.slides.forEach((slide, index) => {
slide.dataset.slideIndex = index;
});
// Improve accessibility for custom dots without altering styles
if (this.dots && this.dots.length) {
this.dots.forEach((dot, index) => {
if (!dot.hasAttribute('role')) dot.setAttribute('role', 'button');
if (!dot.hasAttribute('tabindex')) dot.setAttribute('tabindex', '0');
if (!dot.hasAttribute('aria-label')) dot.setAttribute('aria-label', `Show slide ${index + 1}`);
// If data-ms-slide is missing, infer from position
if (!dot.dataset.msSlide) dot.dataset.msSlide = String(index);
});
}
}
bindEvents() {
// No custom prev/next controls
// Custom dot navigation (data-ms-code dots). Use event delegation if container exists.
if (this.dotsContainer) {
this.dotsContainer.addEventListener('click', (e) => {
const dot = e.target.closest('[data-ms-code="slider-dot"]');
if (!dot || !this.dotsContainer.contains(dot)) return;
e.preventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
// Recollect in case DOM changed
this.dots = this.dotsContainer.querySelectorAll('[data-ms-code="slider-dot"]');
slideIndex = Array.from(this.dots).indexOf(dot);
}
if (!Number.isNaN(slideIndex)) {
this.handleUserInteraction();
this.goToSlide(slideIndex);
}
});
}
// Also attach direct listeners to cover cases without a container
this.dots.forEach(dot => {
dot.addEventListener('click', (e) => {
e.preventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
slideIndex = Array.from(this.dots).indexOf(dot);
}
if (!Number.isNaN(slideIndex)) {
this.handleUserInteraction();
this.goToSlide(slideIndex);
}
});
// Keyboard support for custom dots
dot.addEventListener('keydown', (e) => {
const key = e.key;
if (key === 'Enter' || key === ' ') {
e.preventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
slideIndex = Array.from(this.dots).indexOf(dot);
}
if (!Number.isNaN(slideIndex)) {
this.handleUserInteraction();
this.goToSlide(slideIndex);
}
}
});
});
// Listen for Webflow slider interactions to pause autoplay
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
const webflowArrows = this.slider.querySelectorAll('.w-slider-arrow-left, .w-slider-arrow-right');
webflowDots.forEach(dot => {
dot.addEventListener('click', () => {
this.handleUserInteraction();
// Update our current slide based on Webflow's active dot
const activeDotIndex = Array.from(webflowDots).indexOf(dot);
if (activeDotIndex !== -1) {
this.currentSlide = activeDotIndex;
this.updateActiveStates();
}
// Schedule resume after inactivity
clearTimeout(this.resumeTimer);
this.resumeTimer = setTimeout(() => {
this.isUserInteracting = false;
this.resumeAutoplay();
}, this.resumeDelay);
});
});
webflowArrows.forEach(arrow => {
arrow.addEventListener('click', () => {
this.handleUserInteraction();
// Let Webflow handle the navigation, then sync our state
setTimeout(() => {
this.syncWithWebflow();
// Schedule resume after inactivity
clearTimeout(this.resumeTimer);
this.resumeTimer = setTimeout(() => {
this.isUserInteracting = false;
this.resumeAutoplay();
}, this.resumeDelay);
}, 100);
});
});
// Hover events
if (this.pauseOnHover) {
this.slider.addEventListener('mouseenter', () => {
this.pauseAutoplay();
});
this.slider.addEventListener('mouseleave', () => {
this.isUserInteracting = false;
this.resumeAutoplay();
});
}
// Touch/swipe support (only if not handled by Webflow)
this.setupTouchEvents();
// Keyboard navigation
this.slider.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
this.handleUserInteraction();
this.previousSlide();
} else if (e.key === 'ArrowRight') {
this.handleUserInteraction();
this.nextSlide();
}
});
// Focus management
this.slider.addEventListener('focus', () => {
this.pauseAutoplay();
}, true);
this.slider.addEventListener('blur', () => {
if (!this.isUserInteracting) {
this.resumeAutoplay();
}
}, true);
}
setupTouchEvents() {
let startX = 0;
let currentX = 0;
let isDragging = false;
this.slider.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
isDragging = true;
this.pauseAutoplay();
});
this.slider.addEventListener('touchmove', (e) => {
if (!isDragging) return;
currentX = e.touches[0].clientX;
});
this.slider.addEventListener('touchend', () => {
if (!isDragging) return;
isDragging = false;
const deltaX = startX - currentX;
const threshold = 50;
if (Math.abs(deltaX) > threshold) {
this.handleUserInteraction();
if (deltaX > 0) {
this.nextSlide();
} else {
this.previousSlide();
}
}
});
}
handleUserInteraction() {
this.isUserInteracting = true;
this.pauseAutoplay();
// Clear any existing resume timer
clearTimeout(this.resumeTimer);
// Set timer to resume autoplay after period of inactivity
this.resumeTimer = setTimeout(() => {
this.isUserInteracting = false;
this.resumeAutoplay();
}, this.resumeDelay);
}
startAutoplay() {
if (this.slides.length <= 1) return;
if (this.autoplayTimer) return; // already running
if (this.autoplayOnVisible && !this.isInView) return; // respect visibility
this.autoplayTimer = setInterval(() => {
this.nextSlide();
}, this.interval);
this.isPaused = false;
}
pauseAutoplay() {
if (this.autoplayTimer) {
clearInterval(this.autoplayTimer);
this.autoplayTimer = null;
}
this.isPaused = true;
}
resumeAutoplay() {
if (!this.isPaused || this.isUserInteracting) return;
this.startAutoplay();
}
goToSlide(index) {
// Ensure index is within bounds
if (index < 0) {
this.currentSlide = this.slides.length - 1;
} else if (index >= this.slides.length) {
this.currentSlide = 0;
} else {
this.currentSlide = index;
}
// Use Webflow's native slider navigation if available
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
const webflowRight = this.slider.querySelector('.w-slider-arrow-right');
const webflowLeft = this.slider.querySelector('.w-slider-arrow-left');
if (webflowDots.length > 0) {
// Clicking dots is reliable for direct index navigation
const target = webflowDots[this.currentSlide];
if (target) target.click();
} else if (webflowRight && webflowLeft) {
// Fallback: use arrows to move stepwise toward target
const direction = this.currentSlide > 0 ? 1 : -1;
(direction > 0 ? webflowRight : webflowLeft).click();
} else {
// Fallback: calculate slide position manually
const slideWidth = this.slides[0].offsetWidth;
const translateX = -(this.currentSlide * slideWidth);
this.track.style.transform = `translateX(${translateX}px)`;
}
// Update active states
this.updateActiveStates();
// Trigger custom event
this.slider.dispatchEvent(new CustomEvent('slideChanged', {
detail: {
currentSlide: this.currentSlide,
totalSlides: this.slides.length
}
}));
// If the user has left the dots/nav area, ensure autoplay resumes after delay
if (!this.pauseOnHover && !this.isUserInteracting && !this.autoplayTimer) {
this.startAutoplay();
}
}
nextSlide() {
this.goToSlide(this.currentSlide + 1);
// If autoplay is running, keep it seamless after manual advance
if (!this.autoplayTimer && !this.isUserInteracting) {
this.startAutoplay();
}
}
previousSlide() {
this.goToSlide(this.currentSlide - 1);
if (!this.autoplayTimer && !this.isUserInteracting) {
this.startAutoplay();
}
}
updateActiveStates() {
// Update slides
this.slides.forEach((slide, index) => {
slide.classList.toggle('active', index === this.currentSlide);
slide.setAttribute('aria-hidden', index !== this.currentSlide);
});
// Update dots
this.dots.forEach((dot, index) => {
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) slideIndex = index;
const isActive = slideIndex === this.currentSlide;
// ARIA and data state
dot.setAttribute('aria-pressed', String(isActive));
if (isActive) {
dot.setAttribute('data-active', 'true');
} else {
dot.removeAttribute('data-active');
}
// Generic active class toggle (if they style it)
dot.classList.toggle('active', isActive);
// Optional custom classes provided via attributes
const activeClass = dot.dataset.msActiveClass || this.dotsActiveClass || this.defaultActiveClass;
const inactiveClass = dot.dataset.msInactiveClass || this.dotsInactiveClass || this.defaultInactiveClass;
if (activeClass) dot.classList.toggle(activeClass, isActive);
if (inactiveClass) dot.classList.toggle(inactiveClass, !isActive);
});
}
detectDotClasses() {
if (!this.dots || this.dots.length === 0) return;
// If classes are already provided via attributes, skip detection
if (this.dotsActiveClass || this.dotsInactiveClass) return;
// Find a class containing 'active' and 'inactive' among dot elements
const classCounts = new Map();
this.dots.forEach((dot) => {
dot.classList.forEach((cls) => {
classCounts.set(cls, (classCounts.get(cls) || 0) + 1);
});
});
// Prefer classes that explicitly include 'active'/'inactive'
const allClasses = Array.from(classCounts.keys());
const activeCandidate = allClasses.find((c) => /active/i.test(c));
const inactiveCandidate = allClasses.find((c) => /inactive/i.test(c));
if (activeCandidate) this.defaultActiveClass = activeCandidate;
if (inactiveCandidate) this.defaultInactiveClass = inactiveCandidate;
}
syncWithWebflow() {
// Sync our current slide with Webflow's active slide
const activeWebflowDot = this.slider.querySelector('.w-slider-dot.w-active');
if (activeWebflowDot) {
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
const activeIndex = Array.from(webflowDots).indexOf(activeWebflowDot);
if (activeIndex !== -1 && activeIndex !== this.currentSlide) {
this.currentSlide = activeIndex;
this.updateActiveStates();
}
}
}
collectDots() {
// If dots are not inside the slider, look for a sibling container in the same wrapper
if (!this.dots || this.dots.length === 0) {
let container = this.dotsContainer;
if (!container && this.slider.parentElement) {
container = this.slider.parentElement.querySelector('[data-ms-code="slider-dots"]');
}
if (!container) {
// Try the closest ancestor wrapper then find dots within it that are siblings
const wrapper = this.slider.closest('[data-ms-slider-wrapper], .feature-slider-wrapper, section, div');
if (wrapper) {
// Prefer immediate sibling dots container
const siblingDots = Array.from(wrapper.querySelectorAll('[data-ms-code="slider-dots"]'))
.find((el) => el !== this.slider);
if (siblingDots) container = siblingDots;
}
}
if (container) {
this.dotsContainer = container;
this.dots = container.querySelectorAll('[data-ms-code="slider-dot"]');
}
}
}
// No custom nav buttons
// Public methods for external control
pause() {
this.pauseAutoplay();
}
resume() {
this.isUserInteracting = false;
this.resumeAutoplay();
}
destroy() {
this.pauseAutoplay();
clearTimeout(this.resumeTimer);
if (this.visibilityObserver) {
try { this.visibilityObserver.disconnect(); } catch (e) {}
this.visibilityObserver = null;
}
// Remove event listeners would go here if needed
// This is a simplified version
}
setupVisibilityObserver() {
if (!('IntersectionObserver' in window)) {
// Fallback: start immediately
this.startAutoplay();
return;
}
const thresholds = [];
const step = 0.1;
for (let t = 0; t <= 1; t += step) thresholds.push(parseFloat(t.toFixed(1)));
if (!thresholds.includes(this.visibleThreshold)) thresholds.push(this.visibleThreshold);
thresholds.sort((a, b) => a - b);
this.visibilityObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
this.isInView = entry.isIntersecting && entry.intersectionRatio >= this.visibleThreshold;
if (this.isInView) {
// Resume/start autoplay only if not interacting
if (!this.isUserInteracting) {
this.startAutoplay();
}
} else {
this.pauseAutoplay();
}
});
}, { threshold: thresholds });
this.visibilityObserver.observe(this.slider);
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSliders);
} else {
initSliders();
}
// Expose the class globally for external access if needed
window.MemberScript169 = {
AutoSlider: AutoSlider,
init: initSliders
};
})();
</script>

#168 - Save Trusted Devices
Save trusted devices to extend user sessions and reduce repeated logins on your sites.
<!-- 💙 MEMBERSCRIPT #168 v0.1 💙 - SAVE TRUSTED DEVICE -->
<script>
(function() {
const TRUST_EXPIRY_DAYS = 90;
const MAX_TRUSTED_DEVICES = 5;
const EXTENDED_SESSION_DAYS = 30;
function generateDeviceIdentifier() {
let id = localStorage.getItem('ms_device_id');
if (id) return id;
const info = {
ua: navigator.userAgent,
w: screen.width,
h: screen.height,
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
plat: navigator.platform
};
id = btoa(JSON.stringify(info)).slice(0, 32);
localStorage.setItem('ms_device_id', id);
return id;
}
async function getTrustedDevices() {
const ms = window.$memberstackDom;
const memberJson = await ms.getMemberJSON();
return Array.isArray(memberJson?.data?.trustedDevices) ? memberJson.data.trustedDevices : [];
}
async function saveTrustedDevices(devices) {
const ms = window.$memberstackDom;
const memberJson = await ms.getMemberJSON();
memberJson.data = memberJson.data || {};
memberJson.data.trustedDevices = devices;
await ms.updateMemberJSON({ json: memberJson });
}
async function addTrustedDevice(id, name) {
const now = new Date();
const expires = new Date(now.getTime() + TRUST_EXPIRY_DAYS * 864e5).toISOString();
const devices = await getTrustedDevices();
const existing = devices.find(d => d.id === id);
if (existing) {
existing.trustedAt = now.toISOString();
existing.expiresAt = expires;
} else {
if (devices.length >= MAX_TRUSTED_DEVICES) devices.shift();
devices.push({
id,
trustedAt: now.toISOString(),
expiresAt: expires,
ua: navigator.userAgent.slice(0, 100),
name: name
});
}
await saveTrustedDevices(devices);
}
function getDeviceName() {
const ua = navigator.userAgent;
if (ua.includes('iPhone')) return 'iPhone';
if (ua.includes('iPad')) return 'iPad';
if (ua.includes('Android')) return 'Android';
if (ua.includes('Mac')) return 'Mac';
if (ua.includes('Windows')) return 'Windows';
return 'Device';
}
function setExtendedSession() {
const exp = new Date();
exp.setDate(exp.getDate() + EXTENDED_SESSION_DAYS);
document.cookie = `trustedDevice=true; expires=${exp.toUTCString()}; path=/; SameSite=Strict`;
}
function showNotice() {
const el = document.querySelector('[data-ms-code="trust-device-notice"]');
if (!el) return;
el.style.display = 'block';
sessionStorage.setItem('ms_new_device_detected', '1');
sessionStorage.removeItem('ms_device_trusted');
}
function hideNotice() {
const el = document.querySelector('[data-ms-code="trust-device-notice"]');
if (el) el.style.display = 'none';
sessionStorage.removeItem('ms_new_device_detected');
sessionStorage.setItem('ms_device_trusted', '1');
}
function setupTrustBtn() {
document.addEventListener('click', async e => {
const btn = e.target.closest('[data-ms-code="trust-device-btn"]');
if (!btn) return;
e.preventDefault();
btn.disabled = true;
btn.innerText = 'Trusting Device...';
const member = await window.$memberstackDom.getCurrentMember();
if (!member?.data) {
alert('Please log in first.');
btn.disabled = false;
btn.innerText = 'Trust This Device';
return;
}
const id = generateDeviceIdentifier();
const name = getDeviceName();
await addTrustedDevice(id, name);
setExtendedSession();
btn.innerText = 'Device Trusted!';
setTimeout(hideNotice, 1000);
});
}
async function checkTrust() {
const member = await window.$memberstackDom.getCurrentMember();
if (!member) {
hideNotice();
return;
}
const id = generateDeviceIdentifier();
const devices = await getTrustedDevices();
// Check if current device is trusted
const trusted = devices.some(d => {
// Check if device ID matches and hasn't expired
if (d.id === id && new Date(d.expiresAt) > new Date()) {
return true;
}
// Also check by user agent for better matching
if (d.ua && d.ua.includes(navigator.userAgent.slice(0, 50)) && new Date(d.expiresAt) > new Date()) {
return true;
}
return false;
});
if (trusted) {
hideNotice();
setExtendedSession();
// Also store in sessionStorage to prevent showing on refresh
sessionStorage.setItem('ms_device_trusted', '1');
} else {
// Check if we already showed the notice in this session or have the cookie
if (sessionStorage.getItem('ms_device_trusted') === '1' ||
document.cookie.includes('trustedDevice=true')) {
hideNotice();
} else {
showNotice();
}
}
}
function preventRedirect() {
window.addEventListener('ms:member:will-redirect', e => {
if (sessionStorage.getItem('ms_new_device_detected') === '1') {
e.preventDefault();
}
});
}
function init() {
// Immediately hide notice if device is already trusted in this session
if (sessionStorage.getItem('ms_device_trusted') === '1' ||
document.cookie.includes('trustedDevice=true')) {
hideNotice();
}
if (window.$memberstackDom?.getCurrentMember) {
setupTrustBtn();
preventRedirect();
window.addEventListener('ms:member:login', () => setTimeout(checkTrust, 1000));
window.addEventListener('ms:member:info-changed', checkTrust);
checkTrust();
} else {
setTimeout(init, 500);
}
}
document.addEventListener('DOMContentLoaded', init);
})();
</script>
<!-- 💙 MEMBERSCRIPT #168 v0.1 💙 - SAVE TRUSTED DEVICE -->
<script>
(function() {
const TRUST_EXPIRY_DAYS = 90;
const MAX_TRUSTED_DEVICES = 5;
const EXTENDED_SESSION_DAYS = 30;
function generateDeviceIdentifier() {
let id = localStorage.getItem('ms_device_id');
if (id) return id;
const info = {
ua: navigator.userAgent,
w: screen.width,
h: screen.height,
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
plat: navigator.platform
};
id = btoa(JSON.stringify(info)).slice(0, 32);
localStorage.setItem('ms_device_id', id);
return id;
}
async function getTrustedDevices() {
const ms = window.$memberstackDom;
const memberJson = await ms.getMemberJSON();
return Array.isArray(memberJson?.data?.trustedDevices) ? memberJson.data.trustedDevices : [];
}
async function saveTrustedDevices(devices) {
const ms = window.$memberstackDom;
const memberJson = await ms.getMemberJSON();
memberJson.data = memberJson.data || {};
memberJson.data.trustedDevices = devices;
await ms.updateMemberJSON({ json: memberJson });
}
async function addTrustedDevice(id, name) {
const now = new Date();
const expires = new Date(now.getTime() + TRUST_EXPIRY_DAYS * 864e5).toISOString();
const devices = await getTrustedDevices();
const existing = devices.find(d => d.id === id);
if (existing) {
existing.trustedAt = now.toISOString();
existing.expiresAt = expires;
} else {
if (devices.length >= MAX_TRUSTED_DEVICES) devices.shift();
devices.push({
id,
trustedAt: now.toISOString(),
expiresAt: expires,
ua: navigator.userAgent.slice(0, 100),
name: name
});
}
await saveTrustedDevices(devices);
}
function getDeviceName() {
const ua = navigator.userAgent;
if (ua.includes('iPhone')) return 'iPhone';
if (ua.includes('iPad')) return 'iPad';
if (ua.includes('Android')) return 'Android';
if (ua.includes('Mac')) return 'Mac';
if (ua.includes('Windows')) return 'Windows';
return 'Device';
}
function setExtendedSession() {
const exp = new Date();
exp.setDate(exp.getDate() + EXTENDED_SESSION_DAYS);
document.cookie = `trustedDevice=true; expires=${exp.toUTCString()}; path=/; SameSite=Strict`;
}
function showNotice() {
const el = document.querySelector('[data-ms-code="trust-device-notice"]');
if (!el) return;
el.style.display = 'block';
sessionStorage.setItem('ms_new_device_detected', '1');
sessionStorage.removeItem('ms_device_trusted');
}
function hideNotice() {
const el = document.querySelector('[data-ms-code="trust-device-notice"]');
if (el) el.style.display = 'none';
sessionStorage.removeItem('ms_new_device_detected');
sessionStorage.setItem('ms_device_trusted', '1');
}
function setupTrustBtn() {
document.addEventListener('click', async e => {
const btn = e.target.closest('[data-ms-code="trust-device-btn"]');
if (!btn) return;
e.preventDefault();
btn.disabled = true;
btn.innerText = 'Trusting Device...';
const member = await window.$memberstackDom.getCurrentMember();
if (!member?.data) {
alert('Please log in first.');
btn.disabled = false;
btn.innerText = 'Trust This Device';
return;
}
const id = generateDeviceIdentifier();
const name = getDeviceName();
await addTrustedDevice(id, name);
setExtendedSession();
btn.innerText = 'Device Trusted!';
setTimeout(hideNotice, 1000);
});
}
async function checkTrust() {
const member = await window.$memberstackDom.getCurrentMember();
if (!member) {
hideNotice();
return;
}
const id = generateDeviceIdentifier();
const devices = await getTrustedDevices();
// Check if current device is trusted
const trusted = devices.some(d => {
// Check if device ID matches and hasn't expired
if (d.id === id && new Date(d.expiresAt) > new Date()) {
return true;
}
// Also check by user agent for better matching
if (d.ua && d.ua.includes(navigator.userAgent.slice(0, 50)) && new Date(d.expiresAt) > new Date()) {
return true;
}
return false;
});
if (trusted) {
hideNotice();
setExtendedSession();
// Also store in sessionStorage to prevent showing on refresh
sessionStorage.setItem('ms_device_trusted', '1');
} else {
// Check if we already showed the notice in this session or have the cookie
if (sessionStorage.getItem('ms_device_trusted') === '1' ||
document.cookie.includes('trustedDevice=true')) {
hideNotice();
} else {
showNotice();
}
}
}
function preventRedirect() {
window.addEventListener('ms:member:will-redirect', e => {
if (sessionStorage.getItem('ms_new_device_detected') === '1') {
e.preventDefault();
}
});
}
function init() {
// Immediately hide notice if device is already trusted in this session
if (sessionStorage.getItem('ms_device_trusted') === '1' ||
document.cookie.includes('trustedDevice=true')) {
hideNotice();
}
if (window.$memberstackDom?.getCurrentMember) {
setupTrustBtn();
preventRedirect();
window.addEventListener('ms:member:login', () => setTimeout(checkTrust, 1000));
window.addEventListener('ms:member:info-changed', checkTrust);
checkTrust();
} else {
setTimeout(init, 500);
}
}
document.addEventListener('DOMContentLoaded', init);
})();
</script>

#167 - Login Form Throttle With Security Check
Limit failed login attempts and trigger a timed security check to prevent brute force attacks.
<!-- 💙 MEMBERSCRIPT #167 v0.1 💙 - LOGIN THROTTLE WITH SECURITY CHECK -->
<script>
(function() {
const MAX_ATTEMPTS = 3;
const STORAGE_KEY = 'ms_login_attempts';
const SECURITY_DELAY = 30; // seconds
const formWrapper = document.querySelector('[data-ms-code="login-throttle-form"]');
const submitButton = document.querySelector('[data-ms-code="throttle-submit"]');
const errorMessage = document.querySelector('[data-ms-code="throttle-error"]');
const attemptCounter = document.querySelector('[data-ms-code="attempt-counter"]');
const loginForm = formWrapper?.querySelector('[data-ms-form="login"]');
if (!formWrapper || !submitButton || !loginForm) {
console.warn('MemberScript #167: Required elements not found.');
return;
}
function getAttemptData() {
const stored = sessionStorage.getItem(STORAGE_KEY);
if (!stored) return { count: 0, timestamp: 0 };
try {
return JSON.parse(stored);
} catch {
return { count: 0, timestamp: 0 };
}
}
function setAttemptData(count, timestamp = Date.now()) {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ count, timestamp }));
}
let attemptData = getAttemptData();
let securityTimer = null;
function updateUIState() {
const remainingAttempts = MAX_ATTEMPTS - attemptData.count;
// Don't update UI if security timer is running
if (securityTimer) {
return;
}
if (attemptData.count >= MAX_ATTEMPTS) {
showSecurityCheck();
showError('Too many failed attempts. Please wait for security verification.');
} else {
hideSecurityCheck();
if (attemptData.count > 0) {
showError(`Login failed. ${remainingAttempts} attempt${remainingAttempts === 1 ? '' : 's'} remaining.`);
} else {
hideError();
}
}
if (attemptCounter) {
if (attemptData.count >= MAX_ATTEMPTS) {
attemptCounter.textContent = 'Security verification required';
attemptCounter.style.color = '#e74c3c';
} else if (attemptData.count > 0) {
attemptCounter.textContent = `${remainingAttempts} attempt${remainingAttempts === 1 ? '' : 's'} remaining`;
attemptCounter.style.color = attemptData.count >= 2 ? '#e67e22' : '#95a5a6';
} else {
attemptCounter.textContent = '';
}
}
console.log(`UI State: ${attemptData.count}/${MAX_ATTEMPTS} attempts, security timer: ${securityTimer ? 'active' : 'inactive'}`);
}
function showSecurityCheck() {
let securityBox = formWrapper.querySelector('[data-ms-security-check]');
if (!securityBox) {
securityBox = document.createElement('div');
securityBox.setAttribute('data-ms-security-check', 'true');
securityBox.style.cssText = `
width: 100%;
margin: 15px 0;
padding: 20px;
border: 2px solid #ff6b6b;
border-radius: 8px;
background: #fff5f5;
text-align: center;
box-sizing: border-box;
`;
submitButton.parentNode.insertBefore(securityBox, submitButton);
}
securityBox.innerHTML = `
<strong>Security Check</strong><br>
<small>Please wait ${SECURITY_DELAY} seconds before trying again</small><br>
<div id="security-countdown" style="margin-top: 10px; font-size: 24px; font-weight: bold; color: #ff6b6b;">${SECURITY_DELAY}</div>
`;
// Disable submit button
submitButton.disabled = true;
submitButton.style.opacity = '0.5';
// Clear any existing timer
if (securityTimer) {
clearInterval(securityTimer);
}
// Start countdown
let timeLeft = SECURITY_DELAY;
securityTimer = setInterval(() => {
timeLeft--;
const countdown = securityBox.querySelector('#security-countdown');
if (countdown) {
countdown.textContent = timeLeft;
}
if (timeLeft <= 0) {
clearInterval(securityTimer);
securityTimer = null;
securityBox.innerHTML = `
<strong style="color: #27ae60;">✓ Security Check Complete</strong><br>
<small>You may now try logging in again</small>
`;
// Re-enable submit button
submitButton.disabled = false;
submitButton.style.opacity = '1';
// Reset attempt count so user gets fresh attempts
attemptData.count = 0;
setAttemptData(0);
// Update UI to reflect fresh state
updateUIState();
// Hide security box after 5 seconds (longer so user sees message)
setTimeout(() => {
if (securityBox) {
securityBox.style.display = 'none';
}
}, 5000);
}
}, 1000);
}
function hideSecurityCheck() {
const securityBox = formWrapper.querySelector('[data-ms-security-check]');
if (securityBox && !securityTimer) {
securityBox.remove();
}
if (securityTimer) {
clearInterval(securityTimer);
securityTimer = null;
}
// Only enable button if we're not in security check mode
if (attemptData.count < MAX_ATTEMPTS) {
submitButton.disabled = false;
submitButton.style.opacity = '1';
}
}
function showError(message) {
if (errorMessage) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
}
}
function hideError() {
if (errorMessage) {
errorMessage.style.display = 'none';
}
}
function handleSubmit(event) {
// Prevent submission if security check is active
if (attemptData.count >= MAX_ATTEMPTS && securityTimer) {
event.preventDefault();
showError('Please wait for the security check to complete.');
return false;
}
const currentAttemptCount = attemptData.count;
setTimeout(() => {
checkLoginResult(currentAttemptCount);
}, 1500);
}
function checkLoginResult(previousAttemptCount) {
const hasError = document.querySelector('[data-ms-error]') ||
document.querySelector('.w-form-fail:not([style*="display: none"])') ||
formWrapper.querySelector('.w-form-fail:not([style*="display: none"])') ||
loginForm.querySelector('[data-ms-error]');
if (window.$memberstackDom) {
window.$memberstackDom.getCurrentMember().then(member => {
if (member && member.id) {
// Success! Reset everything
sessionStorage.removeItem(STORAGE_KEY);
attemptData = { count: 0, timestamp: 0 };
hideError();
hideSecurityCheck();
if (attemptCounter) {
attemptCounter.textContent = 'Login successful!';
attemptCounter.style.color = '#27ae60';
}
} else if (hasError) {
handleFailedLogin(previousAttemptCount);
}
}).catch(() => {
if (hasError) {
handleFailedLogin(previousAttemptCount);
}
});
} else {
if (hasError) {
handleFailedLogin(previousAttemptCount);
} else {
const successElement = document.querySelector('.w-form-done:not([style*="display: none"])');
if (successElement) {
sessionStorage.removeItem(STORAGE_KEY);
attemptData = { count: 0, timestamp: 0 };
hideError();
hideSecurityCheck();
}
}
}
}
function handleFailedLogin(previousAttemptCount) {
attemptData.count = previousAttemptCount + 1;
setAttemptData(attemptData.count);
console.log(`Failed login attempt ${attemptData.count}/${MAX_ATTEMPTS}`);
// Force UI update after a brief delay to ensure DOM is ready
setTimeout(() => {
updateUIState();
}, 100);
}
function init() {
loginForm.addEventListener('submit', handleSubmit);
updateUIState();
if (window.$memberstackDom) {
window.$memberstackDom.getCurrentMember().then(member => {
if (member && member.id) {
sessionStorage.removeItem(STORAGE_KEY);
attemptData = { count: 0, timestamp: 0 };
}
}).catch(() => {
// No user logged in
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #167 v0.1 💙 - LOGIN THROTTLE WITH SECURITY CHECK -->
<script>
(function() {
const MAX_ATTEMPTS = 3;
const STORAGE_KEY = 'ms_login_attempts';
const SECURITY_DELAY = 30; // seconds
const formWrapper = document.querySelector('[data-ms-code="login-throttle-form"]');
const submitButton = document.querySelector('[data-ms-code="throttle-submit"]');
const errorMessage = document.querySelector('[data-ms-code="throttle-error"]');
const attemptCounter = document.querySelector('[data-ms-code="attempt-counter"]');
const loginForm = formWrapper?.querySelector('[data-ms-form="login"]');
if (!formWrapper || !submitButton || !loginForm) {
console.warn('MemberScript #167: Required elements not found.');
return;
}
function getAttemptData() {
const stored = sessionStorage.getItem(STORAGE_KEY);
if (!stored) return { count: 0, timestamp: 0 };
try {
return JSON.parse(stored);
} catch {
return { count: 0, timestamp: 0 };
}
}
function setAttemptData(count, timestamp = Date.now()) {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ count, timestamp }));
}
let attemptData = getAttemptData();
let securityTimer = null;
function updateUIState() {
const remainingAttempts = MAX_ATTEMPTS - attemptData.count;
// Don't update UI if security timer is running
if (securityTimer) {
return;
}
if (attemptData.count >= MAX_ATTEMPTS) {
showSecurityCheck();
showError('Too many failed attempts. Please wait for security verification.');
} else {
hideSecurityCheck();
if (attemptData.count > 0) {
showError(`Login failed. ${remainingAttempts} attempt${remainingAttempts === 1 ? '' : 's'} remaining.`);
} else {
hideError();
}
}
if (attemptCounter) {
if (attemptData.count >= MAX_ATTEMPTS) {
attemptCounter.textContent = 'Security verification required';
attemptCounter.style.color = '#e74c3c';
} else if (attemptData.count > 0) {
attemptCounter.textContent = `${remainingAttempts} attempt${remainingAttempts === 1 ? '' : 's'} remaining`;
attemptCounter.style.color = attemptData.count >= 2 ? '#e67e22' : '#95a5a6';
} else {
attemptCounter.textContent = '';
}
}
console.log(`UI State: ${attemptData.count}/${MAX_ATTEMPTS} attempts, security timer: ${securityTimer ? 'active' : 'inactive'}`);
}
function showSecurityCheck() {
let securityBox = formWrapper.querySelector('[data-ms-security-check]');
if (!securityBox) {
securityBox = document.createElement('div');
securityBox.setAttribute('data-ms-security-check', 'true');
securityBox.style.cssText = `
width: 100%;
margin: 15px 0;
padding: 20px;
border: 2px solid #ff6b6b;
border-radius: 8px;
background: #fff5f5;
text-align: center;
box-sizing: border-box;
`;
submitButton.parentNode.insertBefore(securityBox, submitButton);
}
securityBox.innerHTML = `
<strong>Security Check</strong><br>
<small>Please wait ${SECURITY_DELAY} seconds before trying again</small><br>
<div id="security-countdown" style="margin-top: 10px; font-size: 24px; font-weight: bold; color: #ff6b6b;">${SECURITY_DELAY}</div>
`;
// Disable submit button
submitButton.disabled = true;
submitButton.style.opacity = '0.5';
// Clear any existing timer
if (securityTimer) {
clearInterval(securityTimer);
}
// Start countdown
let timeLeft = SECURITY_DELAY;
securityTimer = setInterval(() => {
timeLeft--;
const countdown = securityBox.querySelector('#security-countdown');
if (countdown) {
countdown.textContent = timeLeft;
}
if (timeLeft <= 0) {
clearInterval(securityTimer);
securityTimer = null;
securityBox.innerHTML = `
<strong style="color: #27ae60;">✓ Security Check Complete</strong><br>
<small>You may now try logging in again</small>
`;
// Re-enable submit button
submitButton.disabled = false;
submitButton.style.opacity = '1';
// Reset attempt count so user gets fresh attempts
attemptData.count = 0;
setAttemptData(0);
// Update UI to reflect fresh state
updateUIState();
// Hide security box after 5 seconds (longer so user sees message)
setTimeout(() => {
if (securityBox) {
securityBox.style.display = 'none';
}
}, 5000);
}
}, 1000);
}
function hideSecurityCheck() {
const securityBox = formWrapper.querySelector('[data-ms-security-check]');
if (securityBox && !securityTimer) {
securityBox.remove();
}
if (securityTimer) {
clearInterval(securityTimer);
securityTimer = null;
}
// Only enable button if we're not in security check mode
if (attemptData.count < MAX_ATTEMPTS) {
submitButton.disabled = false;
submitButton.style.opacity = '1';
}
}
function showError(message) {
if (errorMessage) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
}
}
function hideError() {
if (errorMessage) {
errorMessage.style.display = 'none';
}
}
function handleSubmit(event) {
// Prevent submission if security check is active
if (attemptData.count >= MAX_ATTEMPTS && securityTimer) {
event.preventDefault();
showError('Please wait for the security check to complete.');
return false;
}
const currentAttemptCount = attemptData.count;
setTimeout(() => {
checkLoginResult(currentAttemptCount);
}, 1500);
}
function checkLoginResult(previousAttemptCount) {
const hasError = document.querySelector('[data-ms-error]') ||
document.querySelector('.w-form-fail:not([style*="display: none"])') ||
formWrapper.querySelector('.w-form-fail:not([style*="display: none"])') ||
loginForm.querySelector('[data-ms-error]');
if (window.$memberstackDom) {
window.$memberstackDom.getCurrentMember().then(member => {
if (member && member.id) {
// Success! Reset everything
sessionStorage.removeItem(STORAGE_KEY);
attemptData = { count: 0, timestamp: 0 };
hideError();
hideSecurityCheck();
if (attemptCounter) {
attemptCounter.textContent = 'Login successful!';
attemptCounter.style.color = '#27ae60';
}
} else if (hasError) {
handleFailedLogin(previousAttemptCount);
}
}).catch(() => {
if (hasError) {
handleFailedLogin(previousAttemptCount);
}
});
} else {
if (hasError) {
handleFailedLogin(previousAttemptCount);
} else {
const successElement = document.querySelector('.w-form-done:not([style*="display: none"])');
if (successElement) {
sessionStorage.removeItem(STORAGE_KEY);
attemptData = { count: 0, timestamp: 0 };
hideError();
hideSecurityCheck();
}
}
}
}
function handleFailedLogin(previousAttemptCount) {
attemptData.count = previousAttemptCount + 1;
setAttemptData(attemptData.count);
console.log(`Failed login attempt ${attemptData.count}/${MAX_ATTEMPTS}`);
// Force UI update after a brief delay to ensure DOM is ready
setTimeout(() => {
updateUIState();
}, 100);
}
function init() {
loginForm.addEventListener('submit', handleSubmit);
updateUIState();
if (window.$memberstackDom) {
window.$memberstackDom.getCurrentMember().then(member => {
if (member && member.id) {
sessionStorage.removeItem(STORAGE_KEY);
attemptData = { count: 0, timestamp: 0 };
}
}).catch(() => {
// No user logged in
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>

#166 - Show or Hide Content Based on Regions
Show or hide Webflow content based on a visitor’s country using simple data attributes.
<!-- 💙 MEMBERSCRIPT #166 v0.1 💙 - GEO‑GATED REGION BLOCKER (Stable Version) -->
<script>
(function () {
// STEP 1: Store and remove locale-show elements
const localeShowElements = [];
document.querySelectorAll('[data-ms-code="locale-show"]').forEach(el => {
localeShowElements.push({
el,
parent: el.parentNode,
next: el.nextSibling
});
el.remove();
});
// STEP 2: Store and remove locale-hide elements
const localeHideElements = [];
document.querySelectorAll('[data-ms-code="locale-hide"]').forEach(el => {
localeHideElements.push({
el,
parent: el.parentNode,
next: el.nextSibling
});
el.remove();
});
// STEP 3: Safe DOM reinsertion
function safeInsert(parent, el, next) {
if (next && parent.contains(next)) {
parent.insertBefore(el, next);
} else {
parent.appendChild(el);
}
}
// STEP 4: Get country using two fallback-safe APIs
async function getUserCountry() {
try {
const res = await fetch('https://api.country.is/');
const data = await res.json();
if (data && data.country) return data.country.toUpperCase();
} catch (err1) {
try {
const res = await fetch('https://ipwho.is/');
const data = await res.json();
if (data && data.success && data.country_code) {
return data.country_code.toUpperCase();
}
} catch (err2) {
console.error('Geolocation failed:', err2);
}
}
return null;
}
// STEP 5: Run logic after detecting country
getUserCountry().then(userCountry => {
if (!userCountry) return;
// Show if user's country is allowed
localeShowElements.forEach(({ el, parent, next }) => {
const allowed = (el.getAttribute('data-ms-countries') || '')
.split(',')
.map(c => c.trim().toUpperCase());
if (allowed.includes(userCountry)) {
safeInsert(parent, el, next);
}
});
// Hide if user's country is blocked
localeHideElements.forEach(({ el, parent, next }) => {
const blocked = (el.getAttribute('data-ms-countries') || '')
.split(',')
.map(c => c.trim().toUpperCase());
if (!blocked.includes(userCountry)) {
safeInsert(parent, el, next);
}
});
});
})();
</script>
<!-- 💙 MEMBERSCRIPT #166 v0.1 💙 - GEO‑GATED REGION BLOCKER (Stable Version) -->
<script>
(function () {
// STEP 1: Store and remove locale-show elements
const localeShowElements = [];
document.querySelectorAll('[data-ms-code="locale-show"]').forEach(el => {
localeShowElements.push({
el,
parent: el.parentNode,
next: el.nextSibling
});
el.remove();
});
// STEP 2: Store and remove locale-hide elements
const localeHideElements = [];
document.querySelectorAll('[data-ms-code="locale-hide"]').forEach(el => {
localeHideElements.push({
el,
parent: el.parentNode,
next: el.nextSibling
});
el.remove();
});
// STEP 3: Safe DOM reinsertion
function safeInsert(parent, el, next) {
if (next && parent.contains(next)) {
parent.insertBefore(el, next);
} else {
parent.appendChild(el);
}
}
// STEP 4: Get country using two fallback-safe APIs
async function getUserCountry() {
try {
const res = await fetch('https://api.country.is/');
const data = await res.json();
if (data && data.country) return data.country.toUpperCase();
} catch (err1) {
try {
const res = await fetch('https://ipwho.is/');
const data = await res.json();
if (data && data.success && data.country_code) {
return data.country_code.toUpperCase();
}
} catch (err2) {
console.error('Geolocation failed:', err2);
}
}
return null;
}
// STEP 5: Run logic after detecting country
getUserCountry().then(userCountry => {
if (!userCountry) return;
// Show if user's country is allowed
localeShowElements.forEach(({ el, parent, next }) => {
const allowed = (el.getAttribute('data-ms-countries') || '')
.split(',')
.map(c => c.trim().toUpperCase());
if (allowed.includes(userCountry)) {
safeInsert(parent, el, next);
}
});
// Hide if user's country is blocked
localeHideElements.forEach(({ el, parent, next }) => {
const blocked = (el.getAttribute('data-ms-countries') || '')
.split(',')
.map(c => c.trim().toUpperCase());
if (!blocked.includes(userCountry)) {
safeInsert(parent, el, next);
}
});
});
})();
</script>

#165 - Typing Animation in a Search Bar
Create an animated typing effect in search bar placeholders that cycles through custom suggestions.
<!-- 💙 MEMBERSCRIPT #165 v0.1 💙 - TYPING ANIMATION IN A FUNCTIONAL SEARCH BAR -->
<script>
(function() {
const searchInput = document.querySelector('[data-ms-code="search-bar"]');
if (!searchInput) return;
const suggestions = [];
for (let i = 1; i <= 5; i++) {
const suggestion = searchInput.getAttribute(`data-ms-suggestion-${i}`);
if (suggestion) suggestions.push(suggestion);
}
if (suggestions.length === 0) return;
let suggestionIndex = 0;
let charIndex = 0;
let typing;
let isAnimating = false;
let originalPlaceholder = searchInput.placeholder || '';
function typeSuggestion() {
if (!isAnimating) return;
const current = suggestions[suggestionIndex];
searchInput.placeholder = current.slice(0, charIndex++);
if (charIndex <= current.length) {
typing = setTimeout(typeSuggestion, 80);
} else {
setTimeout(eraseSuggestion, 1200);
}
}
function eraseSuggestion() {
if (!isAnimating) return;
const current = suggestions[suggestionIndex];
searchInput.placeholder = current.slice(0, --charIndex);
if (charIndex > 0) {
typing = setTimeout(eraseSuggestion, 40);
} else {
suggestionIndex = (suggestionIndex + 1) % suggestions.length;
setTimeout(typeSuggestion, 500);
}
}
function stopAnimation() {
isAnimating = false;
clearTimeout(typing);
searchInput.placeholder = originalPlaceholder;
}
function startAnimation() {
if (searchInput.value.trim() !== '') return;
isAnimating = true;
charIndex = 0;
typeSuggestion();
}
// Event listeners
searchInput.addEventListener("focus", stopAnimation);
searchInput.addEventListener("blur", () => {
if (searchInput.value.trim() === '') {
setTimeout(startAnimation, 500);
}
});
searchInput.addEventListener("input", () => {
if (isAnimating) stopAnimation();
});
// Start animation after delay
setTimeout(() => {
if (searchInput.value.trim() === '' && document.activeElement !== searchInput) {
startAnimation();
}
}, 2000);
})();
</script>
<!-- 💙 MEMBERSCRIPT #165 v0.1 💙 - TYPING ANIMATION IN A FUNCTIONAL SEARCH BAR -->
<script>
(function() {
const searchInput = document.querySelector('[data-ms-code="search-bar"]');
if (!searchInput) return;
const suggestions = [];
for (let i = 1; i <= 5; i++) {
const suggestion = searchInput.getAttribute(`data-ms-suggestion-${i}`);
if (suggestion) suggestions.push(suggestion);
}
if (suggestions.length === 0) return;
let suggestionIndex = 0;
let charIndex = 0;
let typing;
let isAnimating = false;
let originalPlaceholder = searchInput.placeholder || '';
function typeSuggestion() {
if (!isAnimating) return;
const current = suggestions[suggestionIndex];
searchInput.placeholder = current.slice(0, charIndex++);
if (charIndex <= current.length) {
typing = setTimeout(typeSuggestion, 80);
} else {
setTimeout(eraseSuggestion, 1200);
}
}
function eraseSuggestion() {
if (!isAnimating) return;
const current = suggestions[suggestionIndex];
searchInput.placeholder = current.slice(0, --charIndex);
if (charIndex > 0) {
typing = setTimeout(eraseSuggestion, 40);
} else {
suggestionIndex = (suggestionIndex + 1) % suggestions.length;
setTimeout(typeSuggestion, 500);
}
}
function stopAnimation() {
isAnimating = false;
clearTimeout(typing);
searchInput.placeholder = originalPlaceholder;
}
function startAnimation() {
if (searchInput.value.trim() !== '') return;
isAnimating = true;
charIndex = 0;
typeSuggestion();
}
// Event listeners
searchInput.addEventListener("focus", stopAnimation);
searchInput.addEventListener("blur", () => {
if (searchInput.value.trim() === '') {
setTimeout(startAnimation, 500);
}
});
searchInput.addEventListener("input", () => {
if (isAnimating) stopAnimation();
});
// Start animation after delay
setTimeout(() => {
if (searchInput.value.trim() === '' && document.activeElement !== searchInput) {
startAnimation();
}
}, 2000);
})();
</script>

#164 - Animated Chat Conversation Layout
This script adds an engaging animated chat layout in Webflow, sequentially displaying messages.
<!-- 💙 MEMBERSCRIPT #164 v0.1 💙 - ANIMATED CHAT CONVERSATION LAYOUT -->
<script>
(function() {
// Main animation function
function animateChat() {
const container = document.querySelector('[data-ms-code="chat-container"]');
if (!container) return;
const messages = Array.from(container.querySelectorAll('[data-ms-code="chat-message"]'));
const button = container.querySelector('[data-ms-code="chat-button"]');
let i = 0;
// Reset any previous visibility
messages.forEach(msg => msg.classList.remove('visible'));
if (button) button.classList.remove('visible');
function showNext() {
if (i < messages.length) {
messages[i].classList.add('visible');
i++;
setTimeout(showNext, 500); // next message in 500ms
} else {
if (button) button.classList.add('visible');
/* ➕ LOOPING CODE BEGINS HERE */
setTimeout(() => {
messages.forEach(msg => msg.classList.remove('visible'));
if (button) button.classList.remove('visible');
animateChat(); // 👈 Recursive call to restart the animation
}, 2000);
/* To STOP looping: remove or comment out everything from this setTimeout block */
}
}
showNext();
}
// Start animation as soon as DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', animateChat);
} else {
animateChat();
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #164 v0.1 💙 - ANIMATED CHAT CONVERSATION LAYOUT -->
<script>
(function() {
// Main animation function
function animateChat() {
const container = document.querySelector('[data-ms-code="chat-container"]');
if (!container) return;
const messages = Array.from(container.querySelectorAll('[data-ms-code="chat-message"]'));
const button = container.querySelector('[data-ms-code="chat-button"]');
let i = 0;
// Reset any previous visibility
messages.forEach(msg => msg.classList.remove('visible'));
if (button) button.classList.remove('visible');
function showNext() {
if (i < messages.length) {
messages[i].classList.add('visible');
i++;
setTimeout(showNext, 500); // next message in 500ms
} else {
if (button) button.classList.add('visible');
/* ➕ LOOPING CODE BEGINS HERE */
setTimeout(() => {
messages.forEach(msg => msg.classList.remove('visible'));
if (button) button.classList.remove('visible');
animateChat(); // 👈 Recursive call to restart the animation
}, 2000);
/* To STOP looping: remove or comment out everything from this setTimeout block */
}
}
showNext();
}
// Start animation as soon as DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', animateChat);
} else {
animateChat();
}
})();
</script>

#163 - Text Shuffle On Hover
Adds a playful text shuffle animation on hover—scrambling letters briefly before snapping back.
<!-- 💙 MEMBERSCRIPT #163 v0.1 💙 - TEXT SHUFFLE ON HOVER -->
<script>
(function() {
// Helper: Shuffle the characters in a string
function shuffle(str) {
var arr = str.split('');
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr.join('');
}
// Find all elements with data-ms-code="shuffle-text"
var elements = document.querySelectorAll('[data-ms-code="shuffle-text"]');
elements.forEach(function(el) {
var originalText = el.textContent;
var interval = null;
var duration = 600; // ms
var shuffleSpeed = 50; // ms
el.addEventListener('mouseenter', function() {
var start = Date.now();
clearInterval(interval);
interval = setInterval(function() {
if (Date.now() - start > duration) {
clearInterval(interval);
el.textContent = originalText;
} else {
el.textContent = shuffle(originalText);
}
}, shuffleSpeed);
});
el.addEventListener('mouseleave', function() {
clearInterval(interval);
el.textContent = originalText;
});
});
})();
</script>
<!-- 💙 MEMBERSCRIPT #163 v0.1 💙 - TEXT SHUFFLE ON HOVER -->
<script>
(function() {
// Helper: Shuffle the characters in a string
function shuffle(str) {
var arr = str.split('');
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr.join('');
}
// Find all elements with data-ms-code="shuffle-text"
var elements = document.querySelectorAll('[data-ms-code="shuffle-text"]');
elements.forEach(function(el) {
var originalText = el.textContent;
var interval = null;
var duration = 600; // ms
var shuffleSpeed = 50; // ms
el.addEventListener('mouseenter', function() {
var start = Date.now();
clearInterval(interval);
interval = setInterval(function() {
if (Date.now() - start > duration) {
clearInterval(interval);
el.textContent = originalText;
} else {
el.textContent = shuffle(originalText);
}
}, shuffleSpeed);
});
el.addEventListener('mouseleave', function() {
clearInterval(interval);
el.textContent = originalText;
});
});
})();
</script>

#162 - Change Horizontal Tabs on Page Scroll
Auto‑switch horizontal tabs in Webflow as you scroll, locking the scroll inside the tab section.
<!-- 💙 MEMBERSCRIPT #162 v0.1 💙 - CHANGE HORIZONTAL TABS WHEN PAGE IS SCROLLED -->
<script>
(function() {
// Disable on tablet and mobile (only run on desktop ≥ 992px)
if (window.matchMedia('(max-width: 991px)').matches) return;
const tabSection = document.querySelector('[data-ms-code="tab-section"]');
if (!tabSection) return;
const tabButtons = Array.from(tabSection.querySelectorAll('[data-ms-code^="tab-"]'))
.filter(btn => !btn.hasAttribute('data-ms-code') || !btn.getAttribute('data-ms-code').startsWith('tab-content-'));
const tabContents = Array.from(tabSection.querySelectorAll('[data-ms-code^="tab-content-"]'));
if (!tabButtons.length || !tabContents.length) return;
let isLocked = true;
let isTouching = false;
let touchStartY = 0;
let lastTabChange = 0;
const cooldown = 500; // ms
function getCurrentTabIndex() {
return tabButtons.findIndex(btn => btn.classList.contains('w--current'));
}
function activateTab(index) {
if (index < 0 || index >= tabButtons.length) return;
tabButtons[index].click();
}
function isTabSectionInView() {
const rect = tabSection.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
}
function tryTabChange(direction, event) {
const now = Date.now();
if (now - lastTabChange < cooldown) return;
let currentTab = getCurrentTabIndex();
if (direction > 0 && currentTab < tabButtons.length - 1) {
event.preventDefault();
activateTab(currentTab + 1);
isLocked = true;
} else if (direction < 0 && currentTab > 0) {
event.preventDefault();
activateTab(currentTab - 1);
isLocked = true;
} else {
isLocked = false;
const rect = tabSection.getBoundingClientRect();
window.scrollBy({
top: direction > 0 ? rect.bottom - 1 : rect.top - window.innerHeight + 1,
left: 0,
behavior: 'smooth'
});
}
lastTabChange = now;
}
function onWheel(e) {
if (!isTabSectionInView()) return;
if (e.deltaY > 0) {
tryTabChange(1, e);
} else if (e.deltaY < 0) {
tryTabChange(-1, e);
}
}
function onTouchStart(e) {
if (!isTabSectionInView()) return;
isTouching = true;
touchStartY = e.touches[0].clientY;
}
function onTouchMove(e) {
if (!isTabSectionInView() || !isTouching) return;
const now = Date.now();
if (now - lastTabChange < cooldown) return;
const deltaY = touchStartY - e.touches[0].clientY;
if (Math.abs(deltaY) > 30) {
tryTabChange(deltaY > 0 ? 1 : -1, e);
isTouching = false;
}
}
function onTouchEnd() {
isTouching = false;
}
function preventScroll(e) {
if (isLocked && isTabSectionInView()) {
e.preventDefault();
e.stopPropagation();
return false;
}
}
window.addEventListener('scroll', () => {
const currentTab = getCurrentTabIndex();
isLocked = isTabSectionInView() && currentTab > 0 && currentTab < tabButtons.length - 1;
});
tabSection.addEventListener('wheel', onWheel, { passive: false });
tabSection.addEventListener('touchstart', onTouchStart, { passive: false });
tabSection.addEventListener('touchmove', onTouchMove, { passive: false });
tabSection.addEventListener('touchend', onTouchEnd, { passive: false });
document.addEventListener('wheel', preventScroll, { passive: false });
document.addEventListener('touchmove', preventScroll, { passive: false });
const initialTab = getCurrentTabIndex();
isLocked = initialTab > 0 && initialTab < tabButtons.length - 1;
})();
</script>
<!-- 💙 MEMBERSCRIPT #162 v0.1 💙 - CHANGE HORIZONTAL TABS WHEN PAGE IS SCROLLED -->
<script>
(function() {
// Disable on tablet and mobile (only run on desktop ≥ 992px)
if (window.matchMedia('(max-width: 991px)').matches) return;
const tabSection = document.querySelector('[data-ms-code="tab-section"]');
if (!tabSection) return;
const tabButtons = Array.from(tabSection.querySelectorAll('[data-ms-code^="tab-"]'))
.filter(btn => !btn.hasAttribute('data-ms-code') || !btn.getAttribute('data-ms-code').startsWith('tab-content-'));
const tabContents = Array.from(tabSection.querySelectorAll('[data-ms-code^="tab-content-"]'));
if (!tabButtons.length || !tabContents.length) return;
let isLocked = true;
let isTouching = false;
let touchStartY = 0;
let lastTabChange = 0;
const cooldown = 500; // ms
function getCurrentTabIndex() {
return tabButtons.findIndex(btn => btn.classList.contains('w--current'));
}
function activateTab(index) {
if (index < 0 || index >= tabButtons.length) return;
tabButtons[index].click();
}
function isTabSectionInView() {
const rect = tabSection.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
}
function tryTabChange(direction, event) {
const now = Date.now();
if (now - lastTabChange < cooldown) return;
let currentTab = getCurrentTabIndex();
if (direction > 0 && currentTab < tabButtons.length - 1) {
event.preventDefault();
activateTab(currentTab + 1);
isLocked = true;
} else if (direction < 0 && currentTab > 0) {
event.preventDefault();
activateTab(currentTab - 1);
isLocked = true;
} else {
isLocked = false;
const rect = tabSection.getBoundingClientRect();
window.scrollBy({
top: direction > 0 ? rect.bottom - 1 : rect.top - window.innerHeight + 1,
left: 0,
behavior: 'smooth'
});
}
lastTabChange = now;
}
function onWheel(e) {
if (!isTabSectionInView()) return;
if (e.deltaY > 0) {
tryTabChange(1, e);
} else if (e.deltaY < 0) {
tryTabChange(-1, e);
}
}
function onTouchStart(e) {
if (!isTabSectionInView()) return;
isTouching = true;
touchStartY = e.touches[0].clientY;
}
function onTouchMove(e) {
if (!isTabSectionInView() || !isTouching) return;
const now = Date.now();
if (now - lastTabChange < cooldown) return;
const deltaY = touchStartY - e.touches[0].clientY;
if (Math.abs(deltaY) > 30) {
tryTabChange(deltaY > 0 ? 1 : -1, e);
isTouching = false;
}
}
function onTouchEnd() {
isTouching = false;
}
function preventScroll(e) {
if (isLocked && isTabSectionInView()) {
e.preventDefault();
e.stopPropagation();
return false;
}
}
window.addEventListener('scroll', () => {
const currentTab = getCurrentTabIndex();
isLocked = isTabSectionInView() && currentTab > 0 && currentTab < tabButtons.length - 1;
});
tabSection.addEventListener('wheel', onWheel, { passive: false });
tabSection.addEventListener('touchstart', onTouchStart, { passive: false });
tabSection.addEventListener('touchmove', onTouchMove, { passive: false });
tabSection.addEventListener('touchend', onTouchEnd, { passive: false });
document.addEventListener('wheel', preventScroll, { passive: false });
document.addEventListener('touchmove', preventScroll, { passive: false });
const initialTab = getCurrentTabIndex();
isLocked = initialTab > 0 && initialTab < tabButtons.length - 1;
})();
</script>

#161 - Estimate Article Reading Time
Automatically estimates and displays how long it’ll take to read your blog post.
<!-- 💙 MEMBERSCRIPT #161: DYNAMIC READING TIME -->
<script>
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('[data-ms-code="reading-article"]').forEach(article => {
const rtEl = article.querySelector('[data-ms-code="reading-time"]');
const rich = article.querySelector('[data-ms-code="reading-text"]');
if (!rtEl || !rich) return;
const text = rich.innerText.trim();
const words = text ? text.split(/\s+/).length : 0;
const imgs = rich.querySelectorAll('img').length;
const WPM = 260, SEC_PER_IMG = 10;
const totalSec = (words / WPM) * 60 + imgs * SEC_PER_IMG;
const minutes = Math.max(1, Math.ceil(totalSec / 60));
rtEl.innerText = `${minutes} min read`;
});
});
</script>
<!-- 💙 MEMBERSCRIPT #161: DYNAMIC READING TIME -->
<script>
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('[data-ms-code="reading-article"]').forEach(article => {
const rtEl = article.querySelector('[data-ms-code="reading-time"]');
const rich = article.querySelector('[data-ms-code="reading-text"]');
if (!rtEl || !rich) return;
const text = rich.innerText.trim();
const words = text ? text.split(/\s+/).length : 0;
const imgs = rich.querySelectorAll('img').length;
const WPM = 260, SEC_PER_IMG = 10;
const totalSec = (words / WPM) * 60 + imgs * SEC_PER_IMG;
const minutes = Math.max(1, Math.ceil(totalSec / 60));
rtEl.innerText = `${minutes} min read`;
});
});
</script>

#160 - Limit CMS Items Per Breakpoint
Control how many CMS items are visible on your Webflow site for each device size.
<!-- 💙 MEMBERSCRIPT #160 v0.1 💙 - LIMIT CMS ITEMS PER BREAKPOINT -->
<!--
Dynamically limits how many CMS items show per breakpoint.
Useful for responsive design where fewer items should show on smaller screens.
-->
<script>
(function() {
// Set how many items to show per breakpoint (edit as needed)
const limits = {
desktop: 6, // ≥992px
tablet: 4, // 768px–991px
mobile: 2 // <768px
};
function getBreakpoint() {
const width = window.innerWidth;
if (width >= 992) return 'desktop';
if (width >= 768) return 'tablet';
return 'mobile';
}
function limitItems() {
const breakpoint = getBreakpoint();
const limit = limits[breakpoint] || limits.desktop;
const lists = document.querySelectorAll('[data-ms-code="cms-list"]');
lists.forEach(list => {
const items = list.querySelectorAll('[data-ms-code="cms-item"]');
items.forEach((item, i) => {
item.style.display = (i < limit) ? '' : 'none';
});
});
}
// Run on page load and on resize
window.addEventListener('DOMContentLoaded', limitItems);
window.addEventListener('resize', limitItems);
})();
</script>
<!-- 💙 MEMBERSCRIPT #160 v0.1 💙 - LIMIT CMS ITEMS PER BREAKPOINT -->
<!--
Dynamically limits how many CMS items show per breakpoint.
Useful for responsive design where fewer items should show on smaller screens.
-->
<script>
(function() {
// Set how many items to show per breakpoint (edit as needed)
const limits = {
desktop: 6, // ≥992px
tablet: 4, // 768px–991px
mobile: 2 // <768px
};
function getBreakpoint() {
const width = window.innerWidth;
if (width >= 992) return 'desktop';
if (width >= 768) return 'tablet';
return 'mobile';
}
function limitItems() {
const breakpoint = getBreakpoint();
const limit = limits[breakpoint] || limits.desktop;
const lists = document.querySelectorAll('[data-ms-code="cms-list"]');
lists.forEach(list => {
const items = list.querySelectorAll('[data-ms-code="cms-item"]');
items.forEach((item, i) => {
item.style.display = (i < limit) ? '' : 'none';
});
});
}
// Run on page load and on resize
window.addEventListener('DOMContentLoaded', limitItems);
window.addEventListener('resize', limitItems);
})();
</script>

#159 - Price Estimation Calculator
This script calculates a real-time total price based on user-selected options.
<!-- 💙 MEMBERSCRIPT #159 v0.1 💙 - PRICE ESTIMATION CALCULATOR -->
<!--
Calculates a dynamic price total and summary based on selected inputs.
Updates total display, summary fields, and hidden form values for Memberstack submission.
-->
<script>
document.addEventListener("DOMContentLoaded", function () {
const calculator = document.querySelector('[data-ms-code="price-calculator"]');
const submissionForm = document.querySelector('[data-ms-code="submission-form"]');
if (!calculator || !submissionForm) {
console.error("Calculator or Submission Form not found.");
return;
}
const BASE_PRICE = 500; // Replace with your minimum/base price
const calculatePriceAndSummary = () => {
let subTotal = 0;
// 1. PRICE CALCULATION
const pricedInputs = calculator.querySelectorAll('[data-price], [data-price-per-unit]');
pricedInputs.forEach((input) => {
if (input.type === "checkbox" && !input.checked) return;
const price = parseFloat(input.dataset.price) || 0;
const perUnit = parseFloat(input.dataset.pricePerUnit) || 0;
const value = parseFloat(input.value) || 0;
subTotal += perUnit > 0 ? value * perUnit : price;
});
const total = BASE_PRICE + subTotal;
// 2. UPDATE TOTAL DISPLAY
const totalPriceEl = calculator.querySelector('[data-ms-code-price-total]');
if (totalPriceEl) totalPriceEl.textContent = `$${Math.round(total).toLocaleString()}`;
// 3. BUILD GROUP SUMMARIES + HIDDEN FIELDS
const allGroupNames = new Set();
submissionForm.querySelectorAll("[data-ms-code-hidden]").forEach((el) => {
allGroupNames.add(el.dataset.msCodeHidden);
});
allGroupNames.forEach((group) => {
let selectedLabels = [];
const groupInputs = calculator.querySelectorAll(
`input[data-summary-group="${group}"]:checked,
input[data-feature-group="${group}"]:checked,
input[data-summary-group="${group}"][type="range"],
input[data-ms-code-slider="${group}"],
select[data-summary-group="${group}"]`
);
groupInputs.forEach((input) => {
if (input.type === "range") {
selectedLabels.push(input.value);
} else if (input.tagName === "SELECT") {
const selectedOption = input.options[input.selectedIndex];
if (selectedOption) selectedLabels.push(selectedOption.textContent);
} else {
const label = input.parentElement.querySelector(".w-form-label");
if (label) selectedLabels.push(label.textContent);
}
});
const summaryText = selectedLabels.length > 0 ? selectedLabels.join(", ") : "None";
const summaryEl = calculator.querySelector(`[data-ms-code-summary="${group}"]`);
if (summaryEl) summaryEl.textContent = summaryText;
const hiddenInput = submissionForm.querySelector(`[data-ms-code-hidden="${group}"]`);
if (hiddenInput) hiddenInput.value = summaryText;
});
// 4. SET TOTAL IN HIDDEN FIELD
const hiddenPrice = submissionForm.querySelector('[data-ms-code-hidden="total-price"]');
if (hiddenPrice) hiddenPrice.value = total;
// 5. HANDLE OUTPUT DISPLAY FOR GROUPS (subtotal or raw value)
allGroupNames.forEach((group) => {
let groupTotal = 0;
const groupInputs = calculator.querySelectorAll(
`input[data-summary-group="${group}"]:checked,
input[data-feature-group="${group}"]:checked,
input[data-summary-group="${group}"][type="range"],
input[data-ms-code-slider="${group}"],
select[data-summary-group="${group}"]`
);
groupInputs.forEach((input) => {
const price = parseFloat(input.dataset.price) || 0;
const perUnit = parseFloat(input.dataset.pricePerUnit) || 0;
const value = parseFloat(input.value) || 0;
groupTotal += perUnit > 0 ? value * perUnit : price;
});
const groupOutput = calculator.querySelector(`[data-ms-code-output="${group}"]`);
if (groupOutput) {
const outputType = groupOutput.dataset.outputType || "price";
if (outputType === "value") {
const valueInput = calculator.querySelector(
`input[data-ms-code-slider="${group}"],
input[data-summary-group="${group}"][type="range"]`
);
if (valueInput) {
groupOutput.textContent = valueInput.value;
}
} else {
groupOutput.textContent = `$${Math.round(groupTotal).toLocaleString()}`;
}
}
});
};
const setupEventListeners = () => {
calculator.addEventListener("input", calculatePriceAndSummary);
// Exclusive checkbox behavior
const checkboxGroups = {};
calculator.querySelectorAll('input[type="checkbox"][data-feature-group]').forEach((cb) => {
const groupName = cb.dataset.featureGroup;
if (!checkboxGroups[groupName]) checkboxGroups[groupName] = [];
checkboxGroups[groupName].push(cb);
});
Object.values(checkboxGroups).forEach((group) => {
group.forEach((cb) => {
cb.addEventListener("change", () => {
if (cb.checked) {
group.forEach((otherCb) => {
if (otherCb !== cb) otherCb.checked = false;
});
}
calculatePriceAndSummary();
});
});
});
// Reset on submission
submissionForm.addEventListener("submit", () => {
setTimeout(() => {
calculator.querySelectorAll('input[type="checkbox"]').forEach((cb) => (cb.checked = false));
calculator.querySelectorAll('input[type="range"]').forEach((slider) => {
slider.value = slider.defaultValue || slider.min || "0";
slider.dispatchEvent(new Event("input", { bubbles: true }));
});
calculatePriceAndSummary();
}, 100);
});
};
setupEventListeners();
calculatePriceAndSummary();
});
</script>
<!-- 💙 MEMBERSCRIPT #159 v0.1 💙 - PRICE ESTIMATION CALCULATOR -->
<!--
Calculates a dynamic price total and summary based on selected inputs.
Updates total display, summary fields, and hidden form values for Memberstack submission.
-->
<script>
document.addEventListener("DOMContentLoaded", function () {
const calculator = document.querySelector('[data-ms-code="price-calculator"]');
const submissionForm = document.querySelector('[data-ms-code="submission-form"]');
if (!calculator || !submissionForm) {
console.error("Calculator or Submission Form not found.");
return;
}
const BASE_PRICE = 500; // Replace with your minimum/base price
const calculatePriceAndSummary = () => {
let subTotal = 0;
// 1. PRICE CALCULATION
const pricedInputs = calculator.querySelectorAll('[data-price], [data-price-per-unit]');
pricedInputs.forEach((input) => {
if (input.type === "checkbox" && !input.checked) return;
const price = parseFloat(input.dataset.price) || 0;
const perUnit = parseFloat(input.dataset.pricePerUnit) || 0;
const value = parseFloat(input.value) || 0;
subTotal += perUnit > 0 ? value * perUnit : price;
});
const total = BASE_PRICE + subTotal;
// 2. UPDATE TOTAL DISPLAY
const totalPriceEl = calculator.querySelector('[data-ms-code-price-total]');
if (totalPriceEl) totalPriceEl.textContent = `$${Math.round(total).toLocaleString()}`;
// 3. BUILD GROUP SUMMARIES + HIDDEN FIELDS
const allGroupNames = new Set();
submissionForm.querySelectorAll("[data-ms-code-hidden]").forEach((el) => {
allGroupNames.add(el.dataset.msCodeHidden);
});
allGroupNames.forEach((group) => {
let selectedLabels = [];
const groupInputs = calculator.querySelectorAll(
`input[data-summary-group="${group}"]:checked,
input[data-feature-group="${group}"]:checked,
input[data-summary-group="${group}"][type="range"],
input[data-ms-code-slider="${group}"],
select[data-summary-group="${group}"]`
);
groupInputs.forEach((input) => {
if (input.type === "range") {
selectedLabels.push(input.value);
} else if (input.tagName === "SELECT") {
const selectedOption = input.options[input.selectedIndex];
if (selectedOption) selectedLabels.push(selectedOption.textContent);
} else {
const label = input.parentElement.querySelector(".w-form-label");
if (label) selectedLabels.push(label.textContent);
}
});
const summaryText = selectedLabels.length > 0 ? selectedLabels.join(", ") : "None";
const summaryEl = calculator.querySelector(`[data-ms-code-summary="${group}"]`);
if (summaryEl) summaryEl.textContent = summaryText;
const hiddenInput = submissionForm.querySelector(`[data-ms-code-hidden="${group}"]`);
if (hiddenInput) hiddenInput.value = summaryText;
});
// 4. SET TOTAL IN HIDDEN FIELD
const hiddenPrice = submissionForm.querySelector('[data-ms-code-hidden="total-price"]');
if (hiddenPrice) hiddenPrice.value = total;
// 5. HANDLE OUTPUT DISPLAY FOR GROUPS (subtotal or raw value)
allGroupNames.forEach((group) => {
let groupTotal = 0;
const groupInputs = calculator.querySelectorAll(
`input[data-summary-group="${group}"]:checked,
input[data-feature-group="${group}"]:checked,
input[data-summary-group="${group}"][type="range"],
input[data-ms-code-slider="${group}"],
select[data-summary-group="${group}"]`
);
groupInputs.forEach((input) => {
const price = parseFloat(input.dataset.price) || 0;
const perUnit = parseFloat(input.dataset.pricePerUnit) || 0;
const value = parseFloat(input.value) || 0;
groupTotal += perUnit > 0 ? value * perUnit : price;
});
const groupOutput = calculator.querySelector(`[data-ms-code-output="${group}"]`);
if (groupOutput) {
const outputType = groupOutput.dataset.outputType || "price";
if (outputType === "value") {
const valueInput = calculator.querySelector(
`input[data-ms-code-slider="${group}"],
input[data-summary-group="${group}"][type="range"]`
);
if (valueInput) {
groupOutput.textContent = valueInput.value;
}
} else {
groupOutput.textContent = `$${Math.round(groupTotal).toLocaleString()}`;
}
}
});
};
const setupEventListeners = () => {
calculator.addEventListener("input", calculatePriceAndSummary);
// Exclusive checkbox behavior
const checkboxGroups = {};
calculator.querySelectorAll('input[type="checkbox"][data-feature-group]').forEach((cb) => {
const groupName = cb.dataset.featureGroup;
if (!checkboxGroups[groupName]) checkboxGroups[groupName] = [];
checkboxGroups[groupName].push(cb);
});
Object.values(checkboxGroups).forEach((group) => {
group.forEach((cb) => {
cb.addEventListener("change", () => {
if (cb.checked) {
group.forEach((otherCb) => {
if (otherCb !== cb) otherCb.checked = false;
});
}
calculatePriceAndSummary();
});
});
});
// Reset on submission
submissionForm.addEventListener("submit", () => {
setTimeout(() => {
calculator.querySelectorAll('input[type="checkbox"]').forEach((cb) => (cb.checked = false));
calculator.querySelectorAll('input[type="range"]').forEach((slider) => {
slider.value = slider.defaultValue || slider.min || "0";
slider.dispatchEvent(new Event("input", { bubbles: true }));
});
calculatePriceAndSummary();
}, 100);
});
};
setupEventListeners();
calculatePriceAndSummary();
});
</script>
¿Necesitas ayuda con MemberScripts? ¡Únete a nuestra comunidad Slack de más de 5.500 miembros! 🙌
Los MemberScripts son un recurso comunitario de Memberstack - si necesitas ayuda para que funcionen con tu proyecto, ¡únete al Slack de Memberstack 2.0 y pide ayuda!
Únete a nuestro SlackExplore empresas reales que han tenido éxito con Memberstack
No se fíe sólo de nuestra palabra: eche un vistazo a las empresas de todos los tamaños que confían en Memberstack para su autenticación y sus pagos.
Empieza a construir tus sueños
Memberstack es 100% gratis hasta que estés listo para lanzarla - así que, ¿a qué estás esperando? Crea tu primera aplicación y empieza a construir hoy mismo.