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.

#58 - Entradas del deslizador de rango de precios
Cree una entrada de rango de precios con entradas individuales para mín. y máx.
<!-- 💙 MEMBERSCRIPT #58 v0.1 💙 RANGE SLIDER INPUT -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/css/ion.rangeSlider.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/js/ion.rangeSlider.min.js"></script>
<style>
.irs {
font-family: inherit;
}
</style>
<script>
$(document).ready(function() {
var rangeSlider = $('[ms-code-input="range-slider"]');
var priceFromInput = $('[ms-code-input="price-from"]');
var priceToInput = $('[ms-code-input="price-to"]');
// Set the default range values
var defaultFrom = 20000;
var defaultTo = 50000;
rangeSlider.ionRangeSlider({
skin: "round", // You can also try "flat", "big", "modern", "sharp", or "square". Default styles can be overridden with CSS.
type: "double",
grid: true,
min: 0,
max: 100000,
from: defaultFrom,
to: defaultTo,
prefix: "$",
onStart: function(data) {
priceFromInput.val(data.from);
priceToInput.val(data.to);
},
onChange: function(data) {
priceFromInput.val(data.from);
priceToInput.val(data.to);
}
});
// Get the initial range values and update the inputs
var initialRange = rangeSlider.data("ionRangeSlider");
var initialData = initialRange.result;
priceFromInput.val(initialData.from);
priceToInput.val(initialData.to);
// Update the range slider and inputs when the inputs lose focus
priceFromInput.on("blur", function() {
var value = $(this).val();
var toValue = priceToInput.val();
// Perform validation
if (
value < initialRange.options.min ||
value > initialRange.options.max ||
value >= toValue ||
value > initialData.to // Check if fromValue is higher than the current toValue
) {
value = defaultFrom;
}
rangeSlider.data("ionRangeSlider").update({
from: value
});
priceFromInput.val(value);
});
priceToInput.on("blur", function() {
var value = $(this).val();
var fromValue = priceFromInput.val();
// Perform validation
if (
value < initialRange.options.min ||
value > initialRange.options.max ||
value <= fromValue ||
value < initialData.from // Check if toValue is lower than the current fromValue
) {
value = defaultTo;
}
rangeSlider.data("ionRangeSlider").update({
to: value
});
priceToInput.val(value);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #58 v0.1 💙 RANGE SLIDER INPUT -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/css/ion.rangeSlider.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/js/ion.rangeSlider.min.js"></script>
<style>
.irs {
font-family: inherit;
}
</style>
<script>
$(document).ready(function() {
var rangeSlider = $('[ms-code-input="range-slider"]');
var priceFromInput = $('[ms-code-input="price-from"]');
var priceToInput = $('[ms-code-input="price-to"]');
// Set the default range values
var defaultFrom = 20000;
var defaultTo = 50000;
rangeSlider.ionRangeSlider({
skin: "round", // You can also try "flat", "big", "modern", "sharp", or "square". Default styles can be overridden with CSS.
type: "double",
grid: true,
min: 0,
max: 100000,
from: defaultFrom,
to: defaultTo,
prefix: "$",
onStart: function(data) {
priceFromInput.val(data.from);
priceToInput.val(data.to);
},
onChange: function(data) {
priceFromInput.val(data.from);
priceToInput.val(data.to);
}
});
// Get the initial range values and update the inputs
var initialRange = rangeSlider.data("ionRangeSlider");
var initialData = initialRange.result;
priceFromInput.val(initialData.from);
priceToInput.val(initialData.to);
// Update the range slider and inputs when the inputs lose focus
priceFromInput.on("blur", function() {
var value = $(this).val();
var toValue = priceToInput.val();
// Perform validation
if (
value < initialRange.options.min ||
value > initialRange.options.max ||
value >= toValue ||
value > initialData.to // Check if fromValue is higher than the current toValue
) {
value = defaultFrom;
}
rangeSlider.data("ionRangeSlider").update({
from: value
});
priceFromInput.val(value);
});
priceToInput.on("blur", function() {
var value = $(this).val();
var fromValue = priceFromInput.val();
// Perform validation
if (
value < initialRange.options.min ||
value > initialRange.options.max ||
value <= fromValue ||
value < initialData.from // Check if toValue is lower than the current fromValue
) {
value = defaultTo;
}
rangeSlider.data("ionRangeSlider").update({
to: value
});
priceToInput.val(value);
});
});
</script>

#57 - Entrada del selector de tiempo
Añada un selector de hora a su formulario y rellene previamente la hora en un campo.
<!-- 💙 MEMBERSCRIPT #57 v0.1 💙 TIME PICKER -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.css">
<script src="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.js"> </script>
<script>
$(document).ready(function() {
var tpSpinbox = new tui.TimePicker(document.querySelector('[ms-code-timepicker="box"]'), {
inputType: 'spinbox',
showMeridiem: true // If you don't use AM/PM remove this line
});
// Setup an event handler for when the time is selected
tpSpinbox.on('change', function() {
// Get the selected time
var hour = tpSpinbox.getHour();
var minute = tpSpinbox.getMinute();
var selectedTime = hour + ':' + (minute < 10 ? '0' : '') + minute;
// Update the value of the input element
document.querySelector('[ms-code-timepicker="input"]').value = selectedTime;
});
});
</script>
<!-- 💙 MEMBERSCRIPT #57 v0.1 💙 TIME PICKER -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.css">
<script src="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.js"> </script>
<script>
$(document).ready(function() {
var tpSpinbox = new tui.TimePicker(document.querySelector('[ms-code-timepicker="box"]'), {
inputType: 'spinbox',
showMeridiem: true // If you don't use AM/PM remove this line
});
// Setup an event handler for when the time is selected
tpSpinbox.on('change', function() {
// Get the selected time
var hour = tpSpinbox.getHour();
var minute = tpSpinbox.getMinute();
var selectedTime = hour + ':' + (minute < 10 ? '0' : '') + minute;
// Update the value of the input element
document.querySelector('[ms-code-timepicker="input"]').value = selectedTime;
});
});
</script>

#56 - Pares de opciones de entrada
Combine los valores de varias entradas en un solo campo.
<!-- 💙 MEMBERSCRIPT #56 v0.1 💙 INPUT OPTION PAIRS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script>
$(document).ready(function() {
var groups = {};
// Get all inputs with the attribute ms-code-combine-inputs
var inputs = $('input[ms-code-combine-inputs], select[ms-code-combine-inputs]');
// For each input
inputs.each(function() {
// Split the attribute value at the dash
var parts = $(this).attr('ms-code-combine-inputs').split('-');
// If the group doesn't exist yet, create it
if (!groups[parts[0]]) {
groups[parts[0]] = {
targets: [],
values: [],
};
}
// If it's a target, add it to the targets
if (parts[1] == 'target') {
groups[parts[0]].targets.push($(this));
} else {
// It's an input, add it to the values and attach a listener
groups[parts[0]].values.push($(this));
$(this).on('input change', function() {
// On input or change, combine all values with a space in between
// and set the targets' value
var combinedValue = '';
$.each(groups[parts[0]].values, function(index, value) {
combinedValue += $(this).val();
if (index < groups[parts[0]].values.length - 1) {
combinedValue += ' '; // Add a space between values
}
});
$.each(groups[parts[0]].targets, function() {
$(this).val(combinedValue);
});
});
}
});
});
</script>
<!-- 💙 MEMBERSCRIPT #56 v0.1 💙 INPUT OPTION PAIRS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script>
$(document).ready(function() {
var groups = {};
// Get all inputs with the attribute ms-code-combine-inputs
var inputs = $('input[ms-code-combine-inputs], select[ms-code-combine-inputs]');
// For each input
inputs.each(function() {
// Split the attribute value at the dash
var parts = $(this).attr('ms-code-combine-inputs').split('-');
// If the group doesn't exist yet, create it
if (!groups[parts[0]]) {
groups[parts[0]] = {
targets: [],
values: [],
};
}
// If it's a target, add it to the targets
if (parts[1] == 'target') {
groups[parts[0]].targets.push($(this));
} else {
// It's an input, add it to the values and attach a listener
groups[parts[0]].values.push($(this));
$(this).on('input change', function() {
// On input or change, combine all values with a space in between
// and set the targets' value
var combinedValue = '';
$.each(groups[parts[0]].values, function(index, value) {
combinedValue += $(this).val();
if (index < groups[parts[0]].values.length - 1) {
combinedValue += ' '; // Add a space between values
}
});
$.each(groups[parts[0]].targets, function() {
$(this).val(combinedValue);
});
});
}
});
});
</script>

#55 - Cambiar el estilo de las casillas de verificación
Cambiar los estilos de otro elemento cuando se marca una casilla de verificación.
<!-- 💙 MEMBERSCRIPT #55 v0.1 💙 UPDATE CHECKBOX PARENT STYLES -->
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
// Get all the checkbox elements
var checkboxes = document.querySelectorAll('[ms-code-checkbox="check"]');
// Iterate over each checkbox element
checkboxes.forEach(function(checkbox) {
// Get the boolean wrap element associated with the current checkbox
var booleanWrap = checkbox.closest('[ms-code-checkbox="wrap"]');
// Function to update the boolean wrap class based on checkbox state
function updateBooleanWrapClass() {
if (checkbox.checked) {
booleanWrap.classList.add('checked');
} else {
booleanWrap.classList.remove('checked');
}
}
// Check the initial value of the checkbox
updateBooleanWrapClass();
// Add an event listener to the checkbox to handle changes
checkbox.addEventListener('change', function() {
updateBooleanWrapClass();
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #55 v0.1 💙 UPDATE CHECKBOX PARENT STYLES -->
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
// Get all the checkbox elements
var checkboxes = document.querySelectorAll('[ms-code-checkbox="check"]');
// Iterate over each checkbox element
checkboxes.forEach(function(checkbox) {
// Get the boolean wrap element associated with the current checkbox
var booleanWrap = checkbox.closest('[ms-code-checkbox="wrap"]');
// Function to update the boolean wrap class based on checkbox state
function updateBooleanWrapClass() {
if (checkbox.checked) {
booleanWrap.classList.add('checked');
} else {
booleanWrap.classList.remove('checked');
}
}
// Check the initial value of the checkbox
updateBooleanWrapClass();
// Add an event listener to the checkbox to handle changes
checkbox.addEventListener('change', function() {
updateBooleanWrapClass();
});
});
});
</script>

#54 - Lógica del campo de formulario de casilla de verificación
Bloquear otros campos/elementos si una casilla de verificación no está marcada.
<!-- 💙 MEMBERSCRIPT #54 v0.1 💙 CHECKBOX FIELD FORM LOGIC -->
<style>
.disabled {
pointer-events: none;
opacity: 0.5;
}
</style>
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
// Get all the trigger checkboxes
var triggerCheckboxes = document.querySelectorAll('[ms-code-field-logic-trigger]');
// Iterate over each trigger checkbox
triggerCheckboxes.forEach(function(checkbox) {
// Get the value of the trigger checkbox's attribute
var triggerValue = checkbox.getAttribute('ms-code-field-logic-trigger');
// Function to update the target elements' class based on checkbox state
function updateTargetElementsClass() {
// Find the associated target elements based on the attribute value
var targetElements = document.querySelectorAll('[ms-code-field-logic-target="' + triggerValue + '"]');
// Check the new value of the trigger checkbox
if (!checkbox.checked) {
// Add the "disabled" class to each target element
targetElements.forEach(function(targetElement) {
targetElement.classList.add('disabled');
});
} else {
// Remove the "disabled" class from each target element
targetElements.forEach(function(targetElement) {
targetElement.classList.remove('disabled');
});
}
}
// Check the initial value of the trigger checkbox
updateTargetElementsClass();
// Add an event listener to the trigger checkbox to handle changes
checkbox.addEventListener('change', function() {
updateTargetElementsClass();
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #54 v0.1 💙 CHECKBOX FIELD FORM LOGIC -->
<style>
.disabled {
pointer-events: none;
opacity: 0.5;
}
</style>
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
// Get all the trigger checkboxes
var triggerCheckboxes = document.querySelectorAll('[ms-code-field-logic-trigger]');
// Iterate over each trigger checkbox
triggerCheckboxes.forEach(function(checkbox) {
// Get the value of the trigger checkbox's attribute
var triggerValue = checkbox.getAttribute('ms-code-field-logic-trigger');
// Function to update the target elements' class based on checkbox state
function updateTargetElementsClass() {
// Find the associated target elements based on the attribute value
var targetElements = document.querySelectorAll('[ms-code-field-logic-target="' + triggerValue + '"]');
// Check the new value of the trigger checkbox
if (!checkbox.checked) {
// Add the "disabled" class to each target element
targetElements.forEach(function(targetElement) {
targetElement.classList.add('disabled');
});
} else {
// Remove the "disabled" class from each target element
targetElements.forEach(function(targetElement) {
targetElement.classList.remove('disabled');
});
}
}
// Check the initial value of the trigger checkbox
updateTargetElementsClass();
// Add an event listener to the trigger checkbox to handle changes
checkbox.addEventListener('change', function() {
updateTargetElementsClass();
});
});
});
</script>

#53 - Actualizar elementos JSON con un formulario
Permita que sus miembros cambien detalles sobre sus artículos JSON.
<!-- 💙 MEMBERSCRIPT #53 v0.1 💙 UPDATE JSON ITEMS WITH A FORM -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const memberstack = window.$memberstackDom;
// Add click event listener to the document
document.addEventListener("click", async function(event) {
const target = event.target;
// Check if the clicked element has ms-code-edit-item attribute
const editItem = target.closest('[ms-code-edit-item="prompt"]');
if (editItem) {
// Get the item key from the closest ancestor element with ms-code-item-key attribute
const key = editItem.closest('[ms-code-item-key]').getAttribute('ms-code-item-key');
// Retrieve the current member JSON data
const member = await memberstack.getMemberJSON();
// SET THE TARGET - EDIT ME
let targetObject = member.data.projects; // Update this line with the desired target location
if (member.data && targetObject && targetObject[key]) {
// Get the form element with the ms-code-edit-item="form" attribute
const form = document.querySelector('form[ms-code-edit-item="form"]');
if (form) {
// Loop through the form fields
for (const field of form.elements) {
const jsonName = field.getAttribute('ms-code-json-name');
if (jsonName && targetObject[key].hasOwnProperty(jsonName)) {
// Pre-fill the form field with the corresponding value from the JSON item
field.value = targetObject[key][jsonName];
}
}
// Get the modal element with the ms-code-edit-item="modal" attribute
const modal = document.querySelector('[ms-code-edit-item="modal"]');
if (modal) {
// Set the display property of the modal to flex
modal.style.display = 'flex';
}
// Add submit event listener to the form
form.addEventListener("submit", async function(event) {
event.preventDefault(); // Prevent the form from submitting normally
// Create an object to hold the updated values
const updatedValues = {};
// Loop through the form fields
for (const field of form.elements) {
const jsonName = field.getAttribute('ms-code-json-name');
if (jsonName) {
// Update the corresponding value in the updatedValues object
updatedValues[jsonName] = field.value;
}
}
// Update the target object with the new values
targetObject[key] = { ...targetObject[key], ...updatedValues };
// Update the member JSON using the Memberstack SDK
await memberstack.updateMemberJSON({
json: member.data
});
// Optional: Display a success message or perform any other desired action
console.log('Member JSON updated successfully');
});
} else {
console.error('Form element not found');
}
} else {
console.error(`Could not find item with key: ${key}`);
}
}
});
});
</script>
<!-- 💙 MEMBERSCRIPT #53 v0.1 💙 UPDATE JSON ITEMS WITH A FORM -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const memberstack = window.$memberstackDom;
// Add click event listener to the document
document.addEventListener("click", async function(event) {
const target = event.target;
// Check if the clicked element has ms-code-edit-item attribute
const editItem = target.closest('[ms-code-edit-item="prompt"]');
if (editItem) {
// Get the item key from the closest ancestor element with ms-code-item-key attribute
const key = editItem.closest('[ms-code-item-key]').getAttribute('ms-code-item-key');
// Retrieve the current member JSON data
const member = await memberstack.getMemberJSON();
// SET THE TARGET - EDIT ME
let targetObject = member.data.projects; // Update this line with the desired target location
if (member.data && targetObject && targetObject[key]) {
// Get the form element with the ms-code-edit-item="form" attribute
const form = document.querySelector('form[ms-code-edit-item="form"]');
if (form) {
// Loop through the form fields
for (const field of form.elements) {
const jsonName = field.getAttribute('ms-code-json-name');
if (jsonName && targetObject[key].hasOwnProperty(jsonName)) {
// Pre-fill the form field with the corresponding value from the JSON item
field.value = targetObject[key][jsonName];
}
}
// Get the modal element with the ms-code-edit-item="modal" attribute
const modal = document.querySelector('[ms-code-edit-item="modal"]');
if (modal) {
// Set the display property of the modal to flex
modal.style.display = 'flex';
}
// Add submit event listener to the form
form.addEventListener("submit", async function(event) {
event.preventDefault(); // Prevent the form from submitting normally
// Create an object to hold the updated values
const updatedValues = {};
// Loop through the form fields
for (const field of form.elements) {
const jsonName = field.getAttribute('ms-code-json-name');
if (jsonName) {
// Update the corresponding value in the updatedValues object
updatedValues[jsonName] = field.value;
}
}
// Update the target object with the new values
targetObject[key] = { ...targetObject[key], ...updatedValues };
// Update the member JSON using the Memberstack SDK
await memberstack.updateMemberJSON({
json: member.data
});
// Optional: Display a success message or perform any other desired action
console.log('Member JSON updated successfully');
});
} else {
console.error('Form element not found');
}
} else {
console.error(`Could not find item with key: ${key}`);
}
}
});
});
</script>

#52 - Retraso en la redirección de la página
Redirigir a los miembros a una nueva página con un retraso opcional.
<!-- 💙 MEMBERSCRIPT #52 v0.1 💙 DELAYED REDIRECT TO NEW PAGE -->
<script>
const redirectToNewPage = function() {
setTimeout(function() {
window.location.href = "/your-page";
}, 1000); // Delay in milliseconds
};
redirectToNewPage();
</script>
<!-- 💙 MEMBERSCRIPT #52 v0.1 💙 DELAYED REDIRECT TO NEW PAGE -->
<script>
const redirectToNewPage = function() {
setTimeout(function() {
window.location.href = "/your-page";
}, 1000); // Delay in milliseconds
};
redirectToNewPage();
</script>

#51 - Mostrar metadatos de los miembros
Muestre los metadatos de los afiliados de forma dinámica en su sitio web.
<!-- 💙 MEMBERSCRIPT #51 v0.2 💙 DISPLAY MEMBER METADATA -->
<script>
function replaceTextWithMetadata(metadata) {
var els = Array.from(document.querySelectorAll('[ms-code-member-meta]'));
els.forEach((el) => {
const key = el.getAttribute('ms-code-member-meta');
const value = metadata[key];
if (value !== undefined) {
el.innerHTML = value;
el.value = value;
el.src = value;
}
});
}
const memberstack = window.$memberstackDom;
memberstack.getCurrentMember()
.then(({ data: member }) => {
if (member && member.metaData) {
replaceTextWithMetadata(member.metaData);
}
})
.catch((error) => {
console.error('Error retrieving member data:', error);
});
</script>
<!-- 💙 MEMBERSCRIPT #51 v0.2 💙 DISPLAY MEMBER METADATA -->
<script>
function replaceTextWithMetadata(metadata) {
var els = Array.from(document.querySelectorAll('[ms-code-member-meta]'));
els.forEach((el) => {
const key = el.getAttribute('ms-code-member-meta');
const value = metadata[key];
if (value !== undefined) {
el.innerHTML = value;
el.value = value;
el.src = value;
}
});
}
const memberstack = window.$memberstackDom;
memberstack.getCurrentMember()
.then(({ data: member }) => {
if (member && member.metaData) {
replaceTextWithMetadata(member.metaData);
}
})
.catch((error) => {
console.error('Error retrieving member data:', error);
});
</script>

#50 - Modo oscuro entre dispositivos
Opción de modo oscuro persistente que sigue funcionando en los diferentes dispositivos de sus miembros.
Código de la cabeza
Put this in the <head> section of your site.
<!-- 💙 MEMBERSCRIPT #50 HEAD CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const themePreference = localStorage.getItem('themePreference');
if (themePreference === 'dark') {
document.body.classList.add('dark');
}
});
</script>
Código del cuerpo
Put this in the </body> section of your site.
<!-- 💙 MEMBERSCRIPT #50 BODY CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const darkModeToggle = document.querySelector('[ms-code-dark-mode="toggle"]');
const bodyElement = document.querySelector('.body');
// Function to check if theme preference is saved in member JSON
function checkThemePreference() {
const memberstack = window.$memberstackDom;
memberstack.getMemberJSON()
.then(function(memberData) {
const themePreference = memberData.data?.themePreference;
if (themePreference === 'dark') {
enableDarkMode();
} else {
disableDarkMode();
}
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
}
// Function to enable dark mode
function enableDarkMode() {
darkModeToggle.classList.add('dark');
bodyElement.classList.add('dark');
updateThemePreference('dark');
}
// Function to disable dark mode
function disableDarkMode() {
darkModeToggle.classList.remove('dark');
bodyElement.classList.remove('dark');
updateThemePreference('light');
}
// Function to update theme preference in member JSON
function updateThemePreference(themePreference) {
const memberstack = window.$memberstackDom;
memberstack.getMemberJSON()
.then(function(memberData) {
memberData.data = memberData.data || {};
memberData.data.themePreference = themePreference;
memberstack.updateMemberJSON({ json: memberData.data })
.then(function() {
localStorage.setItem('themePreference', themePreference);
})
.catch(function(error) {
console.error('Error updating member data:', error);
});
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
}
// Event listener for dark mode toggle
darkModeToggle.addEventListener('click', function() {
if (darkModeToggle.classList.contains('dark')) {
disableDarkMode();
} else {
enableDarkMode();
}
});
// Apply transition duration and timing to all elements
const transitionDuration = '0.0s';
const transitionTiming = 'ease';
const elementsToTransition = [darkModeToggle, bodyElement];
elementsToTransition.forEach(function(element) {
element.style.transitionDuration = transitionDuration;
element.style.transitionTimingFunction = transitionTiming;
});
// Check theme preference on page load
const savedThemePreference = localStorage.getItem('themePreference');
if (savedThemePreference === 'dark') {
enableDarkMode();
} else {
disableDarkMode();
}
checkThemePreference();
});
</script>
Código de la cabeza
Put this in the <head> section of your site.
<!-- 💙 MEMBERSCRIPT #50 HEAD CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const themePreference = localStorage.getItem('themePreference');
if (themePreference === 'dark') {
document.body.classList.add('dark');
}
});
</script>
Código del cuerpo
Put this in the </body> section of your site.
<!-- 💙 MEMBERSCRIPT #50 BODY CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const darkModeToggle = document.querySelector('[ms-code-dark-mode="toggle"]');
const bodyElement = document.querySelector('.body');
// Function to check if theme preference is saved in member JSON
function checkThemePreference() {
const memberstack = window.$memberstackDom;
memberstack.getMemberJSON()
.then(function(memberData) {
const themePreference = memberData.data?.themePreference;
if (themePreference === 'dark') {
enableDarkMode();
} else {
disableDarkMode();
}
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
}
// Function to enable dark mode
function enableDarkMode() {
darkModeToggle.classList.add('dark');
bodyElement.classList.add('dark');
updateThemePreference('dark');
}
// Function to disable dark mode
function disableDarkMode() {
darkModeToggle.classList.remove('dark');
bodyElement.classList.remove('dark');
updateThemePreference('light');
}
// Function to update theme preference in member JSON
function updateThemePreference(themePreference) {
const memberstack = window.$memberstackDom;
memberstack.getMemberJSON()
.then(function(memberData) {
memberData.data = memberData.data || {};
memberData.data.themePreference = themePreference;
memberstack.updateMemberJSON({ json: memberData.data })
.then(function() {
localStorage.setItem('themePreference', themePreference);
})
.catch(function(error) {
console.error('Error updating member data:', error);
});
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
}
// Event listener for dark mode toggle
darkModeToggle.addEventListener('click', function() {
if (darkModeToggle.classList.contains('dark')) {
disableDarkMode();
} else {
enableDarkMode();
}
});
// Apply transition duration and timing to all elements
const transitionDuration = '0.0s';
const transitionTiming = 'ease';
const elementsToTransition = [darkModeToggle, bodyElement];
elementsToTransition.forEach(function(element) {
element.style.transitionDuration = transitionDuration;
element.style.transitionTimingFunction = transitionTiming;
});
// Check theme preference on page load
const savedThemePreference = localStorage.getItem('themePreference');
if (savedThemePreference === 'dark') {
enableDarkMode();
} else {
disableDarkMode();
}
checkThemePreference();
});
</script>

#49 - Desactivar la primera opción en una entrada de selección
Evite que los usuarios seleccionen la opción de marcador de posición en sus entradas de selección.
<!-- 💙 MEMBERSCRIPT #49 v0.1 💙 DISABLE FIRST OPTION IN SELECT INPUT -->
<script>
let selects = document.querySelectorAll("select[ms-code=hide-first-option]");
selects.forEach((select) => {
let options = select.getElementsByTagName("option");
options[0].hidden = true;
});
</script>
<!-- 💙 MEMBERSCRIPT #49 v0.1 💙 DISABLE FIRST OPTION IN SELECT INPUT -->
<script>
let selects = document.querySelectorAll("select[ms-code=hide-first-option]");
selects.forEach((select) => {
let options = select.getElementsByTagName("option");
options[0].hidden = true;
});
</script>

#48 - Autocompletar entradas de dirección
Rellene previamente todas las entradas de direcciones mediante la API de Google Places.
Código de la cabeza
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #48 HEAD CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR-API-KEY&libraries=places&callback=initAutocomplete" async defer> </script>
<style>
.pac-logo::after {
display: none;
}
.pac-container {
border-radius: 5px;
border: 1px solid #ccc;
}
.pac-item {
padding: 0 10px;
}
</style>
Código del cuerpo
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #48 BODY CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script>
let autocomplete;
function initAutocomplete() {
autocomplete = new google.maps.places.Autocomplete(
document.querySelector('input[ms-code-input="address"]'),
{
componentRestrictions: { country: ['US'] },
fields: ['address_components'],
types: ['address']
}
);
autocomplete.addListener('place_changed', function() {
const place = autocomplete.getPlace();
if (place) {
const addressInput = document.querySelector('input[ms-code-input="address"]');
const cityInput = document.querySelector('input[ms-code-input="city"]');
const regionInput = document.querySelector('input[ms-code-input="region"]');
const countryInput = document.querySelector('input[ms-code-input="country"]');
const postalCodeInput = document.querySelector('input[ms-code-input="postal-code"]');
addressInput.value = extractAddress(place);
cityInput.value = extractCity(place);
regionInput.value = extractRegion(place);
countryInput.value = extractCountry(place);
postalCodeInput.value = extractPostalCode(place);
}
});
}
function extractAddress(place) {
let address = '';
const streetNumber = extractComponent(place, 'street_number');
const route = extractComponent(place, 'route');
if (streetNumber) {
address += streetNumber + ' ';
}
if (route) {
address += route;
}
return address.trim();
}
function extractComponent(place, componentType) {
for (const component of place.address_components) {
if (component.types.includes(componentType)) {
return component.long_name;
}
}
return '';
}
function extractCity(place) {
for (const component of place.address_components) {
if (component.types.includes('locality')) {
return component.long_name;
}
}
return '';
}
function extractRegion(place) {
for (const component of place.address_components) {
if (component.types.includes('administrative_area_level_1')) {
return component.long_name;
}
}
return '';
}
function extractCountry(place) {
for (const component of place.address_components) {
if (component.types.includes('country')) {
return component.long_name;
}
}
return '';
}
function extractPostalCode(place) {
for (const component of place.address_components) {
if (component.types.includes('postal_code')) {
return component.long_name;
}
}
return '';
}
</script>
Código de la cabeza
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #48 HEAD CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR-API-KEY&libraries=places&callback=initAutocomplete" async defer> </script>
<style>
.pac-logo::after {
display: none;
}
.pac-container {
border-radius: 5px;
border: 1px solid #ccc;
}
.pac-item {
padding: 0 10px;
}
</style>
Código del cuerpo
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #48 BODY CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script>
let autocomplete;
function initAutocomplete() {
autocomplete = new google.maps.places.Autocomplete(
document.querySelector('input[ms-code-input="address"]'),
{
componentRestrictions: { country: ['US'] },
fields: ['address_components'],
types: ['address']
}
);
autocomplete.addListener('place_changed', function() {
const place = autocomplete.getPlace();
if (place) {
const addressInput = document.querySelector('input[ms-code-input="address"]');
const cityInput = document.querySelector('input[ms-code-input="city"]');
const regionInput = document.querySelector('input[ms-code-input="region"]');
const countryInput = document.querySelector('input[ms-code-input="country"]');
const postalCodeInput = document.querySelector('input[ms-code-input="postal-code"]');
addressInput.value = extractAddress(place);
cityInput.value = extractCity(place);
regionInput.value = extractRegion(place);
countryInput.value = extractCountry(place);
postalCodeInput.value = extractPostalCode(place);
}
});
}
function extractAddress(place) {
let address = '';
const streetNumber = extractComponent(place, 'street_number');
const route = extractComponent(place, 'route');
if (streetNumber) {
address += streetNumber + ' ';
}
if (route) {
address += route;
}
return address.trim();
}
function extractComponent(place, componentType) {
for (const component of place.address_components) {
if (component.types.includes(componentType)) {
return component.long_name;
}
}
return '';
}
function extractCity(place) {
for (const component of place.address_components) {
if (component.types.includes('locality')) {
return component.long_name;
}
}
return '';
}
function extractRegion(place) {
for (const component of place.address_components) {
if (component.types.includes('administrative_area_level_1')) {
return component.long_name;
}
}
return '';
}
function extractCountry(place) {
for (const component of place.address_components) {
if (component.types.includes('country')) {
return component.long_name;
}
}
return '';
}
function extractPostalCode(place) {
for (const component of place.address_components) {
if (component.types.includes('postal_code')) {
return component.long_name;
}
}
return '';
}
</script>

#47 - Mostrar fecha del miembro JSON
Indique a los afiliados una fecha, por ejemplo, cuándo vence su plan.
<!-- 💙 MEMBERSCRIPT #47 v0.1 💙 DISPLAY ONE TIME DATE -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
const memberstack = window.$memberstackDom;
const formatDate = function(date) {
const options = { month: 'long', day: 'numeric', year: 'numeric' };
return new Date(date).toLocaleDateString('en-US', options);
// Replace 'en-US' with one of these depending on your locale: en-US, en-GB, en-CA, en-AU, fr-FR, de-DE, es-ES, it-IT, ja-JP, ko-KR, pt-BR, ru-RU, zn-CH, ar-SA
};
const updateTextSpans = async function() {
const member = await memberstack.getMemberJSON();
if (!member.data || !member.data['one-time-date']) {
// Member data or one-time date not available, do nothing
return;
}
const oneTimeDate = formatDate(member.data['one-time-date']);
const textSpans = document.querySelectorAll('[ms-code-display-text="one-time-date"]');
textSpans.forEach(span => {
span.textContent = oneTimeDate;
});
};
updateTextSpans();
});
</script>
<!-- 💙 MEMBERSCRIPT #47 v0.1 💙 DISPLAY ONE TIME DATE -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
const memberstack = window.$memberstackDom;
const formatDate = function(date) {
const options = { month: 'long', day: 'numeric', year: 'numeric' };
return new Date(date).toLocaleDateString('en-US', options);
// Replace 'en-US' with one of these depending on your locale: en-US, en-GB, en-CA, en-AU, fr-FR, de-DE, es-ES, it-IT, ja-JP, ko-KR, pt-BR, ru-RU, zn-CH, ar-SA
};
const updateTextSpans = async function() {
const member = await memberstack.getMemberJSON();
if (!member.data || !member.data['one-time-date']) {
// Member data or one-time date not available, do nothing
return;
}
const oneTimeDate = formatDate(member.data['one-time-date']);
const textSpans = document.querySelectorAll('[ms-code-display-text="one-time-date"]');
textSpans.forEach(span => {
span.textContent = oneTimeDate;
});
};
updateTextSpans();
});
</script>

#46 - Confirmar contraseña
Añade una entrada de confirmación de contraseña a tus formularios de registro y restablecimiento de contraseña.
<!-- 💙 MEMBERSCRIPT #46 v0.1 💙 CONFIRM PASSWORD INPUT -->
<script>
var password = document.querySelector('[data-ms-member=password]')
, confirm_password = document.querySelector('[ms-code-password=confirm]')
function validatePassword(){
if(password.value != confirm_password.value) {
confirm_password.setCustomValidity("Passwords Don't Match");
confirm_password.classList.add("invalid")
confirm_password.classList.remove("valid")
} else {
confirm_password.setCustomValidity('');
confirm_password.classList.remove("invalid")
confirm_password.classList.add("valid")
}
}
password.onchange = validatePassword;
confirm_password.onkeyup = validatePassword;
</script>
<!-- 💙 MEMBERSCRIPT #46 v0.1 💙 CONFIRM PASSWORD INPUT -->
<script>
var password = document.querySelector('[data-ms-member=password]')
, confirm_password = document.querySelector('[ms-code-password=confirm]')
function validatePassword(){
if(password.value != confirm_password.value) {
confirm_password.setCustomValidity("Passwords Don't Match");
confirm_password.classList.add("invalid")
confirm_password.classList.remove("valid")
} else {
confirm_password.setCustomValidity('');
confirm_password.classList.remove("invalid")
confirm_password.classList.add("valid")
}
}
password.onchange = validatePassword;
confirm_password.onkeyup = validatePassword;
</script>

#45 - Mostrar/Ocultar contraseña
Añade un botón de mostrar/ocultar contraseña a cualquier formulario con una entrada de contraseña.
<!-- 💙 MEMBERSCRIPT #45 v0.2 💙 SHOW AND HIDE PASSWORD -->
<script>
document.querySelectorAll("[ms-code-password='transform']").forEach(function(button) {
button.addEventListener("click", transform);
});
var isPassword = true;
function transform() {
var passwordInputs = document.querySelectorAll("[data-ms-member='password'], [data-ms-member='new-password'], [data-ms-member='current-password']");
passwordInputs.forEach(function(myInput) {
var inputType = myInput.getAttribute("type");
if (isPassword) {
myInput.setAttribute("type", "text");
} else {
myInput.setAttribute("type", "password");
}
});
isPassword = !isPassword;
}
</script>
<!-- 💙 MEMBERSCRIPT #45 v0.2 💙 SHOW AND HIDE PASSWORD -->
<script>
document.querySelectorAll("[ms-code-password='transform']").forEach(function(button) {
button.addEventListener("click", transform);
});
var isPassword = true;
function transform() {
var passwordInputs = document.querySelectorAll("[data-ms-member='password'], [data-ms-member='new-password'], [data-ms-member='current-password']");
passwordInputs.forEach(function(myInput) {
var inputType = myInput.getAttribute("type");
if (isPassword) {
myInput.setAttribute("type", "text");
} else {
myInput.setAttribute("type", "password");
}
});
isPassword = !isPassword;
}
</script>

#44 - Mostrar elemento si atributo coincide con ID de miembro
Mostrar elementos condicionalmente si tienen un atributo que coincida con el ID de los miembros.
<!-- 💙 MEMBERSCRIPT #44 v0.1 💙 SHOW ELEMENT IF ATTRIBUTE MATCHES MEMBER ID -->
<script>
document.addEventListener("DOMContentLoaded", function() {
if (localStorage.getItem("_ms-mem")) {
const memberData = JSON.parse(localStorage.getItem("_ms-mem"));
const memberId = memberData.id;
const elements = document.querySelectorAll("[ms-code-member-id='" + memberId + "']");
elements.forEach(element => {
element.style.display = "block";
});
}
});
</script>
<!-- 💙 MEMBERSCRIPT #44 v0.1 💙 SHOW ELEMENT IF ATTRIBUTE MATCHES MEMBER ID -->
<script>
document.addEventListener("DOMContentLoaded", function() {
if (localStorage.getItem("_ms-mem")) {
const memberData = JSON.parse(localStorage.getItem("_ms-mem"));
const memberId = memberData.id;
const elements = document.querySelectorAll("[ms-code-member-id='" + memberId + "']");
elements.forEach(element => {
element.style.display = "block";
});
}
});
</script>

#43 - Bloquear el desplazamiento cuando el modal está abierto
Evitar que la página se desplace cuando alguien abre un modal.
<!-- 💙 MEMBERSCRIPT #43 v0.1 💙 BLOCK SCROLLING WHEN MODAL IS OPEN -->
<style>
.no-scroll {
overflow: hidden;
}
</style>
<script>
function isDesktopViewport() {
return window.innerWidth >= 900; // Adjust the breakpoint width as needed
}
const codeBlocks = document.querySelectorAll('[ms-code-block-scroll]');
function handleScrollBlock(event) {
if (isDesktopViewport()) {
document.body.classList.add('no-scroll');
}
}
function handleScrollUnblock(event) {
if (isDesktopViewport()) {
document.body.classList.remove('no-scroll');
}
}
codeBlocks.forEach(codeBlock => {
codeBlock.addEventListener('mouseenter', handleScrollBlock);
codeBlock.addEventListener('mouseleave', handleScrollUnblock);
});
</script>
<!-- 💙 MEMBERSCRIPT #43 v0.1 💙 BLOCK SCROLLING WHEN MODAL IS OPEN -->
<style>
.no-scroll {
overflow: hidden;
}
</style>
<script>
function isDesktopViewport() {
return window.innerWidth >= 900; // Adjust the breakpoint width as needed
}
const codeBlocks = document.querySelectorAll('[ms-code-block-scroll]');
function handleScrollBlock(event) {
if (isDesktopViewport()) {
document.body.classList.add('no-scroll');
}
}
function handleScrollUnblock(event) {
if (isDesktopViewport()) {
document.body.classList.remove('no-scroll');
}
}
codeBlocks.forEach(codeBlock => {
codeBlock.addEventListener('mouseenter', handleScrollBlock);
codeBlock.addEventListener('mouseleave', handleScrollUnblock);
});
</script>

#42 - Campo de formulario del editor de imágenes
Permite que los usuarios suban y editen fotos y, a continuación, envíalas a Google Drive.
Código de la cabeza
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #42 HEAD CODE v0.2 💙 FILE EDITOR FEATURE -->
<link rel="stylesheet" href="https://unpkg.com/filepond@^4/dist/filepond.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" />
Código del cuerpo
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #42 BODY CODE v0.2 💙 FILE EDITOR FEATURE -->
<script> src="https://unpkg.com/filepond-plugin-file-encode/dist/filepond-plugin-file-encode.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.js"> </script>
<script> src="https://unpkg.com/filepond@^4/dist/filepond.js"> </script>
<script> src="https://scaleflex.cloudimg.io/v7/plugins/filerobot-image-editor/latest/filerobot-image-editor.min.js"> </script>
<style>
.dXhZSB {
background-color: #2962ff;
}
.FIE_root * {
font-family: inherit !important;
}
.SfxModal-Wrapper * {
font-family: inherit !important;
}
.jpHEiD {
font-family: inherit !important;
}
#editor_container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Register the plugins
FilePond.registerPlugin(FilePondPluginImagePreview);
FilePond.registerPlugin(FilePondPluginImageEdit);
const inputElement = document.querySelector('input[type="file"]');
const pond = FilePond.create(inputElement, {
credits: false,
name: 'fileToUpload',
storeAsFile: true,
imageEditEditor: {
open: (file, instructions) => {
console.log('Open editor', file, instructions);
openFilerobotImageEditor(file, instructions);
},
onconfirm: (output) => {
console.log('Confirm editor', output);
handleImageEditConfirm(output);
},
oncancel: () => {
console.log('Cancel editor');
handleImageEditCancel();
},
onclose: () => {
console.log('Close editor');
handleImageEditClose();
}
}
});
function openFilerobotImageEditor(file, instructions) {
const imageURL = URL.createObjectURL(file);
const config = {
source: imageURL,
onSave: (updatedImage) => {
confirmCallback(updatedImage);
},
annotationsCommon: {
fill: '#ff0000'
},
Text: {
text: 'Add your text here',
font: 'inherit'
}, // Set font to inherit from the page body
Rotate: {
angle: instructions.rotation,
componentType: 'slider'
},
tabsIds: [
'Adjust',
'Annotate',
'Watermark'
],
defaultTabId: 'Annotate',
defaultToolId: 'Text'
};
const editorContainer = document.createElement('div');
editorContainer.id = 'editor_container';
document.body.appendChild(editorContainer);
const filerobotImageEditor = new window.FilerobotImageEditor(editorContainer, config);
const confirmCallback = (output) => {
console.log('Confirmed:', output);
const dataURL = output.imageBase64;
const file = dataURLToFile(dataURL, output.name);
// Add the file to FilePond
pond.addFiles([file]);
document.body.removeChild(editorContainer); // Remove the editor container
};
function dataURLToFile(dataURL, fileName) {
const arr = dataURL.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const fileExtension = mime.split('/')[1];
const updatedFileName = fileName + '.' + fileExtension;
const bstr = atob(arr[1]);
const n = bstr.length;
const u8arr = new Uint8Array(n);
for (let i = 0; i < n; i++) {
u8arr[i] = bstr.charCodeAt(i);
}
return new File([u8arr], updatedFileName, { type: mime });
}
const cancelCallback = () => {
console.log('Canceled');
document.body.removeChild(editorContainer); // Remove the editor container
};
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.addEventListener('click', () => {
filerobotImageEditor.onClose();
});
const buttonContainer = document.createElement('div');
buttonContainer.appendChild(closeButton);
editorContainer.appendChild(buttonContainer);
filerobotImageEditor.render({
onClose: (closingReason) => {
console.log('Closing reason', closingReason);
filerobotImageEditor.terminate();
},
});
}
function handleImageEditConfirm(output) {
console.log('Image edit confirmed:', output);
// Handle the confirmed output here
}
function handleImageEditCancel() {
console.log('Image edit canceled');
// Handle the canceled edit here
}
function handleImageEditClose() {
console.log('Image editor closed');
// Handle the editor close here
}
});
</script>
Código de la cabeza
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #42 HEAD CODE v0.2 💙 FILE EDITOR FEATURE -->
<link rel="stylesheet" href="https://unpkg.com/filepond@^4/dist/filepond.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" />
Código del cuerpo
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #42 BODY CODE v0.2 💙 FILE EDITOR FEATURE -->
<script> src="https://unpkg.com/filepond-plugin-file-encode/dist/filepond-plugin-file-encode.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.js"> </script>
<script> src="https://unpkg.com/filepond@^4/dist/filepond.js"> </script>
<script> src="https://scaleflex.cloudimg.io/v7/plugins/filerobot-image-editor/latest/filerobot-image-editor.min.js"> </script>
<style>
.dXhZSB {
background-color: #2962ff;
}
.FIE_root * {
font-family: inherit !important;
}
.SfxModal-Wrapper * {
font-family: inherit !important;
}
.jpHEiD {
font-family: inherit !important;
}
#editor_container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Register the plugins
FilePond.registerPlugin(FilePondPluginImagePreview);
FilePond.registerPlugin(FilePondPluginImageEdit);
const inputElement = document.querySelector('input[type="file"]');
const pond = FilePond.create(inputElement, {
credits: false,
name: 'fileToUpload',
storeAsFile: true,
imageEditEditor: {
open: (file, instructions) => {
console.log('Open editor', file, instructions);
openFilerobotImageEditor(file, instructions);
},
onconfirm: (output) => {
console.log('Confirm editor', output);
handleImageEditConfirm(output);
},
oncancel: () => {
console.log('Cancel editor');
handleImageEditCancel();
},
onclose: () => {
console.log('Close editor');
handleImageEditClose();
}
}
});
function openFilerobotImageEditor(file, instructions) {
const imageURL = URL.createObjectURL(file);
const config = {
source: imageURL,
onSave: (updatedImage) => {
confirmCallback(updatedImage);
},
annotationsCommon: {
fill: '#ff0000'
},
Text: {
text: 'Add your text here',
font: 'inherit'
}, // Set font to inherit from the page body
Rotate: {
angle: instructions.rotation,
componentType: 'slider'
},
tabsIds: [
'Adjust',
'Annotate',
'Watermark'
],
defaultTabId: 'Annotate',
defaultToolId: 'Text'
};
const editorContainer = document.createElement('div');
editorContainer.id = 'editor_container';
document.body.appendChild(editorContainer);
const filerobotImageEditor = new window.FilerobotImageEditor(editorContainer, config);
const confirmCallback = (output) => {
console.log('Confirmed:', output);
const dataURL = output.imageBase64;
const file = dataURLToFile(dataURL, output.name);
// Add the file to FilePond
pond.addFiles([file]);
document.body.removeChild(editorContainer); // Remove the editor container
};
function dataURLToFile(dataURL, fileName) {
const arr = dataURL.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const fileExtension = mime.split('/')[1];
const updatedFileName = fileName + '.' + fileExtension;
const bstr = atob(arr[1]);
const n = bstr.length;
const u8arr = new Uint8Array(n);
for (let i = 0; i < n; i++) {
u8arr[i] = bstr.charCodeAt(i);
}
return new File([u8arr], updatedFileName, { type: mime });
}
const cancelCallback = () => {
console.log('Canceled');
document.body.removeChild(editorContainer); // Remove the editor container
};
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.addEventListener('click', () => {
filerobotImageEditor.onClose();
});
const buttonContainer = document.createElement('div');
buttonContainer.appendChild(closeButton);
editorContainer.appendChild(buttonContainer);
filerobotImageEditor.render({
onClose: (closingReason) => {
console.log('Closing reason', closingReason);
filerobotImageEditor.terminate();
},
});
}
function handleImageEditConfirm(output) {
console.log('Image edit confirmed:', output);
// Handle the confirmed output here
}
function handleImageEditCancel() {
console.log('Image edit canceled');
// Handle the canceled edit here
}
function handleImageEditClose() {
console.log('Image editor closed');
// Handle the editor close here
}
});
</script>

#41 - Entradas de números de teléfono perfectas
Entradas de números de teléfono internacionales, como debe ser.
Con búsqueda de IP
Utilice esta opción si desea que el país IP de los usuarios se rellene automáticamente. IMPORTANTE: No utilice esta opción con formularios de perfil o se comportará de forma errática.
<!-- 💙 MEMBERSCRIPT #41 v0.2 💙 PERFECT PHONE NUMBER INPUTS (WITH IP LOOKUP) -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/css/intlTelInput.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/intlTelInput.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"> </script>
<script>
$(document).ready(function() {
$('input[ms-code-phone-number]').each(function() {
var input = this;
var preferredCountries = $(input).attr('ms-code-phone-number').split(',');
var iti = window.intlTelInput(input, {
preferredCountries: preferredCountries,
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"
});
$.get("https://ipinfo.io", function(response) {
var countryCode = response.country;
iti.setCountry(countryCode);
}, "jsonp");
input.addEventListener('change', formatPhoneNumber);
input.addEventListener('keyup', formatPhoneNumber);
function formatPhoneNumber() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
}
var form = $(input).closest('form');
form.submit(function() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
});
});
});
</script>
Sin búsqueda de IP
Utilícelo en formularios de perfil y/o si no desea que se rellene automáticamente en función de la IP del usuario.
<!-- 💙 MEMBERSCRIPT #41 v0.2 💙 PERFECT PHONE NUMBER INPUTS (WITHOUT IP LOOKUP) -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/css/intlTelInput.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/intlTelInput.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"> </script>
<script>
$(document).ready(function() {
$('input[ms-code-phone-number]').each(function() {
var input = this;
var preferredCountries = $(input).attr('ms-code-phone-number').split(',');
var iti = window.intlTelInput(input, {
preferredCountries: preferredCountries,
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"
});
input.addEventListener('change', formatPhoneNumber);
input.addEventListener('keyup', formatPhoneNumber);
function formatPhoneNumber() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
}
var form = $(input).closest('form');
form.submit(function() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
});
});
});
</script>
Con búsqueda de IP
Utilice esta opción si desea que el país IP de los usuarios se rellene automáticamente. IMPORTANTE: No utilice esta opción con formularios de perfil o se comportará de forma errática.
<!-- 💙 MEMBERSCRIPT #41 v0.2 💙 PERFECT PHONE NUMBER INPUTS (WITH IP LOOKUP) -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/css/intlTelInput.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/intlTelInput.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"> </script>
<script>
$(document).ready(function() {
$('input[ms-code-phone-number]').each(function() {
var input = this;
var preferredCountries = $(input).attr('ms-code-phone-number').split(',');
var iti = window.intlTelInput(input, {
preferredCountries: preferredCountries,
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"
});
$.get("https://ipinfo.io", function(response) {
var countryCode = response.country;
iti.setCountry(countryCode);
}, "jsonp");
input.addEventListener('change', formatPhoneNumber);
input.addEventListener('keyup', formatPhoneNumber);
function formatPhoneNumber() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
}
var form = $(input).closest('form');
form.submit(function() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
});
});
});
</script>
Sin búsqueda de IP
Utilícelo en formularios de perfil y/o si no desea que se rellene automáticamente en función de la IP del usuario.
<!-- 💙 MEMBERSCRIPT #41 v0.2 💙 PERFECT PHONE NUMBER INPUTS (WITHOUT IP LOOKUP) -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/css/intlTelInput.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/intlTelInput.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"> </script>
<script>
$(document).ready(function() {
$('input[ms-code-phone-number]').each(function() {
var input = this;
var preferredCountries = $(input).attr('ms-code-phone-number').split(',');
var iti = window.intlTelInput(input, {
preferredCountries: preferredCountries,
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"
});
input.addEventListener('change', formatPhoneNumber);
input.addEventListener('keyup', formatPhoneNumber);
function formatPhoneNumber() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
}
var form = $(input).closest('form');
form.submit(function() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
});
});
});
</script>

#40 - Cargador de archivos de arrastrar y soltar
Añada fácilmente una función de carga de archivos mediante arrastrar y soltar a su sitio Webflow.
Importante
Si estás usando MemberScript #38 , ¡asegúrate de poner este script DESPUÉS!
<!-- 💙 MEMBERSCRIPT #40 v0.1 💙 DRAG AND DROP FILE UPLOADER -->
<script> src="https://unpkg.com/filepond@^4/dist/filepond.js"> </script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const inputElement = document.querySelector('input[type="file"]');
const pond = FilePond.create(inputElement, {
credits: false,
name: 'fileToUpload',
storeAsFile: true
// for more property options, go to https://pqina.nl/filepond/docs/api/instance/properties/
});
});
</script>
Importante
Si estás usando MemberScript #38 , ¡asegúrate de poner este script DESPUÉS!
<!-- 💙 MEMBERSCRIPT #40 v0.1 💙 DRAG AND DROP FILE UPLOADER -->
<script> src="https://unpkg.com/filepond@^4/dist/filepond.js"> </script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const inputElement = document.querySelector('input[type="file"]');
const pond = FilePond.create(inputElement, {
credits: false,
name: 'fileToUpload',
storeAsFile: true
// for more property options, go to https://pqina.nl/filepond/docs/api/instance/properties/
});
});
</script>

#39 - Seleccionar mejor los campos
Añade búsquedas y una mejor interfaz de usuario para seleccionar y multiseleccionar campos.
Código de la cabeza
Put this in the <head> section of your page.
<!-- 💙 MEMBERSCRIPT #39 v0.1 HEAD CODE 💙 BETTER SELECT FIELDS -->
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"> </script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" />
Código del cuerpo
Put this in the </body> section of your page.
<!-- 💙 MEMBERSCRIPT #39 v0.1 BODY CODE 💙 BETTER SELECT FIELDS -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"> </script>
<script>
$(document).ready(function() {
$('[ms-code-custom-select="select-with-search"]').select2();
});
</script>
Código de la cabeza
Put this in the <head> section of your page.
<!-- 💙 MEMBERSCRIPT #39 v0.1 HEAD CODE 💙 BETTER SELECT FIELDS -->
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"> </script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" />
Código del cuerpo
Put this in the </body> section of your page.
<!-- 💙 MEMBERSCRIPT #39 v0.1 BODY CODE 💙 BETTER SELECT FIELDS -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"> </script>
<script>
$(document).ready(function() {
$('[ms-code-custom-select="select-with-search"]').select2();
});
</script>
MemberScripts
Instantly add custom features to your Webflow site.
Just paste a script, set attributes, and go live.
Join the Memberstack 2.0 Slack for tips, answers, and community scripts. Please note that these are not official features and support cannot be guaranteed.

#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 v1.0 💙 - FREE MULTILINGUAL SITE WITH GOOGLE TRANSLATE -->
<script>
// 1. Inject Google Translate script dynamically
const gtScript = document.createElement('script');
gtScript.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
document.head.appendChild(gtScript);
// 2. Inject CSS to hide Google Translate 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. Google Translate init
window.googleTranslateElementInit = function () {
new google.translate.TranslateElement({
pageLanguage: 'en',
layout: google.translate.TranslateElement.FloatPosition.TOP_LEFT
}, 'google_translate_element');
};
// 4. Helper to get 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;
}
// 5. 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"]
]);
// 6. Detect current language
let currentLang = getCookie("googtrans")?.split("/").pop() || "en";
// 7. Show language-specific content & set up language switch
document.addEventListener("DOMContentLoaded", function () {
const readableLang = languageMap.get(currentLang);
const langClass = `.languagespecific.${readableLang?.toLowerCase()}specific`;
const fallbackClass = `.languagespecific.englishspecific`;
if (document.querySelector(langClass)) {
document.querySelectorAll(langClass).forEach(el => el.style.display = 'block');
} else {
document.querySelectorAll(fallbackClass).forEach(el => el.style.display = 'block');
}
document.querySelectorAll('[data-ms-code-lang-select]').forEach(el => {
el.addEventListener('click', function (e) {
e.preventDefault();
const selectedLang = this.getAttribute('data-ms-code-lang');
if (selectedLang === 'en') {
document.cookie = "googtrans=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
document.cookie = "googtrans=;domain=.webflow.io;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
window.location.hash = "";
setTimeout(() => location.reload(true), 100);
} else {
const combo = document.querySelector('.goog-te-combo');
if (combo) combo.value = selectedLang;
window.location.hash = "#googtrans(en|" + selectedLang + ")";
location.reload();
}
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #153 v1.0 💙 - FREE MULTILINGUAL SITE WITH GOOGLE TRANSLATE -->
<script>
// 1. Inject Google Translate script dynamically
const gtScript = document.createElement('script');
gtScript.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
document.head.appendChild(gtScript);
// 2. Inject CSS to hide Google Translate 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. Google Translate init
window.googleTranslateElementInit = function () {
new google.translate.TranslateElement({
pageLanguage: 'en',
layout: google.translate.TranslateElement.FloatPosition.TOP_LEFT
}, 'google_translate_element');
};
// 4. Helper to get 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;
}
// 5. 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"]
]);
// 6. Detect current language
let currentLang = getCookie("googtrans")?.split("/").pop() || "en";
// 7. Show language-specific content & set up language switch
document.addEventListener("DOMContentLoaded", function () {
const readableLang = languageMap.get(currentLang);
const langClass = `.languagespecific.${readableLang?.toLowerCase()}specific`;
const fallbackClass = `.languagespecific.englishspecific`;
if (document.querySelector(langClass)) {
document.querySelectorAll(langClass).forEach(el => el.style.display = 'block');
} else {
document.querySelectorAll(fallbackClass).forEach(el => el.style.display = 'block');
}
document.querySelectorAll('[data-ms-code-lang-select]').forEach(el => {
el.addEventListener('click', function (e) {
e.preventDefault();
const selectedLang = this.getAttribute('data-ms-code-lang');
if (selectedLang === 'en') {
document.cookie = "googtrans=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
document.cookie = "googtrans=;domain=.webflow.io;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
window.location.hash = "";
setTimeout(() => location.reload(true), 100);
} else {
const combo = document.querySelector('.goog-te-combo');
if (combo) combo.value = selectedLang;
window.location.hash = "#googtrans(en|" + selectedLang + ")";
location.reload();
}
});
});
});
</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>

#148 - Disable Webflow Form Success Window
Use this script to override Webflow's default form submission behavior by hiding the success message.
<!-- 💙 MEMBERSCRIPT #148 v0.1 💙 - DISABLE WEBFLOW FORM SUCCESS WINDOW -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.querySelector('[ms-code-form="form"]');
const successEl = document.querySelector('[ms-code-success="true"]');
if (!form || !successEl) return;
const observer = new MutationObserver(() => {
const isVisible = window.getComputedStyle(successEl).display !== 'none';
if (isVisible) {
successEl.style.display = 'none';
form.style.display = 'block';
}
});
observer.observe(successEl, { attributes: true, attributeFilter: ['style'] });
// Cleanup observer when done (optional, for performance)
form.addEventListener('w-form-success', () => {
setTimeout(() => {
observer.disconnect();
}, 100);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #148 v0.1 💙 - DISABLE WEBFLOW FORM SUCCESS WINDOW -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.querySelector('[ms-code-form="form"]');
const successEl = document.querySelector('[ms-code-success="true"]');
if (!form || !successEl) return;
const observer = new MutationObserver(() => {
const isVisible = window.getComputedStyle(successEl).display !== 'none';
if (isVisible) {
successEl.style.display = 'none';
form.style.display = 'block';
}
});
observer.observe(successEl, { attributes: true, attributeFilter: ['style'] });
// Cleanup observer when done (optional, for performance)
form.addEventListener('w-form-success', () => {
setTimeout(() => {
observer.disconnect();
}, 100);
});
});
</script>

#147 - Age Verification Popup with Cookies in Webflow
Use this script to add an age verification popup to your Webflow site.
<!-- 💙 MEMBERSCRIPT #147 v0.1 💙 - AGE VERIFICATION POPUP WITH COOKIES -->
<script>
// Simple cookie helper functions
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
}
function getCookie(name) {
const cname = name + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const cookies = decodedCookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let c = cookies[i].trim();
if (c.indexOf(cname) === 0) {
return c.substring(cname.length, c.length);
}
}
return "";
}
document.addEventListener('DOMContentLoaded', function() {
// Select the age gate element via ms-code attribute
const ageGateEl = document.querySelector('[ms-code-agegate]');
// On page load: if the cookie exists, hide the age gate
if (getCookie('ms-code-ageVerified') === 'true') {
if (ageGateEl) ageGateEl.style.display = 'none';
}
// Listen for clicks on elements with ms-code-click
document.addEventListener('click', function(event) {
if (event.target.closest('[ms-code-click="confirmAge"]')) {
setCookie('ms-code-ageVerified', 'true', 30);
if (ageGateEl) ageGateEl.style.display = 'none';
} else if (event.target.closest('[ms-code-click="denyAge"]')) {
window.location.href = 'https://age-verification-popup-with-cookies.webflow.io/access-denied'; // Change URL as needed
}
});
});
</script>
<!-- 💙 MEMBERSCRIPT #147 v0.1 💙 - AGE VERIFICATION POPUP WITH COOKIES -->
<script>
// Simple cookie helper functions
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
}
function getCookie(name) {
const cname = name + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const cookies = decodedCookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let c = cookies[i].trim();
if (c.indexOf(cname) === 0) {
return c.substring(cname.length, c.length);
}
}
return "";
}
document.addEventListener('DOMContentLoaded', function() {
// Select the age gate element via ms-code attribute
const ageGateEl = document.querySelector('[ms-code-agegate]');
// On page load: if the cookie exists, hide the age gate
if (getCookie('ms-code-ageVerified') === 'true') {
if (ageGateEl) ageGateEl.style.display = 'none';
}
// Listen for clicks on elements with ms-code-click
document.addEventListener('click', function(event) {
if (event.target.closest('[ms-code-click="confirmAge"]')) {
setCookie('ms-code-ageVerified', 'true', 30);
if (ageGateEl) ageGateEl.style.display = 'none';
} else if (event.target.closest('[ms-code-click="denyAge"]')) {
window.location.href = 'https://age-verification-popup-with-cookies.webflow.io/access-denied'; // Change URL as needed
}
});
});
</script>

#146 - Stop Videos from Playing When A Modal Closes
Automatically stop video playback when closing modals in Webflow.
<!-- 💙 MEMBERSCRIPT #146 v0.1 💙 - STOP VIDEOS FROM PLAYING WHEN A MODAL CLOSES -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Select all modals on the page
var modals = document.querySelectorAll('[data-ms-modal="modal"]');
function initializeModal(modal) {
var iframe = modal.querySelector('iframe');
if (!iframe) return; // If no iframe found, do nothing
var originalSrc = iframe.dataset.src || iframe.src;
// Function to stop video playback
function stopVideo() {
iframe.src = "";
setTimeout(() => {
iframe.src = originalSrc;
}, 100);
}
// Attach event listeners to all close buttons inside the modal
var closeBtns = modal.querySelectorAll('[data-ms-modal="close"]');
closeBtns.forEach(function(closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
stopVideo();
});
});
// Also close the modal when clicking outside the content
modal.addEventListener('click', function(e) {
if (e.target === modal) {
stopVideo();
}
});
}
// Initialize all modals
modals.forEach(initializeModal);
});
</script>
<!-- 💙 MEMBERSCRIPT #146 v0.1 💙 - STOP VIDEOS FROM PLAYING WHEN A MODAL CLOSES -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Select all modals on the page
var modals = document.querySelectorAll('[data-ms-modal="modal"]');
function initializeModal(modal) {
var iframe = modal.querySelector('iframe');
if (!iframe) return; // If no iframe found, do nothing
var originalSrc = iframe.dataset.src || iframe.src;
// Function to stop video playback
function stopVideo() {
iframe.src = "";
setTimeout(() => {
iframe.src = originalSrc;
}, 100);
}
// Attach event listeners to all close buttons inside the modal
var closeBtns = modal.querySelectorAll('[data-ms-modal="close"]');
closeBtns.forEach(function(closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
stopVideo();
});
});
// Also close the modal when clicking outside the content
modal.addEventListener('click', function(e) {
if (e.target === modal) {
stopVideo();
}
});
}
// Initialize all modals
modals.forEach(initializeModal);
});
</script>

#145 - Automatically Save & Prefill Forms
Automatically save and prefill forms in a browsers localStorage upon form submission.
<!-- 💙 MEMBERSCRIPT #145 v0.1 💙 - HOW TO PRE-FILL FORM INPUT FIELDS AT PAGE LOAD -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to store form data in localStorage
function storeFormData() {
const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
fields.forEach(function(field) {
const fieldId = field.getAttribute('ms-code-field-id');
const value = field.value.trim();
if (value) {
localStorage.setItem(fieldId, value);
}
});
}
// Function to pre-fill form fields with stored data
function preFillForm() {
const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
fields.forEach(function(field) {
const fieldId = field.getAttribute('ms-code-field-id');
const storedValue = localStorage.getItem(fieldId);
if (storedValue) {
field.value = storedValue;
}
});
}
// Handle form submission
const form = document.querySelector('#my-form, form[ms-code-form-id]');
if (form) {
form.addEventListener('submit', function(event) {
event.preventDefault(); // Prevent default form submission
storeFormData();
// Refresh the page after storing data
setTimeout(function() {
location.reload();
}, 500); // Short delay to simulate form submission
});
}
// Pre-fill form fields when the page loads
preFillForm();
});
</script>
<!-- 💙 MEMBERSCRIPT #145 v0.1 💙 - HOW TO PRE-FILL FORM INPUT FIELDS AT PAGE LOAD -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to store form data in localStorage
function storeFormData() {
const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
fields.forEach(function(field) {
const fieldId = field.getAttribute('ms-code-field-id');
const value = field.value.trim();
if (value) {
localStorage.setItem(fieldId, value);
}
});
}
// Function to pre-fill form fields with stored data
function preFillForm() {
const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
fields.forEach(function(field) {
const fieldId = field.getAttribute('ms-code-field-id');
const storedValue = localStorage.getItem(fieldId);
if (storedValue) {
field.value = storedValue;
}
});
}
// Handle form submission
const form = document.querySelector('#my-form, form[ms-code-form-id]');
if (form) {
form.addEventListener('submit', function(event) {
event.preventDefault(); // Prevent default form submission
storeFormData();
// Refresh the page after storing data
setTimeout(function() {
location.reload();
}, 500); // Short delay to simulate form submission
});
}
// Pre-fill form fields when the page loads
preFillForm();
});
</script>

#144 - Track Users Login History & Active Users
Automatically track a members login history, keep a login streak and total visits
<!-- 💙 MEMBERSCRIPT #144 v0.1 💙 - TRACK A USERS LOGIN HISTORY & ACTIVE USERS -->
<script>
(function(){
const memberstack = window.$memberstackDom;
// Helper: Execute callback when Memberstack is ready
function onMemberstackReady(cb) {
if (window.$memberstackReady) {
cb();
} else {
document.addEventListener("memberstack.ready", cb);
}
}
async function initTracking() {
// Check if a member is logged in (via localStorage)
const currentUser = JSON.parse(localStorage.getItem("_ms-mem") || "null");
if (!currentUser) {
console.warn("No logged-in member found. Tracking not applied.");
return;
}
// Retrieve member metadata
const memberJson = await memberstack.getMemberJSON();
let metadata = memberJson.data || {};
// Ensure userVisits exists as an array
metadata.userVisits = Array.isArray(metadata.userVisits) ? metadata.userVisits : [];
// Use ISO date (YYYY-MM-DD) to record one visit per day
const today = new Date().toISOString().split("T")[0];
if (!metadata.userVisits.includes(today)) {
metadata.userVisits.push(today);
}
// Helper: Compute consecutive login streak from userVisits
function computeStreak(visits) {
if (!visits.length) return 0;
// Ensure dates are unique and sorted ascending
const uniqueVisits = [...new Set(visits)].sort();
let streak = 1;
let currentDate = new Date(uniqueVisits[uniqueVisits.length - 1]);
for (let i = uniqueVisits.length - 2; i >= 0; i--) {
const prevDate = new Date(uniqueVisits[i]);
const diffDays = Math.floor((currentDate - prevDate) / (1000 * 60 * 60 * 24));
if (diffDays === 1) {
streak++;
currentDate = prevDate;
} else {
break;
}
}
return streak;
}
// Calculate the login streak and total visits
metadata.loginStreak = computeStreak(metadata.userVisits);
metadata.totalVisits = metadata.userVisits.length;
// Update Memberstack metadata
await memberstack.updateMemberJSON({ json: metadata });
console.log("User visits:", metadata.userVisits);
console.log("Login streak:", metadata.loginStreak);
console.log("Total visits:", metadata.totalVisits);
}
onMemberstackReady(initTracking);
})();
</script>
<!-- 💙 MEMBERSCRIPT #144 v0.1 💙 - TRACK A USERS LOGIN HISTORY & ACTIVE USERS -->
<script>
(function(){
const memberstack = window.$memberstackDom;
// Helper: Execute callback when Memberstack is ready
function onMemberstackReady(cb) {
if (window.$memberstackReady) {
cb();
} else {
document.addEventListener("memberstack.ready", cb);
}
}
async function initTracking() {
// Check if a member is logged in (via localStorage)
const currentUser = JSON.parse(localStorage.getItem("_ms-mem") || "null");
if (!currentUser) {
console.warn("No logged-in member found. Tracking not applied.");
return;
}
// Retrieve member metadata
const memberJson = await memberstack.getMemberJSON();
let metadata = memberJson.data || {};
// Ensure userVisits exists as an array
metadata.userVisits = Array.isArray(metadata.userVisits) ? metadata.userVisits : [];
// Use ISO date (YYYY-MM-DD) to record one visit per day
const today = new Date().toISOString().split("T")[0];
if (!metadata.userVisits.includes(today)) {
metadata.userVisits.push(today);
}
// Helper: Compute consecutive login streak from userVisits
function computeStreak(visits) {
if (!visits.length) return 0;
// Ensure dates are unique and sorted ascending
const uniqueVisits = [...new Set(visits)].sort();
let streak = 1;
let currentDate = new Date(uniqueVisits[uniqueVisits.length - 1]);
for (let i = uniqueVisits.length - 2; i >= 0; i--) {
const prevDate = new Date(uniqueVisits[i]);
const diffDays = Math.floor((currentDate - prevDate) / (1000 * 60 * 60 * 24));
if (diffDays === 1) {
streak++;
currentDate = prevDate;
} else {
break;
}
}
return streak;
}
// Calculate the login streak and total visits
metadata.loginStreak = computeStreak(metadata.userVisits);
metadata.totalVisits = metadata.userVisits.length;
// Update Memberstack metadata
await memberstack.updateMemberJSON({ json: metadata });
console.log("User visits:", metadata.userVisits);
console.log("Login streak:", metadata.loginStreak);
console.log("Total visits:", metadata.totalVisits);
}
onMemberstackReady(initTracking);
})();
</script>

#143 - Initial Based Profile Avatar
Generate a custom avatar with initials when a member has no profile picture.
<!-- 💙 MEMBERSCRIPT #143 v0.1 💙 - GENERATE INITIALS BASED AVATAR -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const checkMemberstack = setInterval(() => {
if (window.$memberstackDom) {
clearInterval(checkMemberstack);
window.$memberstackDom.getCurrentMember().then(({ data }) => {
if (!data) return console.log("No member data (logged out)");
const profileImage = document.querySelector('[data-ms-member="profile-image"]');
const avatarWrapper = document.querySelector('[data-ms-code="avatar"]');
const initialsDiv = avatarWrapper?.querySelector('.ms-avatar-initial');
if (data.profileImage) {
profileImage?.style.setProperty("display", "block");
avatarWrapper?.style.setProperty("display", "none");
} else {
profileImage?.style.setProperty("display", "none");
avatarWrapper?.style.setProperty("display", "flex");
// Get initials from available fields
const first = data.customFields["first-name"]?.trim().charAt(0).toUpperCase() || "";
const last = data.customFields["last-name"]?.trim().charAt(0).toUpperCase() || "";
let initials = first + last;
if (!initials) {
const fullName = data.customFields["name"]?.trim().split(" ") || [];
initials = fullName.length > 1
? (fullName[0].charAt(0) + fullName[1].charAt(0)).toUpperCase()
: fullName[0]?.charAt(0).toUpperCase() || "?";
}
if (initialsDiv) {
initialsDiv.textContent = initials;
} else {
avatarWrapper.innerHTML = `${initials}`;
}
}
}).catch(console.error);
}
}, 100);
});
</script>
<!-- 💙 MEMBERSCRIPT #143 v0.1 💙 - GENERATE INITIALS BASED AVATAR -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const checkMemberstack = setInterval(() => {
if (window.$memberstackDom) {
clearInterval(checkMemberstack);
window.$memberstackDom.getCurrentMember().then(({ data }) => {
if (!data) return console.log("No member data (logged out)");
const profileImage = document.querySelector('[data-ms-member="profile-image"]');
const avatarWrapper = document.querySelector('[data-ms-code="avatar"]');
const initialsDiv = avatarWrapper?.querySelector('.ms-avatar-initial');
if (data.profileImage) {
profileImage?.style.setProperty("display", "block");
avatarWrapper?.style.setProperty("display", "none");
} else {
profileImage?.style.setProperty("display", "none");
avatarWrapper?.style.setProperty("display", "flex");
// Get initials from available fields
const first = data.customFields["first-name"]?.trim().charAt(0).toUpperCase() || "";
const last = data.customFields["last-name"]?.trim().charAt(0).toUpperCase() || "";
let initials = first + last;
if (!initials) {
const fullName = data.customFields["name"]?.trim().split(" ") || [];
initials = fullName.length > 1
? (fullName[0].charAt(0) + fullName[1].charAt(0)).toUpperCase()
: fullName[0]?.charAt(0).toUpperCase() || "?";
}
if (initialsDiv) {
initialsDiv.textContent = initials;
} else {
avatarWrapper.innerHTML = `${initials}`;
}
}
}).catch(console.error);
}
}, 100);
});
</script>

#142 - Embed PDFs For Webflow
Easily embed a PDF on your Webflow site - for free, without any custom code.
<!-- 💙 MEMBERSCRIPT #142 v0.1 💙 - EMBED PDFS IN WEBFLOW -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const pdfElements = document.querySelectorAll('[ms-code-pdf-src]');
pdfElements.forEach(function (element) {
const src = element.getAttribute('ms-code-pdf-src');
const height = element.getAttribute('ms-code-pdf-height') || '500px';
const iframe = document.createElement('iframe');
iframe.src = src;
iframe.style.width = '100%';
iframe.style.height = height;
iframe.style.border = 'none';
// Set the iframe to block to remove any inline element gaps
iframe.style.display = 'block';
iframe.setAttribute('scrolling', 'auto');
element.innerHTML = '';
element.appendChild(iframe);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #142 v0.1 💙 - EMBED PDFS IN WEBFLOW -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const pdfElements = document.querySelectorAll('[ms-code-pdf-src]');
pdfElements.forEach(function (element) {
const src = element.getAttribute('ms-code-pdf-src');
const height = element.getAttribute('ms-code-pdf-height') || '500px';
const iframe = document.createElement('iframe');
iframe.src = src;
iframe.style.width = '100%';
iframe.style.height = height;
iframe.style.border = 'none';
// Set the iframe to block to remove any inline element gaps
iframe.style.display = 'block';
iframe.setAttribute('scrolling', 'auto');
element.innerHTML = '';
element.appendChild(iframe);
});
});
</script>

#141 - Iniciar la inserción en YouTube a una hora específica
Habilita enlaces compartibles y comienza a reproducir vídeos a una hora determinada.
<!-- 💙 MEMBERSCRIPT #141 v0.1 💙 - START YOUTUBE VIDEO AT SPECIFIC TIME -->
<script>
(function() {
// Function to get URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^]*)');
var results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
// Function to update YouTube embed src within Embedly iframe
function updateYouTubeEmbed(embedly_iframe, startTime) {
var embedly_src = embedly_iframe.src;
var youtube_src_match = embedly_src.match(/src=([^&]+)/);
if (youtube_src_match) {
var youtube_src = decodeURIComponent(youtube_src_match[1]);
var new_youtube_src = youtube_src.replace(/(\?|&)start=\d+/, '');
new_youtube_src += (new_youtube_src.includes('?') ? '&' : '?') + 'start=' + startTime;
var new_embedly_src = embedly_src.replace(/src=([^&]+)/, 'src=' + encodeURIComponent(new_youtube_src));
embedly_iframe.src = new_embedly_src;
}
}
// Get all elements with ms-code-yt-start attribute
var elements = document.querySelectorAll('[ms-code-yt-start]');
elements.forEach(function(element) {
var paramName = element.getAttribute('ms-code-yt-start');
var startTime = getUrlParameter(paramName);
var defaultStartTime = element.getAttribute('ms-code-yt-start-default');
// If no URL parameter, use the default start time (if specified)
if (!startTime && defaultStartTime) {
startTime = defaultStartTime;
}
// If we have a start time (either from URL or default), update the embed
if (startTime) {
var iframe = element.querySelector('iframe.embedly-embed');
if (iframe) {
updateYouTubeEmbed(iframe, startTime);
}
}
});
})();
</script>
<!-- 💙 MEMBERSCRIPT #141 v0.1 💙 - START YOUTUBE VIDEO AT SPECIFIC TIME -->
<script>
(function() {
// Function to get URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^]*)');
var results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
// Function to update YouTube embed src within Embedly iframe
function updateYouTubeEmbed(embedly_iframe, startTime) {
var embedly_src = embedly_iframe.src;
var youtube_src_match = embedly_src.match(/src=([^&]+)/);
if (youtube_src_match) {
var youtube_src = decodeURIComponent(youtube_src_match[1]);
var new_youtube_src = youtube_src.replace(/(\?|&)start=\d+/, '');
new_youtube_src += (new_youtube_src.includes('?') ? '&' : '?') + 'start=' + startTime;
var new_embedly_src = embedly_src.replace(/src=([^&]+)/, 'src=' + encodeURIComponent(new_youtube_src));
embedly_iframe.src = new_embedly_src;
}
}
// Get all elements with ms-code-yt-start attribute
var elements = document.querySelectorAll('[ms-code-yt-start]');
elements.forEach(function(element) {
var paramName = element.getAttribute('ms-code-yt-start');
var startTime = getUrlParameter(paramName);
var defaultStartTime = element.getAttribute('ms-code-yt-start-default');
// If no URL parameter, use the default start time (if specified)
if (!startTime && defaultStartTime) {
startTime = defaultStartTime;
}
// If we have a start time (either from URL or default), update the embed
if (startTime) {
var iframe = element.querySelector('iframe.embedly-embed');
if (iframe) {
updateYouTubeEmbed(iframe, startTime);
}
}
});
})();
</script>

#140 - Confirmar la coincidencia de entradas
Verificar una entrada antes de permitir su envío: ¡genial para evitar información incorrecta!
<!-- 💙 MEMBERSCRIPT #140 v0.1 💙 - CONFIRM MATCHING INPUTS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('form');
forms.forEach(form => {
const inputPairs = form.querySelectorAll('[ms-code-conf-input]');
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
if (!submitButton) {
console.error('Submit button not found in the form');
return;
}
function validateForm() {
let fieldsMatch = true;
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
const errorElement = form.querySelector(`[ms-code-conf-error="${confType}"]`);
if (confirmInput && errorElement) {
if (input.value && confirmInput.value) {
if (input.value !== confirmInput.value) {
errorElement.style.removeProperty('display');
fieldsMatch = false;
} else {
errorElement.style.display = 'none';
}
} else {
errorElement.style.display = 'none';
}
}
});
if (fieldsMatch) {
submitButton.style.removeProperty('pointer-events');
submitButton.disabled = false;
} else {
submitButton.style.pointerEvents = 'none';
submitButton.disabled = true;
}
}
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
if (confirmInput) {
input.addEventListener('input', validateForm);
confirmInput.addEventListener('input', validateForm);
}
});
// Initial validation
validateForm();
// Extra precaution: prevent form submission if fields don't match
form.addEventListener('submit', function(event) {
if (submitButton.disabled) {
event.preventDefault();
console.log('Form submission blocked: Fields do not match');
}
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #140 v0.1 💙 - CONFIRM MATCHING INPUTS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('form');
forms.forEach(form => {
const inputPairs = form.querySelectorAll('[ms-code-conf-input]');
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
if (!submitButton) {
console.error('Submit button not found in the form');
return;
}
function validateForm() {
let fieldsMatch = true;
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
const errorElement = form.querySelector(`[ms-code-conf-error="${confType}"]`);
if (confirmInput && errorElement) {
if (input.value && confirmInput.value) {
if (input.value !== confirmInput.value) {
errorElement.style.removeProperty('display');
fieldsMatch = false;
} else {
errorElement.style.display = 'none';
}
} else {
errorElement.style.display = 'none';
}
}
});
if (fieldsMatch) {
submitButton.style.removeProperty('pointer-events');
submitButton.disabled = false;
} else {
submitButton.style.pointerEvents = 'none';
submitButton.disabled = true;
}
}
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
if (confirmInput) {
input.addEventListener('input', validateForm);
confirmInput.addEventListener('input', validateForm);
}
});
// Initial validation
validateForm();
// Extra precaution: prevent form submission if fields don't match
form.addEventListener('submit', function(event) {
if (submitButton.disabled) {
event.preventDefault();
console.log('Form submission blocked: Fields do not match');
}
});
});
});
</script>

#139 - Restablecer formulario tras envío
Cree un botón en el estado de éxito del formulario que permita enviarlo de nuevo.
<!-- 💙 MEMBERSCRIPT #139 v0.1 💙 - RESET FORM BUTTON -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Find all "Add another" buttons
const resetButtons = document.querySelectorAll('[ms-code-reset-form]');
// Add click event listener to each button
resetButtons.forEach(function(resetButton) {
resetButton.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default link behavior
// Find the closest form and success message elements
const formWrapper = this.closest('.w-form');
const form = formWrapper.querySelector('form');
const successMessage = formWrapper.querySelector('.w-form-done');
// Reset the form
form.reset();
// Hide the success message
successMessage.style.display = 'none';
// Show the form
form.style.display = 'block';
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #139 v0.1 💙 - RESET FORM BUTTON -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Find all "Add another" buttons
const resetButtons = document.querySelectorAll('[ms-code-reset-form]');
// Add click event listener to each button
resetButtons.forEach(function(resetButton) {
resetButton.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default link behavior
// Find the closest form and success message elements
const formWrapper = this.closest('.w-form');
const form = formWrapper.querySelector('form');
const successMessage = formWrapper.querySelector('.w-form-done');
// Reset the form
form.reset();
// Hide the success message
successMessage.style.display = 'none';
// Show the form
form.style.display = 'block';
});
});
});
</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.