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.

#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>

#158 - Emoji Feedback Widget for Memberstack
This widget lets your logged-in members quickly share how they feel using simple emoji buttons.
<!-- 💙 MEMBERSCRIPT #158 v1.0 💙 - EMOJI FEEDBACK WIDGET -->
<!--
Collect emoji-based feedback from logged-in members.
Saves submission state in localStorage and sends data to Make.com.
-->
<script>
(function() {
const msDom = window.$memberstackDom;
if (!msDom) {
console.error('Memberstack DOM not found.');
return;
}
// Elements
const widget = document.querySelector('[data-ms-code="emoji-feedback-widget"]');
const closeBtn = widget.querySelector('[data-ms-code="emoji-feedback-close"]');
const buttons = widget.querySelectorAll('[data-ms-code="emoji-feedback-btn"]');
const thanks = widget.querySelector('[data-ms-code="emoji-feedback-thanks"]');
// Exit early if feedback is done or dismissed
if (
localStorage.getItem('emojiFeedbackDone') === 'true' ||
localStorage.getItem('emojiFeedbackClosed') === 'true'
) {
widget.style.display = 'none';
return;
}
// Handle close (×) click
closeBtn.addEventListener('click', e => {
e.preventDefault();
localStorage.setItem('emojiFeedbackClosed', 'true');
widget.style.display = 'none';
});
// Fetch member data
msDom.getCurrentMember()
.then(({ data: member }) => {
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const score = btn.getAttribute('data-value');
// Payload for Make.com
const payload = {
memberId: member.id,
name: member.customFields["first-name"] || '',
email: member.auth.email || '',
pageUrl: window.location.href,
feedback: score,
timestamp: new Date().toISOString()
};
// Send feedback to Make
fetch('https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => {
if (!res.ok) throw new Error(res.statusText);
// Success state: hide emojis, show thank you
localStorage.setItem('emojiFeedbackDone', 'true');
widget.querySelector('[data-ms-code="emoji-feedback-buttons"]').style.display = 'none';
thanks.style.display = 'block';
})
.catch(err => {
console.error('Emoji feedback error:', err);
// Optionally add error handling UI here
});
});
});
})
.catch(err => console.error('Couldn’t get member:', err));
})();
</script>
<!-- 💙 MEMBERSCRIPT #158 v1.0 💙 - EMOJI FEEDBACK WIDGET -->
<!--
Collect emoji-based feedback from logged-in members.
Saves submission state in localStorage and sends data to Make.com.
-->
<script>
(function() {
const msDom = window.$memberstackDom;
if (!msDom) {
console.error('Memberstack DOM not found.');
return;
}
// Elements
const widget = document.querySelector('[data-ms-code="emoji-feedback-widget"]');
const closeBtn = widget.querySelector('[data-ms-code="emoji-feedback-close"]');
const buttons = widget.querySelectorAll('[data-ms-code="emoji-feedback-btn"]');
const thanks = widget.querySelector('[data-ms-code="emoji-feedback-thanks"]');
// Exit early if feedback is done or dismissed
if (
localStorage.getItem('emojiFeedbackDone') === 'true' ||
localStorage.getItem('emojiFeedbackClosed') === 'true'
) {
widget.style.display = 'none';
return;
}
// Handle close (×) click
closeBtn.addEventListener('click', e => {
e.preventDefault();
localStorage.setItem('emojiFeedbackClosed', 'true');
widget.style.display = 'none';
});
// Fetch member data
msDom.getCurrentMember()
.then(({ data: member }) => {
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const score = btn.getAttribute('data-value');
// Payload for Make.com
const payload = {
memberId: member.id,
name: member.customFields["first-name"] || '',
email: member.auth.email || '',
pageUrl: window.location.href,
feedback: score,
timestamp: new Date().toISOString()
};
// Send feedback to Make
fetch('https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => {
if (!res.ok) throw new Error(res.statusText);
// Success state: hide emojis, show thank you
localStorage.setItem('emojiFeedbackDone', 'true');
widget.querySelector('[data-ms-code="emoji-feedback-buttons"]').style.display = 'none';
thanks.style.display = 'block';
})
.catch(err => {
console.error('Emoji feedback error:', err);
// Optionally add error handling UI here
});
});
});
})
.catch(err => console.error('Couldn’t get member:', err));
})();
</script>

#157 - Range Slider Feedback Widget
A simple and friendly slider widget that lets logged-in members give quick feedback (0–10).
<!-- 💙 MEMBERSCRIPT #157 v1.0 💙 - RANGE SLIDER FEEDBACK WIDGET -->
<!--
A lightweight feedback widget that uses a range slider.
Prevents duplicate submissions with localStorage, and sends feedback to Make.com via webhook.
-->
<script>
Webflow.push(function() {
// Silently disable all form submissions
$('form').submit(function(e) {
e.preventDefault();
return false;
});
});
(function () {
const msDom = window.$memberstackDom;
if (!msDom) {
console.error("Memberstack DOM not found. Did you include data-memberstack-app?");
return;
}
const widget = document.querySelector('[data-ms-code="feedback-widget"]');
const dialog = widget?.querySelector('[data-ms-code="feedback-dialog"]');
const toggle = widget?.querySelector('[data-ms-code="feedback-toggle"]');
const slider = widget?.querySelector('[data-ms-code="feedback-range"]');
const submit = widget?.querySelector('[data-ms-code="feedback-next"]');
const form = widget?.closest("form");
const done = localStorage.getItem("feedbackDone") === "true";
const closed = localStorage.getItem("feedbackClosed") === "true";
if (!widget || !dialog || !toggle || !slider || !submit || done || closed) {
if (widget) widget.style.display = "none";
return;
}
// Manual close button logic
toggle.addEventListener("click", (e) => {
e.preventDefault();
localStorage.setItem("feedbackClosed", "true");
widget.style.display = "none";
});
// Fetch logged-in member
msDom.getCurrentMember()
.then(({ data: member }) => {
slider.addEventListener("input", () => {
submit.disabled = false;
});
submit.addEventListener("click", () => {
const payload = {
memberId: member.id,
name: member.customFields["first-name"] || "",
email: member.auth.email || "",
pageUrl: window.location.href,
feedback: slider.value,
timestamp: new Date().toISOString()
};
fetch("https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
localStorage.setItem("feedbackDone", "true");
const msg = document.createElement("p");
msg.textContent = "Thanks for your feedback!";
msg.style.padding = "1em";
msg.style.textAlign = "center";
dialog.innerHTML = "";
dialog.appendChild(msg);
})
.catch((err) => {
console.error("Feedback error:", err);
dialog.insertAdjacentHTML(
"beforeend",
'<p style="color:red; text-align:center;">Oops! Could not send. Try again?</p>'
);
});
});
})
.catch((err) => console.error("Couldn’t get member:", err));
})();
</script>
<!-- 💙 MEMBERSCRIPT #157 v1.0 💙 - RANGE SLIDER FEEDBACK WIDGET -->
<!--
A lightweight feedback widget that uses a range slider.
Prevents duplicate submissions with localStorage, and sends feedback to Make.com via webhook.
-->
<script>
Webflow.push(function() {
// Silently disable all form submissions
$('form').submit(function(e) {
e.preventDefault();
return false;
});
});
(function () {
const msDom = window.$memberstackDom;
if (!msDom) {
console.error("Memberstack DOM not found. Did you include data-memberstack-app?");
return;
}
const widget = document.querySelector('[data-ms-code="feedback-widget"]');
const dialog = widget?.querySelector('[data-ms-code="feedback-dialog"]');
const toggle = widget?.querySelector('[data-ms-code="feedback-toggle"]');
const slider = widget?.querySelector('[data-ms-code="feedback-range"]');
const submit = widget?.querySelector('[data-ms-code="feedback-next"]');
const form = widget?.closest("form");
const done = localStorage.getItem("feedbackDone") === "true";
const closed = localStorage.getItem("feedbackClosed") === "true";
if (!widget || !dialog || !toggle || !slider || !submit || done || closed) {
if (widget) widget.style.display = "none";
return;
}
// Manual close button logic
toggle.addEventListener("click", (e) => {
e.preventDefault();
localStorage.setItem("feedbackClosed", "true");
widget.style.display = "none";
});
// Fetch logged-in member
msDom.getCurrentMember()
.then(({ data: member }) => {
slider.addEventListener("input", () => {
submit.disabled = false;
});
submit.addEventListener("click", () => {
const payload = {
memberId: member.id,
name: member.customFields["first-name"] || "",
email: member.auth.email || "",
pageUrl: window.location.href,
feedback: slider.value,
timestamp: new Date().toISOString()
};
fetch("https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
localStorage.setItem("feedbackDone", "true");
const msg = document.createElement("p");
msg.textContent = "Thanks for your feedback!";
msg.style.padding = "1em";
msg.style.textAlign = "center";
dialog.innerHTML = "";
dialog.appendChild(msg);
})
.catch((err) => {
console.error("Feedback error:", err);
dialog.insertAdjacentHTML(
"beforeend",
'<p style="color:red; text-align:center;">Oops! Could not send. Try again?</p>'
);
});
});
})
.catch((err) => console.error("Couldn’t get member:", err));
})();
</script>

#156 - Encrypt Sensitive Data Before Sending to Memberstack
This script protects sensitive user data by encrypting it in the browser before it’s sent to Memberstack.
<!-- 💙 MEMBERSCRIPT #156 v1.0 💙 - ENCRYPT SENSITIVE DATA BEFORE SENDING TO MEMBERSTACK -->
<!--
This script encrypts input fields before they're submitted to Memberstack,
using AES-GCM with a passphrase-based modal.
-->
<script>
document.addEventListener('DOMContentLoaded', function () {
(function () {
const enc = new TextEncoder();
const dec = new TextDecoder();
// Show the passphrase modal
function showModal() {
return new Promise(resolve => {
const modal = document.querySelector('[data-ms-code="encrypt-modal"]');
if (!modal) return alert('Encryption modal missing from the page.');
const input = modal.querySelector('[data-ms-code="pass-input"]');
const remember = modal.querySelector('[data-ms-code="remember-pass"]');
const submit = modal.querySelector('[data-ms-code="submit-pass"]');
const closeButtons = modal.querySelectorAll(
'[data-ms-code="close-encrypt-modal"], [data-ms-code="close-encrypt-icon"]'
);
modal.style.display = 'flex';
input.value = '';
input.focus();
const cleanup = () => {
modal.style.display = 'none';
};
if (submit) {
submit.onclick = () => {
const pass = input.value;
const keep = remember.checked;
cleanup();
resolve({ pass, remember: keep });
};
}
closeButtons.forEach(btn => {
btn.onclick = () => {
cleanup();
resolve({ pass: null });
};
});
});
}
// Derive AES key using PBKDF2
async function deriveKey(pass, salt) {
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(pass),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// Encrypt a string
async function encryptText(text, pass) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(pass, salt);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
enc.encode(text)
);
return [
btoa(String.fromCharCode(...salt)),
btoa(String.fromCharCode(...iv)),
btoa(String.fromCharCode(...new Uint8Array(encrypted)))
].join(':');
}
// Decrypt a string
async function decryptText(encrypted, pass) {
const [saltB64, ivB64, dataB64] = encrypted.split(':');
if (!saltB64 || !ivB64 || !dataB64) throw new Error('Invalid format');
const salt = Uint8Array.from(atob(saltB64), c => c.charCodeAt(0));
const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
const data = Uint8Array.from(atob(dataB64), c => c.charCodeAt(0));
const key = await deriveKey(pass, salt);
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data);
return dec.decode(decrypted);
}
// Encrypt and submit form
document.querySelectorAll('[data-ms-code-encrypt]').forEach(btn => {
if (!btn.hasAttribute('data-ms-encrypt-attached')) {
btn.addEventListener('click', async e => {
e.preventDefault();
let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
if (!passphrase) {
const { pass, remember } = await showModal();
if (!pass) return;
passphrase = pass;
if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
}
const fields = document.querySelectorAll('[data-ms-code-id]');
for (let field of fields) {
const value = field.value.trim();
if (!value) continue;
try {
const encrypted = await encryptText(value, passphrase);
field.value = encrypted;
} catch (err) {
console.error('Encryption error:', err);
alert('Encryption failed.');
return;
}
}
const form = btn.closest('form');
if (form) form.requestSubmit();
});
btn.setAttribute('data-ms-encrypt-attached', 'true');
}
});
// Add decrypt button logic
function attachDecryptButton() {
const decryptBtn = document.querySelector('[data-ms-code="decrypt-all"]');
if (!decryptBtn || decryptBtn.hasAttribute('data-ms-decrypt-attached')) return;
decryptBtn.addEventListener('click', async e => {
e.preventDefault();
const encryptedFields = document.querySelectorAll('[data-ms-code-id]');
if (encryptedFields.length === 0) return alert('No fields to decrypt.');
let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
if (!passphrase) {
const { pass, remember } = await showModal();
if (!pass) return;
passphrase = pass;
if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
}
for (let field of encryptedFields) {
const encrypted = field.value.trim();
if (!encrypted) continue;
try {
const decrypted = await decryptText(encrypted, passphrase);
field.value = decrypted;
} catch (err) {
console.error('Decryption error:', err);
alert('One or more fields failed to decrypt.');
return;
}
}
});
decryptBtn.setAttribute('data-ms-decrypt-attached', 'true');
}
attachDecryptButton();
})();
});
</script>
<!-- 💙 MEMBERSCRIPT #156 v1.0 💙 - ENCRYPT SENSITIVE DATA BEFORE SENDING TO MEMBERSTACK -->
<!--
This script encrypts input fields before they're submitted to Memberstack,
using AES-GCM with a passphrase-based modal.
-->
<script>
document.addEventListener('DOMContentLoaded', function () {
(function () {
const enc = new TextEncoder();
const dec = new TextDecoder();
// Show the passphrase modal
function showModal() {
return new Promise(resolve => {
const modal = document.querySelector('[data-ms-code="encrypt-modal"]');
if (!modal) return alert('Encryption modal missing from the page.');
const input = modal.querySelector('[data-ms-code="pass-input"]');
const remember = modal.querySelector('[data-ms-code="remember-pass"]');
const submit = modal.querySelector('[data-ms-code="submit-pass"]');
const closeButtons = modal.querySelectorAll(
'[data-ms-code="close-encrypt-modal"], [data-ms-code="close-encrypt-icon"]'
);
modal.style.display = 'flex';
input.value = '';
input.focus();
const cleanup = () => {
modal.style.display = 'none';
};
if (submit) {
submit.onclick = () => {
const pass = input.value;
const keep = remember.checked;
cleanup();
resolve({ pass, remember: keep });
};
}
closeButtons.forEach(btn => {
btn.onclick = () => {
cleanup();
resolve({ pass: null });
};
});
});
}
// Derive AES key using PBKDF2
async function deriveKey(pass, salt) {
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(pass),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// Encrypt a string
async function encryptText(text, pass) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(pass, salt);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
enc.encode(text)
);
return [
btoa(String.fromCharCode(...salt)),
btoa(String.fromCharCode(...iv)),
btoa(String.fromCharCode(...new Uint8Array(encrypted)))
].join(':');
}
// Decrypt a string
async function decryptText(encrypted, pass) {
const [saltB64, ivB64, dataB64] = encrypted.split(':');
if (!saltB64 || !ivB64 || !dataB64) throw new Error('Invalid format');
const salt = Uint8Array.from(atob(saltB64), c => c.charCodeAt(0));
const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
const data = Uint8Array.from(atob(dataB64), c => c.charCodeAt(0));
const key = await deriveKey(pass, salt);
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data);
return dec.decode(decrypted);
}
// Encrypt and submit form
document.querySelectorAll('[data-ms-code-encrypt]').forEach(btn => {
if (!btn.hasAttribute('data-ms-encrypt-attached')) {
btn.addEventListener('click', async e => {
e.preventDefault();
let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
if (!passphrase) {
const { pass, remember } = await showModal();
if (!pass) return;
passphrase = pass;
if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
}
const fields = document.querySelectorAll('[data-ms-code-id]');
for (let field of fields) {
const value = field.value.trim();
if (!value) continue;
try {
const encrypted = await encryptText(value, passphrase);
field.value = encrypted;
} catch (err) {
console.error('Encryption error:', err);
alert('Encryption failed.');
return;
}
}
const form = btn.closest('form');
if (form) form.requestSubmit();
});
btn.setAttribute('data-ms-encrypt-attached', 'true');
}
});
// Add decrypt button logic
function attachDecryptButton() {
const decryptBtn = document.querySelector('[data-ms-code="decrypt-all"]');
if (!decryptBtn || decryptBtn.hasAttribute('data-ms-decrypt-attached')) return;
decryptBtn.addEventListener('click', async e => {
e.preventDefault();
const encryptedFields = document.querySelectorAll('[data-ms-code-id]');
if (encryptedFields.length === 0) return alert('No fields to decrypt.');
let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
if (!passphrase) {
const { pass, remember } = await showModal();
if (!pass) return;
passphrase = pass;
if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
}
for (let field of encryptedFields) {
const encrypted = field.value.trim();
if (!encrypted) continue;
try {
const decrypted = await decryptText(encrypted, passphrase);
field.value = decrypted;
} catch (err) {
console.error('Decryption error:', err);
alert('One or more fields failed to decrypt.');
return;
}
}
});
decryptBtn.setAttribute('data-ms-decrypt-attached', 'true');
}
attachDecryptButton();
})();
});
</script>

#155 - Bulk Update Customer Subscriptions via Stripe & Make
Bulk update existing members to a new pricing plan with Stripe and Make

#154 - Two-Factor Authentication (2FA) for Memberstack Logins
Add an extra layer of security to your Memberstack logins by enabling Two-Factor Authentication (2FA).
<!--
MEMBERSCRIPT #154
---------------------------------
LOGIN PAGE SCRIPT
-->
<script>
(async function() {
const delay = ms => new Promise(r => setTimeout(r, ms));
async function routeLogin() {
try {
// Check if Memberstack is loaded
if (!window.$memberstackDom) {
console.log("Memberstack not loaded yet");
return;
}
// Get current member
const { data: member } = await window.$memberstackDom.getCurrentMember();
if (!member) return; // Exit if not logged in
// Get member JSON data
const jsonResponse = await window.$memberstackDom.getMemberJSON();
const memberData = jsonResponse.data || {};
// Check if 2FA is enabled
const needs2FA = memberData["2fa_enabled"] === true ||
jsonResponse["2fa_enabled"] === true;
// Check session storage for verification status
const verified = sessionStorage.getItem("2fa_verified") === "true";
console.log("2FA Status:", {
enabled: needs2FA,
verified: verified,
currentPath: window.location.pathname
});
// Handle 2FA redirect
if (needs2FA && !verified) {
if (!window.location.pathname.includes("/2fa-verify")) {
console.log("Redirecting to /2fa-verify");
window.location.href = "/2fa-verify";
}
return; // Stop further execution
}
// Handle success redirect
if (!window.location.pathname.includes("/success")) {
console.log("Redirecting to /success");
// Remove Memberstack's auto-redirect if login form exists
const loginForm = document.querySelector('[data-ms-form="login"]');
if (loginForm) {
loginForm.removeAttribute('data-ms-redirect');
}
window.location.href = "/success";
}
} catch (err) {
console.error("2FA routing error:", err);
}
}
// Wait for Memberstack to initialize
await delay(300);
routeLogin();
// Poll with cleanup
const pollInterval = setInterval(routeLogin, 500);
setTimeout(() => clearInterval(pollInterval), 10000);
})();
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
SETTINGS PAGE SCRIPT
-->
<!-- Load otplib preset-browser -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const { data: member } = await ms.getCurrentMember();
if (!member) return;
const checkbox = document.querySelector('[data-ms-code="enable-2fa"]');
const qrContainer = document.querySelector('[data-ms-code="2fa-qr-container"]');
const qrImage = document.querySelector('[data-ms-code="2fa-qr-image"]');
// Hide QR container by default
qrContainer.style.display = "none";
// Load member JSON and ensure data object exists
const jsonObj = await ms.getMemberJSON(); // { data: ... } or { data: null }
if (!jsonObj.data) jsonObj.data = {};
const inner = jsonObj.data;
// Set checkbox initial state
const enabled = inner["2fa_enabled"] === true;
checkbox.checked = enabled;
checkbox.addEventListener("change", async (e) => {
const isChecked = e.target.checked;
// Reload member and JSON
const { data: member } = await ms.getCurrentMember();
const jsonObj2 = await ms.getMemberJSON();
if (!jsonObj2.data) jsonObj2.data = {};
const inner2 = jsonObj2.data;
if (isChecked) {
// Enable 2FA: generate secret and QR
const secret = window.otplib.authenticator.generateSecret();
const uri = window.otplib.authenticator.keyuri(member.email, "Memberscript #154", secret);
qrImage.src = "https://api.qrserver.com/v1/create-qr-code/?data=" + encodeURIComponent(uri);
qrContainer.style.display = "flex";
inner2["2fa_enabled"] = true;
inner2["2fa_secret"] = secret;
await ms.updateMember({
customFields: { "2fa-enabled": "true" }
});
} else {
// Disable 2FA: remove secret and hide QR
qrContainer.style.display = "none";
inner2["2fa_enabled"] = false;
delete inner2["2fa_secret"];
await ms.updateMember({
customFields: { "2fa-enabled": "false" }
});
}
// Persist only nested JSON
await ms.updateMemberJSON({ json: inner2 });
// ✅ Debugging: check result
const check = await ms.getMemberJSON();
console.log(check);
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
SUCCESS / DASHBOARD PAGE SCRIPT
-->
<script>
window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
if (!member) return; // Not logged in, no redirect
window.$memberstackDom.getMemberJSON().then(json => {
const enabled = json["2fa_enabled"];
const verified = sessionStorage.getItem("2fa_verified") === "true";
if (enabled && !verified && window.location.pathname !== "/2fa-verify") {
window.location.href = "/2fa-verify";
}
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
TWO FACTOR VERIFICATION PAGE SCRIPT
-->
<!-- Include the Buffer polyfill -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<!-- Include the otplib library -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const memberstack = window.$memberstackDom;
const { data: member } = await memberstack.getCurrentMember();
if (!member) return;
const form = document.querySelector('[data-ms-form="2fa-verification"]');
if (!form) return;
const codeInput = form.querySelector('[data-ms-code="2fa-code"]');
const errorContainer = form.querySelector('[data-ms-error="2fa-code"]');
function showError(message) {
if (errorContainer) {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
} else {
alert(message);
}
}
function clearError() {
if (errorContainer) {
errorContainer.textContent = '';
errorContainer.style.display = 'none';
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
e.stopImmediatePropagation(); // important to stop other listeners
clearError();
const code = codeInput.value.trim();
const { data: json } = await memberstack.getMemberJSON();
const secret = json?.["2fa_secret"];
if (!secret || !code) {
const msg = form.getAttribute('data-ms-error-msg-missing') || 'Please enter your 2FA code';
showError(msg);
return;
}
if (otplib.authenticator.check(code, secret)) {
sessionStorage.setItem('2fa_verified', 'true');
window.location.href = '/success';
} else {
const msg = form.getAttribute('data-ms-error-msg-invalid') || 'Oops, the 2FA code is incorrect. Try again.';
showError(msg);
}
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
LOGIN PAGE SCRIPT
-->
<script>
(async function() {
const delay = ms => new Promise(r => setTimeout(r, ms));
async function routeLogin() {
try {
// Check if Memberstack is loaded
if (!window.$memberstackDom) {
console.log("Memberstack not loaded yet");
return;
}
// Get current member
const { data: member } = await window.$memberstackDom.getCurrentMember();
if (!member) return; // Exit if not logged in
// Get member JSON data
const jsonResponse = await window.$memberstackDom.getMemberJSON();
const memberData = jsonResponse.data || {};
// Check if 2FA is enabled
const needs2FA = memberData["2fa_enabled"] === true ||
jsonResponse["2fa_enabled"] === true;
// Check session storage for verification status
const verified = sessionStorage.getItem("2fa_verified") === "true";
console.log("2FA Status:", {
enabled: needs2FA,
verified: verified,
currentPath: window.location.pathname
});
// Handle 2FA redirect
if (needs2FA && !verified) {
if (!window.location.pathname.includes("/2fa-verify")) {
console.log("Redirecting to /2fa-verify");
window.location.href = "/2fa-verify";
}
return; // Stop further execution
}
// Handle success redirect
if (!window.location.pathname.includes("/success")) {
console.log("Redirecting to /success");
// Remove Memberstack's auto-redirect if login form exists
const loginForm = document.querySelector('[data-ms-form="login"]');
if (loginForm) {
loginForm.removeAttribute('data-ms-redirect');
}
window.location.href = "/success";
}
} catch (err) {
console.error("2FA routing error:", err);
}
}
// Wait for Memberstack to initialize
await delay(300);
routeLogin();
// Poll with cleanup
const pollInterval = setInterval(routeLogin, 500);
setTimeout(() => clearInterval(pollInterval), 10000);
})();
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
SETTINGS PAGE SCRIPT
-->
<!-- Load otplib preset-browser -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const { data: member } = await ms.getCurrentMember();
if (!member) return;
const checkbox = document.querySelector('[data-ms-code="enable-2fa"]');
const qrContainer = document.querySelector('[data-ms-code="2fa-qr-container"]');
const qrImage = document.querySelector('[data-ms-code="2fa-qr-image"]');
// Hide QR container by default
qrContainer.style.display = "none";
// Load member JSON and ensure data object exists
const jsonObj = await ms.getMemberJSON(); // { data: ... } or { data: null }
if (!jsonObj.data) jsonObj.data = {};
const inner = jsonObj.data;
// Set checkbox initial state
const enabled = inner["2fa_enabled"] === true;
checkbox.checked = enabled;
checkbox.addEventListener("change", async (e) => {
const isChecked = e.target.checked;
// Reload member and JSON
const { data: member } = await ms.getCurrentMember();
const jsonObj2 = await ms.getMemberJSON();
if (!jsonObj2.data) jsonObj2.data = {};
const inner2 = jsonObj2.data;
if (isChecked) {
// Enable 2FA: generate secret and QR
const secret = window.otplib.authenticator.generateSecret();
const uri = window.otplib.authenticator.keyuri(member.email, "Memberscript #154", secret);
qrImage.src = "https://api.qrserver.com/v1/create-qr-code/?data=" + encodeURIComponent(uri);
qrContainer.style.display = "flex";
inner2["2fa_enabled"] = true;
inner2["2fa_secret"] = secret;
await ms.updateMember({
customFields: { "2fa-enabled": "true" }
});
} else {
// Disable 2FA: remove secret and hide QR
qrContainer.style.display = "none";
inner2["2fa_enabled"] = false;
delete inner2["2fa_secret"];
await ms.updateMember({
customFields: { "2fa-enabled": "false" }
});
}
// Persist only nested JSON
await ms.updateMemberJSON({ json: inner2 });
// ✅ Debugging: check result
const check = await ms.getMemberJSON();
console.log(check);
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
SUCCESS / DASHBOARD PAGE SCRIPT
-->
<script>
window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
if (!member) return; // Not logged in, no redirect
window.$memberstackDom.getMemberJSON().then(json => {
const enabled = json["2fa_enabled"];
const verified = sessionStorage.getItem("2fa_verified") === "true";
if (enabled && !verified && window.location.pathname !== "/2fa-verify") {
window.location.href = "/2fa-verify";
}
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
TWO FACTOR VERIFICATION PAGE SCRIPT
-->
<!-- Include the Buffer polyfill -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<!-- Include the otplib library -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const memberstack = window.$memberstackDom;
const { data: member } = await memberstack.getCurrentMember();
if (!member) return;
const form = document.querySelector('[data-ms-form="2fa-verification"]');
if (!form) return;
const codeInput = form.querySelector('[data-ms-code="2fa-code"]');
const errorContainer = form.querySelector('[data-ms-error="2fa-code"]');
function showError(message) {
if (errorContainer) {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
} else {
alert(message);
}
}
function clearError() {
if (errorContainer) {
errorContainer.textContent = '';
errorContainer.style.display = 'none';
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
e.stopImmediatePropagation(); // important to stop other listeners
clearError();
const code = codeInput.value.trim();
const { data: json } = await memberstack.getMemberJSON();
const secret = json?.["2fa_secret"];
if (!secret || !code) {
const msg = form.getAttribute('data-ms-error-msg-missing') || 'Please enter your 2FA code';
showError(msg);
return;
}
if (otplib.authenticator.check(code, secret)) {
sessionStorage.setItem('2fa_verified', 'true');
window.location.href = '/success';
} else {
const msg = form.getAttribute('data-ms-error-msg-invalid') || 'Oops, the 2FA code is incorrect. Try again.';
showError(msg);
}
});
});
</script>

#153 - Instant Multilingual Site with Google Translate
Make your Webflow site multilingual in minutes with Google Translate and Memberstack
<!-- 💙 MEMBERSCRIPT #153 v2.0 💙 - FREE MULTILINGUAL SITE WITH GOOGLE TRANSLATE -->
<script>
// 1) Load Google Translate
const gtScript = document.createElement('script');
gtScript.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
document.head.appendChild(gtScript);
// 2) Hide Google UI
const style = document.createElement('style');
style.innerHTML = `
body { top: 0px !important; position: static !important; }
.goog-te-banner-frame, .skiptranslate,
#goog-gt-tt, .goog-te-balloon-frame,
.goog-text-highlight {
display: none !important;
background: none !important;
box-shadow: none !important;
}
`;
document.head.appendChild(style);
// 3) Ensure container exists
if (!document.getElementById('google_translate_element')) {
const holder = document.createElement('div');
holder.id = 'google_translate_element';
holder.style.display = 'none';
document.body.appendChild(holder);
}
// 4) Init widget
window.googleTranslateElementInit = function () {
new google.translate.TranslateElement({
pageLanguage: 'en',
layout: google.translate.TranslateElement.FloatPosition.TOP_LEFT
}, 'google_translate_element');
};
// 5) Cookies
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [key, value] = cookie.trim().split('=');
if (key === name) return decodeURIComponent(value);
}
return null;
}
function setGoogTransCookie(pathValue) {
const expiresPast = "expires=Thu, 01 Jan 1970 00:00:01 GMT;";
const expiresFuture = "expires=Fri, 31 Dec 9999 23:59:59 GMT;";
if (!pathValue) {
document.cookie = "googtrans=;path=/;" + expiresPast;
document.cookie = "googtrans=;domain=.webflow.io;path=/;" + expiresPast;
} else {
document.cookie = "googtrans=" + encodeURIComponent(pathValue) + ";path=/;" + expiresFuture;
document.cookie = "googtrans=" + encodeURIComponent(pathValue) + ";domain=.webflow.io;path=/;" + expiresFuture;
}
}
// 6) Language map
const languageMap = new Map([
["af","Afrikaans"], ["sq","Albanian"], ["ar","Arabic"], ["hy","Armenian"],
["az","Azerbaijani"], ["eu","Basque"], ["be","Belarusian"], ["bg","Bulgarian"],
["ca","Catalan"], ["zh-CN","ChineseSimplified"], ["zh-TW","ChineseTraditional"],
["hr","Croatian"], ["cs","Czech"], ["da","Danish"], ["nl","Dutch"], ["de","German"],
["en","English"], ["et","Estonian"], ["tl","Filipino"], ["fi","Finnish"],
["fr","French"], ["gl","Galician"], ["ka","Georgian"], ["el","Greek"],
["ht","Haitian"], ["iw","Hebrew"], ["hi","Hindi"], ["hu","Hungarian"],
["is","Icelandic"], ["id","Indonesian"], ["ga","Irish"], ["it","Italian"],
["ja","Japanese"], ["ko","Korean"], ["lv","Latvian"], ["lt","Lithuanian"],
["mk","Macedonian"], ["ms","Malay"], ["mt","Maltese"], ["no","Norwegian"],
["fa","Persian"], ["pl","Polish"], ["pt","Portuguese"], ["ro","Romanian"],
["ru","Russian"], ["sr","Serbian"], ["sk","Slovak"], ["sl","Slovenian"],
["es","Spanish"], ["sw","Swahili"], ["sv","Swedish"], ["th","Thai"],
["tr","Turkish"], ["uk","Ukrainian"], ["ur","Urdu"], ["vi","Vietnamese"],
["cy","Welsh"], ["yi","Yiddish"]
]);
// 7) Current language
let currentLang = getCookie("googtrans")?.split("/").pop() || "en";
// 8) Badge + content helpers
function formatLangCodeForBadge(code) {
return code && code.includes('-') ? code.toUpperCase() : (code || 'en').slice(0, 2).toUpperCase();
}
function updateLanguageBadge(langCode) {
const badgeEl = document.querySelector('.material-icons.language .google-icons');
if (badgeEl) {
const lc = langCode || 'en';
badgeEl.textContent = formatLangCodeForBadge(lc);
badgeEl.setAttribute('data-lang', lc);
const readable = languageMap.get(lc) || lc;
badgeEl.setAttribute('title', readable);
badgeEl.setAttribute('aria-label', `Language: ${readable}`);
}
}
function updateLanguageSpecificContent(langCode) {
const readable = languageMap.get(langCode || 'en');
const langClass = readable ? `.languagespecific.${readable.toLowerCase()}specific` : `.languagespecific.englishspecific`;
const fallbackClass = `.languagespecific.englishspecific`;
document.querySelectorAll('.languagespecific').forEach(el => { el.style.display = 'none'; });
if (document.querySelector(langClass)) {
document.querySelectorAll(langClass).forEach(el => el.style.display = 'block');
} else {
document.querySelectorAll(fallbackClass).forEach(el => el.style.display = 'block');
}
}
// 9) Wait for dropdown
function waitForCombo(maxWaitMs = 6000) {
return new Promise((resolve) => {
const existing = document.querySelector('.goog-te-combo');
if (existing) return resolve(existing);
const started = Date.now();
const obs = new MutationObserver(() => {
const c = document.querySelector('.goog-te-combo');
if (c) {
obs.disconnect();
resolve(c);
} else if (Date.now() - started > maxWaitMs) {
obs.disconnect();
resolve(null);
}
});
obs.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
const c = document.querySelector('.goog-te-combo');
if (c) {
obs.disconnect();
resolve(c);
}
}, maxWaitMs);
});
}
// 10) Robust event dispatch
function fireComboEvents(combo) {
combo.dispatchEvent(new Event('input', { bubbles: true }));
combo.dispatchEvent(new Event('change', { bubbles: true }));
combo.dispatchEvent(new Event('blur', { bubbles: true }));
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// 11) Apply language (single-click reliable)
async function applyLanguage(langCode) {
const combo = await waitForCombo();
const target = langCode || 'en';
updateLanguageBadge(target);
updateLanguageSpecificContent(target);
if (!combo) {
if (target === 'en') {
setGoogTransCookie('');
window.location.hash = '';
} else {
setGoogTransCookie(`/en/${target}`);
window.location.hash = `#googtrans(en|${target})`;
}
return;
}
combo.focus();
if (target === 'en') {
setGoogTransCookie('');
window.location.hash = '';
const hasBlank = Array.from(combo.options).some(o => o.value === '');
if (hasBlank) {
combo.value = '';
fireComboEvents(combo);
} else {
combo.value = 'en';
fireComboEvents(combo);
await sleep(120);
combo.value = '';
fireComboEvents(combo);
}
return;
}
if (combo.value === '') {
combo.value = 'en';
fireComboEvents(combo);
await sleep(120);
}
setGoogTransCookie(`/en/${target}`);
window.location.hash = `#googtrans(en|${target})`;
combo.value = target;
fireComboEvents(combo);
await sleep(120);
if (combo.value !== target) {
combo.value = target;
fireComboEvents(combo);
}
}
// 12) Wire up
document.addEventListener("DOMContentLoaded", function () {
updateLanguageBadge(currentLang);
updateLanguageSpecificContent(currentLang);
document.querySelectorAll('[data-ms-code-lang-select]').forEach(el => {
el.addEventListener('click', function (e) {
e.preventDefault();
const selectedLang = this.getAttribute('data-ms-code-lang');
applyLanguage(selectedLang);
});
});
waitForCombo().then((combo) => {
if (!combo) return;
if (!combo._msBound) {
combo.addEventListener('change', () => {
const val = combo.value || 'en';
updateLanguageBadge(val);
updateLanguageSpecificContent(val);
});
combo._msBound = true;
}
});
});
</script>
<!-- 💙 MEMBERSCRIPT #153 v2.0 💙 - FREE MULTILINGUAL SITE WITH GOOGLE TRANSLATE -->
<script>
// 1) Load Google Translate
const gtScript = document.createElement('script');
gtScript.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
document.head.appendChild(gtScript);
// 2) Hide Google UI
const style = document.createElement('style');
style.innerHTML = `
body { top: 0px !important; position: static !important; }
.goog-te-banner-frame, .skiptranslate,
#goog-gt-tt, .goog-te-balloon-frame,
.goog-text-highlight {
display: none !important;
background: none !important;
box-shadow: none !important;
}
`;
document.head.appendChild(style);
// 3) Ensure container exists
if (!document.getElementById('google_translate_element')) {
const holder = document.createElement('div');
holder.id = 'google_translate_element';
holder.style.display = 'none';
document.body.appendChild(holder);
}
// 4) Init widget
window.googleTranslateElementInit = function () {
new google.translate.TranslateElement({
pageLanguage: 'en',
layout: google.translate.TranslateElement.FloatPosition.TOP_LEFT
}, 'google_translate_element');
};
// 5) Cookies
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [key, value] = cookie.trim().split('=');
if (key === name) return decodeURIComponent(value);
}
return null;
}
function setGoogTransCookie(pathValue) {
const expiresPast = "expires=Thu, 01 Jan 1970 00:00:01 GMT;";
const expiresFuture = "expires=Fri, 31 Dec 9999 23:59:59 GMT;";
if (!pathValue) {
document.cookie = "googtrans=;path=/;" + expiresPast;
document.cookie = "googtrans=;domain=.webflow.io;path=/;" + expiresPast;
} else {
document.cookie = "googtrans=" + encodeURIComponent(pathValue) + ";path=/;" + expiresFuture;
document.cookie = "googtrans=" + encodeURIComponent(pathValue) + ";domain=.webflow.io;path=/;" + expiresFuture;
}
}
// 6) Language map
const languageMap = new Map([
["af","Afrikaans"], ["sq","Albanian"], ["ar","Arabic"], ["hy","Armenian"],
["az","Azerbaijani"], ["eu","Basque"], ["be","Belarusian"], ["bg","Bulgarian"],
["ca","Catalan"], ["zh-CN","ChineseSimplified"], ["zh-TW","ChineseTraditional"],
["hr","Croatian"], ["cs","Czech"], ["da","Danish"], ["nl","Dutch"], ["de","German"],
["en","English"], ["et","Estonian"], ["tl","Filipino"], ["fi","Finnish"],
["fr","French"], ["gl","Galician"], ["ka","Georgian"], ["el","Greek"],
["ht","Haitian"], ["iw","Hebrew"], ["hi","Hindi"], ["hu","Hungarian"],
["is","Icelandic"], ["id","Indonesian"], ["ga","Irish"], ["it","Italian"],
["ja","Japanese"], ["ko","Korean"], ["lv","Latvian"], ["lt","Lithuanian"],
["mk","Macedonian"], ["ms","Malay"], ["mt","Maltese"], ["no","Norwegian"],
["fa","Persian"], ["pl","Polish"], ["pt","Portuguese"], ["ro","Romanian"],
["ru","Russian"], ["sr","Serbian"], ["sk","Slovak"], ["sl","Slovenian"],
["es","Spanish"], ["sw","Swahili"], ["sv","Swedish"], ["th","Thai"],
["tr","Turkish"], ["uk","Ukrainian"], ["ur","Urdu"], ["vi","Vietnamese"],
["cy","Welsh"], ["yi","Yiddish"]
]);
// 7) Current language
let currentLang = getCookie("googtrans")?.split("/").pop() || "en";
// 8) Badge + content helpers
function formatLangCodeForBadge(code) {
return code && code.includes('-') ? code.toUpperCase() : (code || 'en').slice(0, 2).toUpperCase();
}
function updateLanguageBadge(langCode) {
const badgeEl = document.querySelector('.material-icons.language .google-icons');
if (badgeEl) {
const lc = langCode || 'en';
badgeEl.textContent = formatLangCodeForBadge(lc);
badgeEl.setAttribute('data-lang', lc);
const readable = languageMap.get(lc) || lc;
badgeEl.setAttribute('title', readable);
badgeEl.setAttribute('aria-label', `Language: ${readable}`);
}
}
function updateLanguageSpecificContent(langCode) {
const readable = languageMap.get(langCode || 'en');
const langClass = readable ? `.languagespecific.${readable.toLowerCase()}specific` : `.languagespecific.englishspecific`;
const fallbackClass = `.languagespecific.englishspecific`;
document.querySelectorAll('.languagespecific').forEach(el => { el.style.display = 'none'; });
if (document.querySelector(langClass)) {
document.querySelectorAll(langClass).forEach(el => el.style.display = 'block');
} else {
document.querySelectorAll(fallbackClass).forEach(el => el.style.display = 'block');
}
}
// 9) Wait for dropdown
function waitForCombo(maxWaitMs = 6000) {
return new Promise((resolve) => {
const existing = document.querySelector('.goog-te-combo');
if (existing) return resolve(existing);
const started = Date.now();
const obs = new MutationObserver(() => {
const c = document.querySelector('.goog-te-combo');
if (c) {
obs.disconnect();
resolve(c);
} else if (Date.now() - started > maxWaitMs) {
obs.disconnect();
resolve(null);
}
});
obs.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
const c = document.querySelector('.goog-te-combo');
if (c) {
obs.disconnect();
resolve(c);
}
}, maxWaitMs);
});
}
// 10) Robust event dispatch
function fireComboEvents(combo) {
combo.dispatchEvent(new Event('input', { bubbles: true }));
combo.dispatchEvent(new Event('change', { bubbles: true }));
combo.dispatchEvent(new Event('blur', { bubbles: true }));
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// 11) Apply language (single-click reliable)
async function applyLanguage(langCode) {
const combo = await waitForCombo();
const target = langCode || 'en';
updateLanguageBadge(target);
updateLanguageSpecificContent(target);
if (!combo) {
if (target === 'en') {
setGoogTransCookie('');
window.location.hash = '';
} else {
setGoogTransCookie(`/en/${target}`);
window.location.hash = `#googtrans(en|${target})`;
}
return;
}
combo.focus();
if (target === 'en') {
setGoogTransCookie('');
window.location.hash = '';
const hasBlank = Array.from(combo.options).some(o => o.value === '');
if (hasBlank) {
combo.value = '';
fireComboEvents(combo);
} else {
combo.value = 'en';
fireComboEvents(combo);
await sleep(120);
combo.value = '';
fireComboEvents(combo);
}
return;
}
if (combo.value === '') {
combo.value = 'en';
fireComboEvents(combo);
await sleep(120);
}
setGoogTransCookie(`/en/${target}`);
window.location.hash = `#googtrans(en|${target})`;
combo.value = target;
fireComboEvents(combo);
await sleep(120);
if (combo.value !== target) {
combo.value = target;
fireComboEvents(combo);
}
}
// 12) Wire up
document.addEventListener("DOMContentLoaded", function () {
updateLanguageBadge(currentLang);
updateLanguageSpecificContent(currentLang);
document.querySelectorAll('[data-ms-code-lang-select]').forEach(el => {
el.addEventListener('click', function (e) {
e.preventDefault();
const selectedLang = this.getAttribute('data-ms-code-lang');
applyLanguage(selectedLang);
});
});
waitForCombo().then((combo) => {
if (!combo) return;
if (!combo._msBound) {
combo.addEventListener('change', () => {
const val = combo.value || 'en';
updateLanguageBadge(val);
updateLanguageSpecificContent(val);
});
combo._msBound = true;
}
});
});
</script>

#152 - OTP Verification via WhatsApp in Webflow
Verify phone numbers via WhatsApp before allowing form submissions in Webflow.
<!-- 💙 MEMBERSCRIPT #152 v1.0 💙 - OTP VERIFICATION VIA WHATSAPP IN WEBFLOW -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const phoneInput = document.querySelector('[data-ms-code="phone-number"]');
const form = phoneInput?.closest("form");
const submitBtn = form?.querySelector('[type="submit"]');
let isVerified = false;
let originalMsFormValue = "signup"; // update this if your form uses another Memberstack action
if (phoneInput && form && submitBtn && window.$WhatsAuthForm) {
// Temporarily disable Memberstack
form.removeAttribute("data-ms-form");
// Create error message
const errorMsg = document.createElement('div');
errorMsg.style.color = 'red';
errorMsg.style.marginTop = '8px';
errorMsg.style.display = 'none';
errorMsg.textContent = '⚠️ Please verify your phone number via WhatsApp.';
phoneInput.parentNode.appendChild(errorMsg);
// Initially disable submit
submitBtn.disabled = true;
submitBtn.style.opacity = 0.6;
submitBtn.style.cursor = 'not-allowed';
// Init WhatsAuth
window.$WhatsAuthForm.init({
inputSelector: '[data-ms-code="phone-number"]',
apiKey: "k07Zj8EwdAIzHzLcLPQh-5jCuREbSKXG", //REPLACE WITH YOUR API KEY
placeholder: phoneInput.getAttribute("data-ms-placeholder") || "",
primaryColor: phoneInput.getAttribute("data-ms-primary-color") || "",
secondaryColor: phoneInput.getAttribute("data-ms-secondary-color") || "",
btnText: phoneInput.getAttribute("data-ms-btn-text") || ""
});
// Watch for success class from WhatsAuth
const observer = new MutationObserver(() => {
if (phoneInput.classList.contains("whatsauth-success") && !isVerified) {
isVerified = true;
// Enable submit
submitBtn.disabled = false;
submitBtn.style.opacity = 1;
submitBtn.style.cursor = 'pointer';
// Hide error
errorMsg.style.display = 'none';
// Re-enable Memberstack
form.setAttribute("data-ms-form", originalMsFormValue);
}
});
observer.observe(phoneInput, { attributes: true, attributeFilter: ["class"] });
// Final form safeguard
form.addEventListener("submit", function (e) {
if (!isVerified) {
e.preventDefault();
errorMsg.style.display = 'block';
}
});
submitBtn.addEventListener("click", function (e) {
if (!isVerified) {
e.preventDefault();
errorMsg.style.display = 'block';
}
});
}
});
</script>
<!-- 💙 MEMBERSCRIPT #152 v1.0 💙 - OTP VERIFICATION VIA WHATSAPP IN WEBFLOW -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const phoneInput = document.querySelector('[data-ms-code="phone-number"]');
const form = phoneInput?.closest("form");
const submitBtn = form?.querySelector('[type="submit"]');
let isVerified = false;
let originalMsFormValue = "signup"; // update this if your form uses another Memberstack action
if (phoneInput && form && submitBtn && window.$WhatsAuthForm) {
// Temporarily disable Memberstack
form.removeAttribute("data-ms-form");
// Create error message
const errorMsg = document.createElement('div');
errorMsg.style.color = 'red';
errorMsg.style.marginTop = '8px';
errorMsg.style.display = 'none';
errorMsg.textContent = '⚠️ Please verify your phone number via WhatsApp.';
phoneInput.parentNode.appendChild(errorMsg);
// Initially disable submit
submitBtn.disabled = true;
submitBtn.style.opacity = 0.6;
submitBtn.style.cursor = 'not-allowed';
// Init WhatsAuth
window.$WhatsAuthForm.init({
inputSelector: '[data-ms-code="phone-number"]',
apiKey: "k07Zj8EwdAIzHzLcLPQh-5jCuREbSKXG", //REPLACE WITH YOUR API KEY
placeholder: phoneInput.getAttribute("data-ms-placeholder") || "",
primaryColor: phoneInput.getAttribute("data-ms-primary-color") || "",
secondaryColor: phoneInput.getAttribute("data-ms-secondary-color") || "",
btnText: phoneInput.getAttribute("data-ms-btn-text") || ""
});
// Watch for success class from WhatsAuth
const observer = new MutationObserver(() => {
if (phoneInput.classList.contains("whatsauth-success") && !isVerified) {
isVerified = true;
// Enable submit
submitBtn.disabled = false;
submitBtn.style.opacity = 1;
submitBtn.style.cursor = 'pointer';
// Hide error
errorMsg.style.display = 'none';
// Re-enable Memberstack
form.setAttribute("data-ms-form", originalMsFormValue);
}
});
observer.observe(phoneInput, { attributes: true, attributeFilter: ["class"] });
// Final form safeguard
form.addEventListener("submit", function (e) {
if (!isVerified) {
e.preventDefault();
errorMsg.style.display = 'block';
}
});
submitBtn.addEventListener("click", function (e) {
if (!isVerified) {
e.preventDefault();
errorMsg.style.display = 'block';
}
});
}
});
</script>

#151 - Onboarding Tour For New Members
Launch a step-by-step product tour the first time a member logs in. Uses Memberstack’s JS API + Intro.js
<!-- 💙 MEMBERSCRIPT #151 v0.1 💙 - ONBOARDING TOUR FOR NEW MEMBERS -->
<script>
// 1. Wait for Memberstack v2 DOM
function ready(fn) {
if (window.$memberstackReady) return fn();
document.addEventListener("memberstack.ready", fn);
}
// 2. Collect all steps from the DOM
function collectSteps() {
// all elements with ms-code-step, in a NodeList
var els = document.querySelectorAll("[ms-code-step]");
// build an array of { order, element, intro }
var steps = Array.prototype.map.call(els, function(el) {
return {
order: parseInt(el.getAttribute("ms-code-step"), 10),
element: el,
intro: el.getAttribute("ms-code-intro") || ""
};
});
// sort by order ascending
return steps.sort(function(a, b) {
return a.order - b.order;
}).map(function(s) {
return { element: s.element, intro: s.intro };
});
}
// 3. Kick off the tour for first-time members
function launchTour(member) {
if (!member || !member.id) return; // only for logged-in
if (localStorage.getItem("ms-code-tour-shown")) return;
// Build steps dynamically
var options = {
steps: collectSteps(),
showProgress: true,
exitOnOverlayClick: false
};
introJs().setOptions(options).start();
localStorage.setItem("ms-code-tour-shown", "true");
}
// 4. Glue it together
ready(function() {
window.$memberstackDom
.getCurrentMember()
.then(function(res) {
launchTour(res.data);
})
.catch(function(err) {
console.error("MS-Code-Tour error:", err);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #151 v0.1 💙 - ONBOARDING TOUR FOR NEW MEMBERS -->
<script>
// 1. Wait for Memberstack v2 DOM
function ready(fn) {
if (window.$memberstackReady) return fn();
document.addEventListener("memberstack.ready", fn);
}
// 2. Collect all steps from the DOM
function collectSteps() {
// all elements with ms-code-step, in a NodeList
var els = document.querySelectorAll("[ms-code-step]");
// build an array of { order, element, intro }
var steps = Array.prototype.map.call(els, function(el) {
return {
order: parseInt(el.getAttribute("ms-code-step"), 10),
element: el,
intro: el.getAttribute("ms-code-intro") || ""
};
});
// sort by order ascending
return steps.sort(function(a, b) {
return a.order - b.order;
}).map(function(s) {
return { element: s.element, intro: s.intro };
});
}
// 3. Kick off the tour for first-time members
function launchTour(member) {
if (!member || !member.id) return; // only for logged-in
if (localStorage.getItem("ms-code-tour-shown")) return;
// Build steps dynamically
var options = {
steps: collectSteps(),
showProgress: true,
exitOnOverlayClick: false
};
introJs().setOptions(options).start();
localStorage.setItem("ms-code-tour-shown", "true");
}
// 4. Glue it together
ready(function() {
window.$memberstackDom
.getCurrentMember()
.then(function(res) {
launchTour(res.data);
})
.catch(function(err) {
console.error("MS-Code-Tour error:", err);
});
});
</script>

#150 - Save and Unsave Items to Your Collection (Pinterest-style)
A simple save/unsave system that lets members bookmark items into personal collections.
<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 1 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const member = await ms.getCurrentMember();
const isLoggedIn = !!member;
let savedItems = {};
const fetchSavedItems = async () => {
try {
const { data } = await ms.getMemberJSON();
savedItems = data.savedItems || {};
} catch {
savedItems = {};
}
};
const persistSavedItems = async () => {
try {
await ms.updateMemberJSON({ json: { savedItems } });
} catch (err) {
console.error("Error saving items:", err);
}
};
const updateButtons = () => {
document.querySelectorAll('[ms-code-add-button]').forEach(btn => {
const id = btn.getAttribute('ms-code-save');
const category = btn.getAttribute('ms-code-category');
const exists = savedItems[category]?.some(i => i.id === id);
btn.style.display = exists ? 'none' : 'inline-block';
});
document.querySelectorAll('[ms-code-unsave-button]').forEach(btn => {
const id = btn.getAttribute('ms-code-unsave');
const category = btn.getAttribute('ms-code-category');
const exists = savedItems[category]?.some(i => i.id === id);
btn.style.display = exists ? 'inline-block' : 'none';
});
};
const onAddClick = async (e) => {
e.preventDefault();
if (!isLoggedIn) return;
const btn = e.currentTarget;
const container = btn.closest('[ms-code-save-item]');
const id = btn.getAttribute('ms-code-save');
const category = btn.getAttribute('ms-code-category');
const img = container?.querySelector('[ms-code-image]');
const url = img?.src;
if (!savedItems[category]) savedItems[category] = [];
if (!savedItems[category].some(i => i.id === id)) {
savedItems[category].push({ id, url });
updateButtons();
await persistSavedItems();
}
};
const onUnsaveClick = async (e) => {
e.preventDefault();
if (!isLoggedIn) return;
const btn = e.currentTarget;
const id = btn.getAttribute('ms-code-unsave');
const category = btn.getAttribute('ms-code-category');
if (savedItems[category]) {
savedItems[category] = savedItems[category].filter(i => i.id !== id);
if (savedItems[category].length === 0) delete savedItems[category];
updateButtons();
await persistSavedItems();
}
};
const onDownloadClick = (e) => {
e.preventDefault();
const btn = e.currentTarget;
const container = btn.closest('[ms-code-save-item]');
const img = container?.querySelector('[ms-code-image]');
const url = img?.src;
if (url) {
const a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a);
a.click();
a.remove();
}
};
const attachListeners = () => {
document.querySelectorAll('[ms-code-add-button]').forEach(b => b.addEventListener('click', onAddClick));
document.querySelectorAll('[ms-code-unsave-button]').forEach(b => b.addEventListener('click', onUnsaveClick));
document.querySelectorAll('[ms-code-download-button]').forEach(b => b.addEventListener('click', onDownloadClick));
};
await fetchSavedItems();
updateButtons();
attachListeners();
});
</script>
<!-- GENERATE PINTEREST GRID STYLE -->
<script>
$(document).ready(function () {
setTimeout(function() {
function resizeGridItem(item) {
grid = document.getElementsByClassName("grid")[0];
rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));
rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap'));
rowSpan = Math.ceil((item.querySelector('.content').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));
item.style.gridRowEnd = "span " + rowSpan;
}
function resizeAllGridItems() {
allItems = document.getElementsByClassName("item");
for (x = 0; x < allItems.length; x++) {
resizeGridItem(allItems[x]);
}
}
function resizeInstance(instance) {
item = instance.elements[0];
resizeGridItem(item);
}
window.onload = resizeAllGridItems();
window.addEventListener("resize", resizeAllGridItems);
allItems = document.getElementsByClassName("item");
for (x = 0; x < allItems.length; x++) {
imagesLoaded(allItems[x], resizeInstance);
}
setTimeout(function() { resizeInstance() }, 100);
}, 800);
})
</script>
<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 2 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const wrapper = document.querySelector('[ms-code-collections-wrapper]');
const template = document.querySelector('[ms-code-folder-template]') || document.querySelector('[ms-code-folder]');
const emptyState = document.querySelector('[ms-code-empty]');
if (!wrapper || !template) return;
let member;
try {
member = await ms.getCurrentMember();
} catch {
wrapper.textContent = "Please log in to view your collections.";
return;
}
let savedItems = {};
try {
const { data } = await ms.getMemberJSON();
savedItems = data?.savedItems || {};
} catch {
wrapper.textContent = "Could not load your collections.";
return;
}
if (Object.keys(savedItems).length === 0) {
wrapper.innerHTML = '';
if (emptyState) emptyState.style.display = 'block';
return;
}
if (emptyState) emptyState.style.display = 'none';
wrapper.innerHTML = '';
const persistSavedItems = async () => {
try {
await ms.updateMemberJSON({ json: { savedItems } });
} catch (err) {
console.error("Failed to save", err);
}
};
const updateButtons = (modal, id, category) => {
const addBtn = modal.querySelector('[ms-code-add-button]');
const unsaveBtn = modal.querySelector('[ms-code-unsave-button]');
const exists = savedItems[category]?.some(item => item.id === id);
addBtn.style.display = exists ? 'none' : 'inline-block';
unsaveBtn.style.display = exists ? 'inline-block' : 'none';
};
Object.entries(savedItems).forEach(([category, items]) => {
const folderClone = template.cloneNode(true);
const titleEl = folderClone.querySelector('[ms-code-folder-title]');
if (titleEl) titleEl.textContent = `${category} (${items.length})`;
const imageContainer = folderClone.querySelector('[ms-code-folder-items]');
const imageTemplate = folderClone.querySelector('[ms-code-folder-image]');
if (imageTemplate) imageTemplate.style.display = 'none';
const modal = folderClone.querySelector('[ms-code-modal]');
const modalImg = folderClone.querySelector('[ms-code-modal-img]');
const modalClose = folderClone.querySelector('[ms-code-modal-close]');
const addButton = folderClone.querySelector('[ms-code-add-button]');
const unsaveButton = folderClone.querySelector('[ms-code-unsave-button]');
const downloadButton = folderClone.querySelector('[ms-code-download-button]');
const hiddenImage = folderClone.querySelector('[ms-code-image]');
items.forEach(item => {
const imgClone = imageTemplate.cloneNode(true);
imgClone.src = item.url;
imgClone.alt = category;
imgClone.style.display = 'block';
imgClone.style.objectFit = 'cover';
imgClone.style.width = '100%';
imgClone.style.height = 'auto';
imgClone.style.maxWidth = '100%';
imgClone.addEventListener('click', () => {
if (modal && modalImg) {
modalImg.src = item.url;
if (hiddenImage) hiddenImage.src = item.url;
const id = item.id;
addButton.onclick = async (e) => {
e.preventDefault();
savedItems[category] = savedItems[category] || [];
if (!savedItems[category].some(i => i.id === id)) {
savedItems[category].push({ id, url: item.url });
await persistSavedItems();
updateButtons(modal, id, category);
}
};
unsaveButton.onclick = async (e) => {
e.preventDefault();
savedItems[category] = savedItems[category].filter(i => i.id !== id);
if (savedItems[category].length === 0) delete savedItems[category];
await persistSavedItems();
modal.style.display = 'none';
location.reload();
};
downloadButton.onclick = (e) => {
e.preventDefault();
const a = document.createElement('a');
a.href = item.url;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
updateButtons(modal, id, category);
}
modal.style.display = 'flex';
});
imageContainer.appendChild(imgClone);
});
if (modal && modalClose) {
modalClose.addEventListener('click', () => {
modal.style.display = 'none';
if (modalImg) modalImg.src = '';
});
}
wrapper.appendChild(folderClone);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 1 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const member = await ms.getCurrentMember();
const isLoggedIn = !!member;
let savedItems = {};
const fetchSavedItems = async () => {
try {
const { data } = await ms.getMemberJSON();
savedItems = data.savedItems || {};
} catch {
savedItems = {};
}
};
const persistSavedItems = async () => {
try {
await ms.updateMemberJSON({ json: { savedItems } });
} catch (err) {
console.error("Error saving items:", err);
}
};
const updateButtons = () => {
document.querySelectorAll('[ms-code-add-button]').forEach(btn => {
const id = btn.getAttribute('ms-code-save');
const category = btn.getAttribute('ms-code-category');
const exists = savedItems[category]?.some(i => i.id === id);
btn.style.display = exists ? 'none' : 'inline-block';
});
document.querySelectorAll('[ms-code-unsave-button]').forEach(btn => {
const id = btn.getAttribute('ms-code-unsave');
const category = btn.getAttribute('ms-code-category');
const exists = savedItems[category]?.some(i => i.id === id);
btn.style.display = exists ? 'inline-block' : 'none';
});
};
const onAddClick = async (e) => {
e.preventDefault();
if (!isLoggedIn) return;
const btn = e.currentTarget;
const container = btn.closest('[ms-code-save-item]');
const id = btn.getAttribute('ms-code-save');
const category = btn.getAttribute('ms-code-category');
const img = container?.querySelector('[ms-code-image]');
const url = img?.src;
if (!savedItems[category]) savedItems[category] = [];
if (!savedItems[category].some(i => i.id === id)) {
savedItems[category].push({ id, url });
updateButtons();
await persistSavedItems();
}
};
const onUnsaveClick = async (e) => {
e.preventDefault();
if (!isLoggedIn) return;
const btn = e.currentTarget;
const id = btn.getAttribute('ms-code-unsave');
const category = btn.getAttribute('ms-code-category');
if (savedItems[category]) {
savedItems[category] = savedItems[category].filter(i => i.id !== id);
if (savedItems[category].length === 0) delete savedItems[category];
updateButtons();
await persistSavedItems();
}
};
const onDownloadClick = (e) => {
e.preventDefault();
const btn = e.currentTarget;
const container = btn.closest('[ms-code-save-item]');
const img = container?.querySelector('[ms-code-image]');
const url = img?.src;
if (url) {
const a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a);
a.click();
a.remove();
}
};
const attachListeners = () => {
document.querySelectorAll('[ms-code-add-button]').forEach(b => b.addEventListener('click', onAddClick));
document.querySelectorAll('[ms-code-unsave-button]').forEach(b => b.addEventListener('click', onUnsaveClick));
document.querySelectorAll('[ms-code-download-button]').forEach(b => b.addEventListener('click', onDownloadClick));
};
await fetchSavedItems();
updateButtons();
attachListeners();
});
</script>
<!-- GENERATE PINTEREST GRID STYLE -->
<script>
$(document).ready(function () {
setTimeout(function() {
function resizeGridItem(item) {
grid = document.getElementsByClassName("grid")[0];
rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));
rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap'));
rowSpan = Math.ceil((item.querySelector('.content').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));
item.style.gridRowEnd = "span " + rowSpan;
}
function resizeAllGridItems() {
allItems = document.getElementsByClassName("item");
for (x = 0; x < allItems.length; x++) {
resizeGridItem(allItems[x]);
}
}
function resizeInstance(instance) {
item = instance.elements[0];
resizeGridItem(item);
}
window.onload = resizeAllGridItems();
window.addEventListener("resize", resizeAllGridItems);
allItems = document.getElementsByClassName("item");
for (x = 0; x < allItems.length; x++) {
imagesLoaded(allItems[x], resizeInstance);
}
setTimeout(function() { resizeInstance() }, 100);
}, 800);
})
</script>
<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 2 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const wrapper = document.querySelector('[ms-code-collections-wrapper]');
const template = document.querySelector('[ms-code-folder-template]') || document.querySelector('[ms-code-folder]');
const emptyState = document.querySelector('[ms-code-empty]');
if (!wrapper || !template) return;
let member;
try {
member = await ms.getCurrentMember();
} catch {
wrapper.textContent = "Please log in to view your collections.";
return;
}
let savedItems = {};
try {
const { data } = await ms.getMemberJSON();
savedItems = data?.savedItems || {};
} catch {
wrapper.textContent = "Could not load your collections.";
return;
}
if (Object.keys(savedItems).length === 0) {
wrapper.innerHTML = '';
if (emptyState) emptyState.style.display = 'block';
return;
}
if (emptyState) emptyState.style.display = 'none';
wrapper.innerHTML = '';
const persistSavedItems = async () => {
try {
await ms.updateMemberJSON({ json: { savedItems } });
} catch (err) {
console.error("Failed to save", err);
}
};
const updateButtons = (modal, id, category) => {
const addBtn = modal.querySelector('[ms-code-add-button]');
const unsaveBtn = modal.querySelector('[ms-code-unsave-button]');
const exists = savedItems[category]?.some(item => item.id === id);
addBtn.style.display = exists ? 'none' : 'inline-block';
unsaveBtn.style.display = exists ? 'inline-block' : 'none';
};
Object.entries(savedItems).forEach(([category, items]) => {
const folderClone = template.cloneNode(true);
const titleEl = folderClone.querySelector('[ms-code-folder-title]');
if (titleEl) titleEl.textContent = `${category} (${items.length})`;
const imageContainer = folderClone.querySelector('[ms-code-folder-items]');
const imageTemplate = folderClone.querySelector('[ms-code-folder-image]');
if (imageTemplate) imageTemplate.style.display = 'none';
const modal = folderClone.querySelector('[ms-code-modal]');
const modalImg = folderClone.querySelector('[ms-code-modal-img]');
const modalClose = folderClone.querySelector('[ms-code-modal-close]');
const addButton = folderClone.querySelector('[ms-code-add-button]');
const unsaveButton = folderClone.querySelector('[ms-code-unsave-button]');
const downloadButton = folderClone.querySelector('[ms-code-download-button]');
const hiddenImage = folderClone.querySelector('[ms-code-image]');
items.forEach(item => {
const imgClone = imageTemplate.cloneNode(true);
imgClone.src = item.url;
imgClone.alt = category;
imgClone.style.display = 'block';
imgClone.style.objectFit = 'cover';
imgClone.style.width = '100%';
imgClone.style.height = 'auto';
imgClone.style.maxWidth = '100%';
imgClone.addEventListener('click', () => {
if (modal && modalImg) {
modalImg.src = item.url;
if (hiddenImage) hiddenImage.src = item.url;
const id = item.id;
addButton.onclick = async (e) => {
e.preventDefault();
savedItems[category] = savedItems[category] || [];
if (!savedItems[category].some(i => i.id === id)) {
savedItems[category].push({ id, url: item.url });
await persistSavedItems();
updateButtons(modal, id, category);
}
};
unsaveButton.onclick = async (e) => {
e.preventDefault();
savedItems[category] = savedItems[category].filter(i => i.id !== id);
if (savedItems[category].length === 0) delete savedItems[category];
await persistSavedItems();
modal.style.display = 'none';
location.reload();
};
downloadButton.onclick = (e) => {
e.preventDefault();
const a = document.createElement('a');
a.href = item.url;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
updateButtons(modal, id, category);
}
modal.style.display = 'flex';
});
imageContainer.appendChild(imgClone);
});
if (modal && modalClose) {
modalClose.addEventListener('click', () => {
modal.style.display = 'none';
if (modalImg) modalImg.src = '';
});
}
wrapper.appendChild(folderClone);
});
});
</script>

#149 - Favicon for Dark/Light Mode
Use this script to update your website's favicon based on the user's system color scheme preference.
<!-- 💙 MEMBERSCRIPT #149 v0.1 💙 - FAVICON FOR DARK/LIGHT MODE -->
<script>
// Helper: Retrieve or create a favicon element
function getFaviconElement() {
let favicon = document.querySelector('link[rel="icon"]') ||
document.querySelector('link[rel="shortcut icon"]');
if (!favicon) {
favicon = document.createElement('link');
favicon.rel = 'icon';
document.head.appendChild(favicon);
}
return favicon;
}
// Function to update the favicon based on dark mode
function updateFavicon(e) {
const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
const favicon = getFaviconElement();
// Update these paths to your favicon assets in Webflow’s Asset Manager or a CDN
favicon.href = darkModeOn
? 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f85b2a9f281a373ca_Dark%20Mode%20Logo.png'
: 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f1c2fa3cebee1b150_Light%20Mode%20Logo.png';
}
// Initialize the favicon update on page load
updateFavicon();
// Listen for changes in the dark mode media query
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (typeof darkModeMediaQuery.addEventListener === 'function') {
darkModeMediaQuery.addEventListener('change', updateFavicon);
} else if (typeof darkModeMediaQuery.addListener === 'function') {
darkModeMediaQuery.addListener(updateFavicon);
}
</script>
<!-- 💙 MEMBERSCRIPT #149 v0.1 💙 - FAVICON FOR DARK/LIGHT MODE -->
<script>
// Helper: Retrieve or create a favicon element
function getFaviconElement() {
let favicon = document.querySelector('link[rel="icon"]') ||
document.querySelector('link[rel="shortcut icon"]');
if (!favicon) {
favicon = document.createElement('link');
favicon.rel = 'icon';
document.head.appendChild(favicon);
}
return favicon;
}
// Function to update the favicon based on dark mode
function updateFavicon(e) {
const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
const favicon = getFaviconElement();
// Update these paths to your favicon assets in Webflow’s Asset Manager or a CDN
favicon.href = darkModeOn
? 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f85b2a9f281a373ca_Dark%20Mode%20Logo.png'
: 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f1c2fa3cebee1b150_Light%20Mode%20Logo.png';
}
// Initialize the favicon update on page load
updateFavicon();
// Listen for changes in the dark mode media query
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (typeof darkModeMediaQuery.addEventListener === 'function') {
darkModeMediaQuery.addEventListener('change', updateFavicon);
} else if (typeof darkModeMediaQuery.addListener === 'function') {
darkModeMediaQuery.addListener(updateFavicon);
}
</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.




.png)