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.

#192 - Display a Members Current Subscription Plans
Display all of a member's active subscription plans in organized cards, with paid plans shown first.
<!-- 💙 MEMBERSCRIPT #192 v0.1 💙 - DISPLAY A MEMBERS CURRENT SUBSCRIPTION PLANS INFORMATION -->
<script>
(function() {
'use strict';
document.addEventListener("DOMContentLoaded", async function() {
try {
// Wait for Memberstack to be ready
await waitForMemberstack();
const memberstack = window.$memberstackDom;
if (!memberstack) {
console.error('MemberScript #192: Memberstack DOM package is not loaded.');
showError();
return;
}
const memberResult = await memberstack.getCurrentMember();
// Handle both { data: {...} } and direct member object formats
const member = memberResult?.data || memberResult;
// Check various possible locations for plan connections
let planConnections = null;
if (member && member.planConnections) {
planConnections = member.planConnections;
} else if (member && member.data && member.data.planConnections) {
planConnections = member.data.planConnections;
} else if (member && member.plans) {
planConnections = member.plans;
}
if (!planConnections || planConnections.length === 0) {
showNoPlanState();
return;
}
// Prioritize paid plans over free plans
// Sort plans: paid plans (with payment amount > 0) first, then free plans
const sortedPlans = [...planConnections].sort((a, b) => {
const aAmount = a.payment?.amount || 0;
const bAmount = b.payment?.amount || 0;
// Paid plans (amount > 0) come first
if (aAmount > 0 && bAmount === 0) return -1;
if (aAmount === 0 && bAmount > 0) return 1;
// If both are paid, sort by amount descending
if (aAmount > 0 && bAmount > 0) return bAmount - aAmount;
// If both are free, maintain original order
return 0;
});
// Display all plans
await displayAllPlans(sortedPlans, memberstack);
} catch (error) {
console.error("MemberScript #192: Error loading plan information:", error);
showError();
}
});
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackReady) {
resolve();
} else {
document.addEventListener('memberstack.ready', resolve);
// Fallback timeout
setTimeout(resolve, 2000);
}
});
}
async function displayAllPlans(planConnections, memberstack) {
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
// Look for template - can use data-ms-template attribute or data-ms-code="plan-card-template"
let planCardTemplate = document.querySelector('[data-ms-template]') || document.querySelector('[data-ms-code="plan-card-template"]');
// Determine the container
let plansContainer = null;
if (planCardTemplate) {
// Check if the template element also has plan-container attribute
const templateCodeAttr = planCardTemplate.getAttribute('data-ms-code') || '';
if (templateCodeAttr.includes('plan-container')) {
// Template is the same element as container, so use its parent as the container for multiple cards
plansContainer = planCardTemplate.parentElement;
} else {
// Template is separate, find the container
plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
}
} else {
// No template found, look for container
plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
}
// Hide loading and no-plan states
if (loadingState) loadingState.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'none';
// If no template found, look for the first card structure inside the container
if (!planCardTemplate && plansContainer) {
// Find the first card-like structure (one that has plan-name element)
const firstCard = plansContainer.querySelector('[data-ms-code="plan-name"]')?.closest('.grid-list_item, .plan-details_list, [data-ms-code="plan-container"]');
if (firstCard && firstCard !== plansContainer) {
planCardTemplate = firstCard;
}
}
if (!plansContainer) {
console.error('MemberScript #192: Plans container not found. Add data-ms-code="plans-container" or data-ms-code="plan-container" to your container element.');
showError();
return;
}
if (!planCardTemplate) {
console.error('MemberScript #192: Plan card template not found. Add data-ms-template attribute or data-ms-code="plan-card-template" to your card template element, or ensure your container has a card structure with plan details.');
showError();
return;
}
// Save original display state
const originalDisplay = planCardTemplate.style.display || getComputedStyle(planCardTemplate).display;
// Clear existing plan cards (except template) BEFORE hiding template
const existingCards = plansContainer.querySelectorAll('[data-ms-code="plan-card"]');
existingCards.forEach(card => {
if (card !== planCardTemplate && !card.hasAttribute('data-ms-template')) {
card.remove();
}
});
// Also clear any cards that might have been created before (but not the template)
const allCards = Array.from(plansContainer.querySelectorAll('.grid-list_item'));
allCards.forEach(card => {
if (card !== planCardTemplate && !card.hasAttribute('data-ms-template') && card.querySelector('[data-ms-code="plan-name"]')) {
card.remove();
}
});
// Mark template and hide it (but keep in DOM for cloning)
planCardTemplate.setAttribute('data-ms-template', 'true');
planCardTemplate.style.display = 'none';
// Show container
plansContainer.style.display = '';
// Create a card for each plan
for (let i = 0; i < planConnections.length; i++) {
const planConnection = planConnections[i];
const planId = planConnection.planId;
if (!planId) continue;
// Try to get the plan details
let plan = null;
try {
if (memberstack.getPlan) {
plan = await memberstack.getPlan({ planId });
}
} catch (e) {
// Plan details will be extracted from planConnection
}
// Clone the template (deep clone to get all children)
const planCard = planCardTemplate.cloneNode(true);
// Remove template attribute and set card attribute
planCard.removeAttribute('data-ms-template');
planCard.setAttribute('data-ms-code', 'plan-card');
// Set display - use original if it was visible, otherwise use 'block'
planCard.style.display = (originalDisplay && originalDisplay !== 'none') ? originalDisplay : 'block';
// Fill in plan information
fillPlanCard(planCard, plan, planConnection);
// Append to container
plansContainer.appendChild(planCard);
}
}
function fillPlanCard(card, plan, planConnection) {
// Helper function to format plan ID into a readable name
const formatPlanId = (planId) => {
if (!planId) return 'Your Plan';
// Convert "pln_premium-wabh0ux0" to "Premium"
return planId
.replace(/^pln_/, '')
.replace(/-[a-z0-9]+$/, '')
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Update plan name - try multiple sources
let planName = null;
if (plan) {
planName = plan?.data?.name || plan?.data?.planName || plan?.data?.label || plan?.name || plan?.planName || plan?.label;
}
if (!planName) {
planName = formatPlanId(planConnection.planId);
}
updateElementInCard(card, '[data-ms-code="plan-name"]', planName);
// Update plan price - check payment object first, then plan data
let priceValue = null;
if (planConnection.payment && planConnection.payment.amount !== undefined && planConnection.payment.amount !== null) {
priceValue = planConnection.payment.amount;
} else if (plan?.data && plan.data.amount !== undefined && plan.data.amount !== null) {
priceValue = plan.data.amount;
} else if (plan && plan.amount !== undefined && plan.amount !== null) {
priceValue = plan.amount;
} else if (plan?.data && plan.data.price !== undefined && plan.data.price !== null) {
// If price is in cents, convert
priceValue = plan.data.price / 100;
} else if (plan && plan.price !== undefined && plan.price !== null) {
// If price is in cents, convert
priceValue = plan.price / 100;
}
if (priceValue !== null && priceValue > 0) {
const currency = planConnection.payment?.currency || plan?.data?.currency || plan?.currency || 'usd';
const symbol = currency === 'usd' ? '$' : currency.toUpperCase();
const formattedPrice = priceValue.toFixed(2);
updateElementInCard(card, '[data-ms-code="plan-price"]', `${symbol}${formattedPrice}`);
} else {
updateElementInCard(card, '[data-ms-code="plan-price"]', 'Free');
}
// Update billing interval - use planConnection.type
if (planConnection.type) {
const type = planConnection.type.charAt(0).toUpperCase() + planConnection.type.slice(1).toLowerCase();
updateElementInCard(card, '[data-ms-code="plan-interval"]', type);
} else {
updateElementInCard(card, '[data-ms-code="plan-interval"]', 'N/A');
}
// Update status
const statusEl = card.querySelector('[data-ms-code="plan-status"]');
if (statusEl) {
const status = planConnection.status || 'Active';
// Format status nicely (ACTIVE -> Active)
const formattedStatus = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
statusEl.textContent = formattedStatus;
// Add cancelled class for styling
if (status && (status.toLowerCase() === 'canceled' || status.toLowerCase() === 'cancelled')) {
statusEl.classList.add('cancelled');
} else {
statusEl.classList.remove('cancelled');
}
}
// Update next billing date - use payment.nextBillingDate
let billingDate = planConnection.payment?.nextBillingDate;
if (billingDate) {
// Handle Unix timestamp (in seconds, so multiply by 1000)
const date = new Date(billingDate < 10000000000 ? billingDate * 1000 : billingDate);
updateElementInCard(card, '[data-ms-code="plan-next-billing"]', formatDate(date));
} else {
updateElementInCard(card, '[data-ms-code="plan-next-billing"]', 'N/A');
}
}
function updateElementInCard(card, selector, text) {
const el = card.querySelector(selector);
if (el) {
el.textContent = text;
}
}
function formatDate(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function showNoPlanState() {
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (plansContainer) plansContainer.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'block';
}
function showError() {
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (plansContainer) plansContainer.style.display = 'none';
if (noPlanState) {
noPlanState.innerHTML = '<div class="empty-state"><div style="font-size: 3rem;">!</div><h3>Error Loading Plans</h3><p>Unable to load your plan information. Please try again later.</p></div>';
noPlanState.style.display = 'block';
}
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #192 v0.1 💙 - DISPLAY A MEMBERS CURRENT SUBSCRIPTION PLANS INFORMATION -->
<script>
(function() {
'use strict';
document.addEventListener("DOMContentLoaded", async function() {
try {
// Wait for Memberstack to be ready
await waitForMemberstack();
const memberstack = window.$memberstackDom;
if (!memberstack) {
console.error('MemberScript #192: Memberstack DOM package is not loaded.');
showError();
return;
}
const memberResult = await memberstack.getCurrentMember();
// Handle both { data: {...} } and direct member object formats
const member = memberResult?.data || memberResult;
// Check various possible locations for plan connections
let planConnections = null;
if (member && member.planConnections) {
planConnections = member.planConnections;
} else if (member && member.data && member.data.planConnections) {
planConnections = member.data.planConnections;
} else if (member && member.plans) {
planConnections = member.plans;
}
if (!planConnections || planConnections.length === 0) {
showNoPlanState();
return;
}
// Prioritize paid plans over free plans
// Sort plans: paid plans (with payment amount > 0) first, then free plans
const sortedPlans = [...planConnections].sort((a, b) => {
const aAmount = a.payment?.amount || 0;
const bAmount = b.payment?.amount || 0;
// Paid plans (amount > 0) come first
if (aAmount > 0 && bAmount === 0) return -1;
if (aAmount === 0 && bAmount > 0) return 1;
// If both are paid, sort by amount descending
if (aAmount > 0 && bAmount > 0) return bAmount - aAmount;
// If both are free, maintain original order
return 0;
});
// Display all plans
await displayAllPlans(sortedPlans, memberstack);
} catch (error) {
console.error("MemberScript #192: Error loading plan information:", error);
showError();
}
});
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackReady) {
resolve();
} else {
document.addEventListener('memberstack.ready', resolve);
// Fallback timeout
setTimeout(resolve, 2000);
}
});
}
async function displayAllPlans(planConnections, memberstack) {
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
// Look for template - can use data-ms-template attribute or data-ms-code="plan-card-template"
let planCardTemplate = document.querySelector('[data-ms-template]') || document.querySelector('[data-ms-code="plan-card-template"]');
// Determine the container
let plansContainer = null;
if (planCardTemplate) {
// Check if the template element also has plan-container attribute
const templateCodeAttr = planCardTemplate.getAttribute('data-ms-code') || '';
if (templateCodeAttr.includes('plan-container')) {
// Template is the same element as container, so use its parent as the container for multiple cards
plansContainer = planCardTemplate.parentElement;
} else {
// Template is separate, find the container
plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
}
} else {
// No template found, look for container
plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
}
// Hide loading and no-plan states
if (loadingState) loadingState.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'none';
// If no template found, look for the first card structure inside the container
if (!planCardTemplate && plansContainer) {
// Find the first card-like structure (one that has plan-name element)
const firstCard = plansContainer.querySelector('[data-ms-code="plan-name"]')?.closest('.grid-list_item, .plan-details_list, [data-ms-code="plan-container"]');
if (firstCard && firstCard !== plansContainer) {
planCardTemplate = firstCard;
}
}
if (!plansContainer) {
console.error('MemberScript #192: Plans container not found. Add data-ms-code="plans-container" or data-ms-code="plan-container" to your container element.');
showError();
return;
}
if (!planCardTemplate) {
console.error('MemberScript #192: Plan card template not found. Add data-ms-template attribute or data-ms-code="plan-card-template" to your card template element, or ensure your container has a card structure with plan details.');
showError();
return;
}
// Save original display state
const originalDisplay = planCardTemplate.style.display || getComputedStyle(planCardTemplate).display;
// Clear existing plan cards (except template) BEFORE hiding template
const existingCards = plansContainer.querySelectorAll('[data-ms-code="plan-card"]');
existingCards.forEach(card => {
if (card !== planCardTemplate && !card.hasAttribute('data-ms-template')) {
card.remove();
}
});
// Also clear any cards that might have been created before (but not the template)
const allCards = Array.from(plansContainer.querySelectorAll('.grid-list_item'));
allCards.forEach(card => {
if (card !== planCardTemplate && !card.hasAttribute('data-ms-template') && card.querySelector('[data-ms-code="plan-name"]')) {
card.remove();
}
});
// Mark template and hide it (but keep in DOM for cloning)
planCardTemplate.setAttribute('data-ms-template', 'true');
planCardTemplate.style.display = 'none';
// Show container
plansContainer.style.display = '';
// Create a card for each plan
for (let i = 0; i < planConnections.length; i++) {
const planConnection = planConnections[i];
const planId = planConnection.planId;
if (!planId) continue;
// Try to get the plan details
let plan = null;
try {
if (memberstack.getPlan) {
plan = await memberstack.getPlan({ planId });
}
} catch (e) {
// Plan details will be extracted from planConnection
}
// Clone the template (deep clone to get all children)
const planCard = planCardTemplate.cloneNode(true);
// Remove template attribute and set card attribute
planCard.removeAttribute('data-ms-template');
planCard.setAttribute('data-ms-code', 'plan-card');
// Set display - use original if it was visible, otherwise use 'block'
planCard.style.display = (originalDisplay && originalDisplay !== 'none') ? originalDisplay : 'block';
// Fill in plan information
fillPlanCard(planCard, plan, planConnection);
// Append to container
plansContainer.appendChild(planCard);
}
}
function fillPlanCard(card, plan, planConnection) {
// Helper function to format plan ID into a readable name
const formatPlanId = (planId) => {
if (!planId) return 'Your Plan';
// Convert "pln_premium-wabh0ux0" to "Premium"
return planId
.replace(/^pln_/, '')
.replace(/-[a-z0-9]+$/, '')
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Update plan name - try multiple sources
let planName = null;
if (plan) {
planName = plan?.data?.name || plan?.data?.planName || plan?.data?.label || plan?.name || plan?.planName || plan?.label;
}
if (!planName) {
planName = formatPlanId(planConnection.planId);
}
updateElementInCard(card, '[data-ms-code="plan-name"]', planName);
// Update plan price - check payment object first, then plan data
let priceValue = null;
if (planConnection.payment && planConnection.payment.amount !== undefined && planConnection.payment.amount !== null) {
priceValue = planConnection.payment.amount;
} else if (plan?.data && plan.data.amount !== undefined && plan.data.amount !== null) {
priceValue = plan.data.amount;
} else if (plan && plan.amount !== undefined && plan.amount !== null) {
priceValue = plan.amount;
} else if (plan?.data && plan.data.price !== undefined && plan.data.price !== null) {
// If price is in cents, convert
priceValue = plan.data.price / 100;
} else if (plan && plan.price !== undefined && plan.price !== null) {
// If price is in cents, convert
priceValue = plan.price / 100;
}
if (priceValue !== null && priceValue > 0) {
const currency = planConnection.payment?.currency || plan?.data?.currency || plan?.currency || 'usd';
const symbol = currency === 'usd' ? '$' : currency.toUpperCase();
const formattedPrice = priceValue.toFixed(2);
updateElementInCard(card, '[data-ms-code="plan-price"]', `${symbol}${formattedPrice}`);
} else {
updateElementInCard(card, '[data-ms-code="plan-price"]', 'Free');
}
// Update billing interval - use planConnection.type
if (planConnection.type) {
const type = planConnection.type.charAt(0).toUpperCase() + planConnection.type.slice(1).toLowerCase();
updateElementInCard(card, '[data-ms-code="plan-interval"]', type);
} else {
updateElementInCard(card, '[data-ms-code="plan-interval"]', 'N/A');
}
// Update status
const statusEl = card.querySelector('[data-ms-code="plan-status"]');
if (statusEl) {
const status = planConnection.status || 'Active';
// Format status nicely (ACTIVE -> Active)
const formattedStatus = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
statusEl.textContent = formattedStatus;
// Add cancelled class for styling
if (status && (status.toLowerCase() === 'canceled' || status.toLowerCase() === 'cancelled')) {
statusEl.classList.add('cancelled');
} else {
statusEl.classList.remove('cancelled');
}
}
// Update next billing date - use payment.nextBillingDate
let billingDate = planConnection.payment?.nextBillingDate;
if (billingDate) {
// Handle Unix timestamp (in seconds, so multiply by 1000)
const date = new Date(billingDate < 10000000000 ? billingDate * 1000 : billingDate);
updateElementInCard(card, '[data-ms-code="plan-next-billing"]', formatDate(date));
} else {
updateElementInCard(card, '[data-ms-code="plan-next-billing"]', 'N/A');
}
}
function updateElementInCard(card, selector, text) {
const el = card.querySelector(selector);
if (el) {
el.textContent = text;
}
}
function formatDate(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function showNoPlanState() {
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (plansContainer) plansContainer.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'block';
}
function showError() {
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (plansContainer) plansContainer.style.display = 'none';
if (noPlanState) {
noPlanState.innerHTML = '<div class="empty-state"><div style="font-size: 3rem;">!</div><h3>Error Loading Plans</h3><p>Unable to load your plan information. Please try again later.</p></div>';
noPlanState.style.display = 'block';
}
}
})();
</script>

#191 - Browser Compatibility Notice
Automatically detect outdated browsers and display a dismissible notice.
<!-- 💙 MEMBERSCRIPT #191 v0.1 💙 - BETTER BROWSER COMPATIBILITY NOTICES -->
<script>
(function() {
'use strict';
// ═══════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════
const CONFIG = {
// Minimum supported browser versions
MIN_VERSIONS: {
chrome: 90,
firefox: 88,
safari: 14,
edge: 90,
opera: 76
},
// Messages for different browser types
MESSAGES: {
outdated: 'Your browser is outdated and may not support all features. Please update for the best experience.',
unsupported: 'Your browser is not fully supported. Please use a modern browser like Chrome, Firefox, Safari, or Edge.',
update: 'Update your browser',
dismiss: 'Dismiss'
},
// Show notice even if dismissed (for testing)
// Set to true to always show the notice, regardless of browser or dismissal state
FORCE_SHOW: false,
// Storage key for dismissed state
STORAGE_KEY: 'ms_browser_notice_dismissed'
};
// ═══════════════════════════════════════════════════════════════
// BROWSER DETECTION
// ═══════════════════════════════════════════════════════════════
function getBrowserInfo() {
const ua = navigator.userAgent;
let browser = { name: 'unknown', version: 0 };
// Internet Explorer (all versions unsupported)
if (/MSIE|Trident/.test(ua)) {
return { name: 'ie', version: 0, unsupported: true };
}
// Chrome
const chromeMatch = ua.match(/Chrome\/(\d+)/);
if (chromeMatch && !/Edg|OPR/.test(ua)) {
return { name: 'chrome', version: parseInt(chromeMatch[1], 10) };
}
// Edge (Chromium-based)
const edgeMatch = ua.match(/Edg\/(\d+)/);
if (edgeMatch) {
return { name: 'edge', version: parseInt(edgeMatch[1], 10) };
}
// Legacy Edge (unsupported)
if (/Edge\/\d/.test(ua) && !/Edg/.test(ua)) {
return { name: 'edge-legacy', version: 0, unsupported: true };
}
// Firefox
const firefoxMatch = ua.match(/Firefox\/(\d+)/);
if (firefoxMatch) {
return { name: 'firefox', version: parseInt(firefoxMatch[1], 10) };
}
// Safari (must check for Chrome first to avoid false positives)
const safariMatch = ua.match(/Version\/(\d+).*Safari/);
if (safariMatch && !/Chrome|Chromium/.test(ua)) {
return { name: 'safari', version: parseInt(safariMatch[1], 10) };
}
// Opera
const operaMatch = ua.match(/OPR\/(\d+)/);
if (operaMatch) {
return { name: 'opera', version: parseInt(operaMatch[1], 10) };
}
return browser;
}
function isBrowserOutdated(browser) {
// Unsupported browsers (IE, legacy Edge)
if (browser.unsupported) {
return { outdated: true, reason: 'unsupported' };
}
// Unknown browser
if (browser.name === 'unknown') {
return { outdated: false, reason: 'unknown' };
}
// Check against minimum versions
const minVersion = CONFIG.MIN_VERSIONS[browser.name];
if (minVersion && browser.version < minVersion) {
return { outdated: true, reason: 'outdated', current: browser.version, required: minVersion };
}
return { outdated: false, reason: 'supported' };
}
// ═══════════════════════════════════════════════════════════════
// STORAGE HELPERS
// ═══════════════════════════════════════════════════════════════
function isDismissed() {
if (CONFIG.FORCE_SHOW) return false;
try {
return localStorage.getItem(CONFIG.STORAGE_KEY) === 'true';
} catch (e) {
return false;
}
}
function setDismissed() {
try {
localStorage.setItem(CONFIG.STORAGE_KEY, 'true');
} catch (e) {
// Silently fail if localStorage is not available
}
}
// ═══════════════════════════════════════════════════════════════
// NOTICE DISPLAY
// ═══════════════════════════════════════════════════════════════
function getBrowserUpdateUrl(browserName) {
const urls = {
chrome: 'https://www.google.com/chrome/',
firefox: 'https://www.mozilla.org/firefox/',
safari: 'https://www.apple.com/safari/',
edge: 'https://www.microsoft.com/edge',
opera: 'https://www.opera.com/download',
'edge-legacy': 'https://www.microsoft.com/edge',
ie: 'https://www.microsoft.com/edge'
};
return urls[browserName] || 'https://browsehappy.com/';
}
function createNotice(browser, status) {
// Only works with custom Webflow-designed container
const customContainer = document.querySelector('[data-ms-code="browser-notice"]');
if (!customContainer) {
return;
}
// Show the container (override CSS display:none if set in Webflow)
const computedStyle = window.getComputedStyle(customContainer);
if (computedStyle.display === 'none' || customContainer.style.display === 'none') {
// Set explicit display value to override CSS rule
// Use 'block' as default, or preserve original if it was set via inline style
customContainer.style.setProperty('display', 'block', 'important');
}
// Populate individual elements within the container
const messageEl = customContainer.querySelector('[data-ms-code="browser-notice-message"]');
const updateLinkEl = customContainer.querySelector('[data-ms-code="browser-notice-update"]');
const dismissBtnEl = customContainer.querySelector('[data-ms-code="browser-notice-dismiss"]');
// Populate message
if (messageEl) {
const isUnsupported = status.reason === 'unsupported';
messageEl.textContent = isUnsupported ? CONFIG.MESSAGES.unsupported : CONFIG.MESSAGES.outdated;
}
// Populate update link
if (updateLinkEl) {
const updateUrl = getBrowserUpdateUrl(browser.name);
// Handle both <a> tags and other elements
if (updateLinkEl.tagName.toLowerCase() === 'a') {
updateLinkEl.href = updateUrl;
updateLinkEl.setAttribute('target', '_blank');
updateLinkEl.setAttribute('rel', 'noopener noreferrer');
} else {
// For buttons or other elements, add onclick
updateLinkEl.onclick = function(e) {
e.preventDefault();
window.open(updateUrl, '_blank', 'noopener,noreferrer');
};
}
updateLinkEl.textContent = CONFIG.MESSAGES.update;
}
// Populate dismiss button
if (dismissBtnEl) {
dismissBtnEl.textContent = CONFIG.MESSAGES.dismiss;
attachDismissHandler(customContainer);
}
return customContainer;
}
function attachDismissHandler(container) {
const dismissBtn = container.querySelector('[data-ms-code="browser-notice-dismiss"]');
if (dismissBtn) {
dismissBtn.addEventListener('click', function() {
setDismissed();
// Hide container using Webflow's own styling
container.style.display = 'none';
});
}
}
// ═══════════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════════
function init() {
// Check if custom container exists (designed in Webflow)
const customContainer = document.querySelector('[data-ms-code="browser-notice"]');
if (!customContainer) {
return;
}
// Hide banner if already dismissed (unless force show)
if (isDismissed() && !CONFIG.FORCE_SHOW) {
customContainer.style.display = 'none';
return;
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkAndShowNotice);
} else {
checkAndShowNotice();
}
}
function checkAndShowNotice() {
const browser = getBrowserInfo();
const status = isBrowserOutdated(browser);
const customContainer = document.querySelector('[data-ms-code="browser-notice"]');
if (status.outdated || CONFIG.FORCE_SHOW) {
createNotice(browser, status);
} else {
// Hide banner if browser is up to date
if (customContainer) {
customContainer.style.display = 'none';
}
}
}
// Start initialization
init();
})();
</script>
<!-- 💙 MEMBERSCRIPT #191 v0.1 💙 - BETTER BROWSER COMPATIBILITY NOTICES -->
<script>
(function() {
'use strict';
// ═══════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════
const CONFIG = {
// Minimum supported browser versions
MIN_VERSIONS: {
chrome: 90,
firefox: 88,
safari: 14,
edge: 90,
opera: 76
},
// Messages for different browser types
MESSAGES: {
outdated: 'Your browser is outdated and may not support all features. Please update for the best experience.',
unsupported: 'Your browser is not fully supported. Please use a modern browser like Chrome, Firefox, Safari, or Edge.',
update: 'Update your browser',
dismiss: 'Dismiss'
},
// Show notice even if dismissed (for testing)
// Set to true to always show the notice, regardless of browser or dismissal state
FORCE_SHOW: false,
// Storage key for dismissed state
STORAGE_KEY: 'ms_browser_notice_dismissed'
};
// ═══════════════════════════════════════════════════════════════
// BROWSER DETECTION
// ═══════════════════════════════════════════════════════════════
function getBrowserInfo() {
const ua = navigator.userAgent;
let browser = { name: 'unknown', version: 0 };
// Internet Explorer (all versions unsupported)
if (/MSIE|Trident/.test(ua)) {
return { name: 'ie', version: 0, unsupported: true };
}
// Chrome
const chromeMatch = ua.match(/Chrome\/(\d+)/);
if (chromeMatch && !/Edg|OPR/.test(ua)) {
return { name: 'chrome', version: parseInt(chromeMatch[1], 10) };
}
// Edge (Chromium-based)
const edgeMatch = ua.match(/Edg\/(\d+)/);
if (edgeMatch) {
return { name: 'edge', version: parseInt(edgeMatch[1], 10) };
}
// Legacy Edge (unsupported)
if (/Edge\/\d/.test(ua) && !/Edg/.test(ua)) {
return { name: 'edge-legacy', version: 0, unsupported: true };
}
// Firefox
const firefoxMatch = ua.match(/Firefox\/(\d+)/);
if (firefoxMatch) {
return { name: 'firefox', version: parseInt(firefoxMatch[1], 10) };
}
// Safari (must check for Chrome first to avoid false positives)
const safariMatch = ua.match(/Version\/(\d+).*Safari/);
if (safariMatch && !/Chrome|Chromium/.test(ua)) {
return { name: 'safari', version: parseInt(safariMatch[1], 10) };
}
// Opera
const operaMatch = ua.match(/OPR\/(\d+)/);
if (operaMatch) {
return { name: 'opera', version: parseInt(operaMatch[1], 10) };
}
return browser;
}
function isBrowserOutdated(browser) {
// Unsupported browsers (IE, legacy Edge)
if (browser.unsupported) {
return { outdated: true, reason: 'unsupported' };
}
// Unknown browser
if (browser.name === 'unknown') {
return { outdated: false, reason: 'unknown' };
}
// Check against minimum versions
const minVersion = CONFIG.MIN_VERSIONS[browser.name];
if (minVersion && browser.version < minVersion) {
return { outdated: true, reason: 'outdated', current: browser.version, required: minVersion };
}
return { outdated: false, reason: 'supported' };
}
// ═══════════════════════════════════════════════════════════════
// STORAGE HELPERS
// ═══════════════════════════════════════════════════════════════
function isDismissed() {
if (CONFIG.FORCE_SHOW) return false;
try {
return localStorage.getItem(CONFIG.STORAGE_KEY) === 'true';
} catch (e) {
return false;
}
}
function setDismissed() {
try {
localStorage.setItem(CONFIG.STORAGE_KEY, 'true');
} catch (e) {
// Silently fail if localStorage is not available
}
}
// ═══════════════════════════════════════════════════════════════
// NOTICE DISPLAY
// ═══════════════════════════════════════════════════════════════
function getBrowserUpdateUrl(browserName) {
const urls = {
chrome: 'https://www.google.com/chrome/',
firefox: 'https://www.mozilla.org/firefox/',
safari: 'https://www.apple.com/safari/',
edge: 'https://www.microsoft.com/edge',
opera: 'https://www.opera.com/download',
'edge-legacy': 'https://www.microsoft.com/edge',
ie: 'https://www.microsoft.com/edge'
};
return urls[browserName] || 'https://browsehappy.com/';
}
function createNotice(browser, status) {
// Only works with custom Webflow-designed container
const customContainer = document.querySelector('[data-ms-code="browser-notice"]');
if (!customContainer) {
return;
}
// Show the container (override CSS display:none if set in Webflow)
const computedStyle = window.getComputedStyle(customContainer);
if (computedStyle.display === 'none' || customContainer.style.display === 'none') {
// Set explicit display value to override CSS rule
// Use 'block' as default, or preserve original if it was set via inline style
customContainer.style.setProperty('display', 'block', 'important');
}
// Populate individual elements within the container
const messageEl = customContainer.querySelector('[data-ms-code="browser-notice-message"]');
const updateLinkEl = customContainer.querySelector('[data-ms-code="browser-notice-update"]');
const dismissBtnEl = customContainer.querySelector('[data-ms-code="browser-notice-dismiss"]');
// Populate message
if (messageEl) {
const isUnsupported = status.reason === 'unsupported';
messageEl.textContent = isUnsupported ? CONFIG.MESSAGES.unsupported : CONFIG.MESSAGES.outdated;
}
// Populate update link
if (updateLinkEl) {
const updateUrl = getBrowserUpdateUrl(browser.name);
// Handle both <a> tags and other elements
if (updateLinkEl.tagName.toLowerCase() === 'a') {
updateLinkEl.href = updateUrl;
updateLinkEl.setAttribute('target', '_blank');
updateLinkEl.setAttribute('rel', 'noopener noreferrer');
} else {
// For buttons or other elements, add onclick
updateLinkEl.onclick = function(e) {
e.preventDefault();
window.open(updateUrl, '_blank', 'noopener,noreferrer');
};
}
updateLinkEl.textContent = CONFIG.MESSAGES.update;
}
// Populate dismiss button
if (dismissBtnEl) {
dismissBtnEl.textContent = CONFIG.MESSAGES.dismiss;
attachDismissHandler(customContainer);
}
return customContainer;
}
function attachDismissHandler(container) {
const dismissBtn = container.querySelector('[data-ms-code="browser-notice-dismiss"]');
if (dismissBtn) {
dismissBtn.addEventListener('click', function() {
setDismissed();
// Hide container using Webflow's own styling
container.style.display = 'none';
});
}
}
// ═══════════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════════
function init() {
// Check if custom container exists (designed in Webflow)
const customContainer = document.querySelector('[data-ms-code="browser-notice"]');
if (!customContainer) {
return;
}
// Hide banner if already dismissed (unless force show)
if (isDismissed() && !CONFIG.FORCE_SHOW) {
customContainer.style.display = 'none';
return;
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkAndShowNotice);
} else {
checkAndShowNotice();
}
}
function checkAndShowNotice() {
const browser = getBrowserInfo();
const status = isBrowserOutdated(browser);
const customContainer = document.querySelector('[data-ms-code="browser-notice"]');
if (status.outdated || CONFIG.FORCE_SHOW) {
createNotice(browser, status);
} else {
// Hide banner if browser is up to date
if (customContainer) {
customContainer.style.display = 'none';
}
}
}
// Start initialization
init();
})();
</script>

#190 - Cross-plan Member Upvote/Downvoting
Enable members with different subscription plans to vote on each other's profiles.
<!-- 💙 MEMBERSCRIPT #190 v0.1 💙 - CROSS-PLAN UPVOTE/DOWNVOTE SYSTEM -->
<script>
(function() {
'use strict';
// ═══════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════
const CONFIG = {
PLAN_A_ID: 'pln_voting-privileges--4t4n0w8c',
PLAN_B_ID: 'pln_voting-privileges-b-uz3z01vd',
SELECTORS: {
PROFILE: '[data-ms-code="user-profile"]',
UPVOTE: '[data-ms-code="upvote-button"]',
DOWNVOTE: '[data-ms-code="downvote-button"]',
FORM: 'form'
},
TOAST_MS: 3000,
MESSAGES: {
upvoteSuccess: 'Your vote has been recorded.',
downvoteSuccess: 'Your vote has been removed.',
notLoggedIn: 'You must be logged in with a valid plan to vote.',
selfVote: 'You cannot vote on yourself.',
invalidTarget: 'User profile data is missing.',
wrongPlan: 'You can only vote on members with a different plan.',
notFoundForm: 'Vote form not found.',
submitError: 'Failed to submit. Please try again.'
}
};
let memberstack = null;
let currentMember = null;
let currentPlanIds = [];
// LocalStorage helpers - separate storage for upvotes and downvotes
function hasUpvoted(voterId, targetMemberId) {
try {
const upvotes = JSON.parse(localStorage.getItem('upvotes') || '{}');
return upvotes[voterId]?.includes(targetMemberId) || false;
} catch (_) {
return false;
}
}
function hasDownvoted(voterId, targetMemberId) {
try {
const downvotes = JSON.parse(localStorage.getItem('downvotes') || '{}');
return downvotes[voterId]?.includes(targetMemberId) || false;
} catch (_) {
return false;
}
}
function setUpvoted(voterId, targetMemberId) {
try {
const upvotes = JSON.parse(localStorage.getItem('upvotes') || '{}');
if (!upvotes[voterId]) upvotes[voterId] = [];
if (!upvotes[voterId].includes(targetMemberId)) {
upvotes[voterId].push(targetMemberId);
localStorage.setItem('upvotes', JSON.stringify(upvotes));
}
} catch (_) {}
}
function setDownvoted(voterId, targetMemberId) {
try {
const downvotes = JSON.parse(localStorage.getItem('downvotes') || '{}');
if (!downvotes[voterId]) downvotes[voterId] = [];
if (!downvotes[voterId].includes(targetMemberId)) {
downvotes[voterId].push(targetMemberId);
localStorage.setItem('downvotes', JSON.stringify(downvotes));
}
} catch (_) {}
}
// Button state management
function setButtonState(btn, disabled) {
if (!btn) return;
btn.disabled = disabled;
btn.style.pointerEvents = disabled ? 'none' : 'auto';
if (disabled) {
btn.setAttribute('aria-disabled', 'true');
btn.classList.add('voted');
btn.style.opacity = '0.8';
btn.style.cursor = 'not-allowed';
} else {
btn.removeAttribute('aria-disabled');
btn.classList.remove('voted');
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
}
async function getCurrentMemberData() {
try {
if (!memberstack) {
memberstack = window.$memberstackDom;
if (!memberstack) return null;
}
const result = await memberstack.getCurrentMember();
const member = result?.data;
if (!member) return null;
const planConnections = member.planConnections || member.data?.planConnections || member.plans || [];
currentPlanIds = [];
planConnections.forEach(connection => {
const planId = connection?.planId;
if (planId && (planId === CONFIG.PLAN_A_ID || planId === CONFIG.PLAN_B_ID)) {
currentPlanIds.push(planId);
}
});
return member;
} catch (error) {
console.error('MemberScript #190: Error getting member data:', error);
return null;
}
}
function canVote(voterPlanIds, targetPlanId) {
if (!voterPlanIds || !targetPlanId) return false;
const voterPlans = Array.isArray(voterPlanIds) ? voterPlanIds : [voterPlanIds];
const targetIsPlanA = targetPlanId === CONFIG.PLAN_A_ID;
const targetIsPlanB = targetPlanId === CONFIG.PLAN_B_ID;
const voterHasPlanA = voterPlans.includes(CONFIG.PLAN_A_ID);
const voterHasPlanB = voterPlans.includes(CONFIG.PLAN_B_ID);
return (voterHasPlanA && targetIsPlanB) || (voterHasPlanB && targetIsPlanA);
}
async function handleVote(event, voteType) {
event.preventDefault();
event.stopPropagation();
if (!currentMember || currentPlanIds.length === 0) {
showMessage(CONFIG.MESSAGES.notLoggedIn, 'error');
return;
}
const button = event.currentTarget;
const profileContainer = button.closest(CONFIG.SELECTORS.PROFILE);
if (!profileContainer) return;
const targetMemberId = profileContainer.getAttribute('data-target-member-id');
const targetPlanId = profileContainer.getAttribute('data-target-plan-id');
if (!targetMemberId || !targetPlanId) {
showMessage(CONFIG.MESSAGES.invalidTarget, 'error');
return;
}
if (!canVote(currentPlanIds, targetPlanId)) {
showMessage(CONFIG.MESSAGES.wrongPlan, 'error');
return;
}
const currentMemberId = currentMember.id || currentMember._id;
if (currentMemberId === targetMemberId) {
showMessage(CONFIG.MESSAGES.selfVote, 'warning');
return;
}
const upvoteBtn = profileContainer.querySelector(CONFIG.SELECTORS.UPVOTE);
const downvoteBtn = profileContainer.querySelector(CONFIG.SELECTORS.DOWNVOTE);
// Check if already voted with this specific action
if ((voteType === 'upvote' && hasUpvoted(currentMemberId, targetMemberId)) ||
(voteType === 'downvote' && hasDownvoted(currentMemberId, targetMemberId))) {
return;
}
// Check if button already disabled
if ((voteType === 'upvote' && upvoteBtn?.classList.contains('voted')) ||
(voteType === 'downvote' && downvoteBtn?.classList.contains('voted'))) {
return;
}
// Prevent double submission
if (profileContainer.getAttribute('data-submitting') === 'true') return;
profileContainer.setAttribute('data-submitting', 'true');
setButtonState(upvoteBtn, true);
setButtonState(downvoteBtn, true);
try {
const form = profileContainer.querySelector(CONFIG.SELECTORS.FORM);
if (!form) {
showMessage(CONFIG.MESSAGES.notFoundForm, 'error');
setButtonState(upvoteBtn, false);
setButtonState(downvoteBtn, false);
return;
}
const voterField = form.querySelector('[data-ms-code="voter-member-id"]');
const targetField = form.querySelector('[data-ms-code="target-member-id"]');
const actionField = form.querySelector('[data-ms-code="vote-action"]');
const tsField = form.querySelector('[data-ms-code="vote-timestamp"]');
if (voterField) voterField.value = currentMemberId;
if (targetField) targetField.value = targetMemberId;
if (actionField) actionField.value = voteType;
if (tsField) tsField.value = String(Date.now());
form.submit();
// Update UI: disable the clicked button
setButtonState(voteType === 'upvote' ? upvoteBtn : downvoteBtn, true);
// Only re-enable the other button if it wasn't previously voted on
if (voteType === 'upvote' && !hasDownvoted(currentMemberId, targetMemberId)) {
setButtonState(downvoteBtn, false);
} else if (voteType === 'downvote' && !hasUpvoted(currentMemberId, targetMemberId)) {
setButtonState(upvoteBtn, false);
}
// Save to appropriate localStorage
if (voteType === 'upvote') {
setUpvoted(currentMemberId, targetMemberId);
} else {
setDownvoted(currentMemberId, targetMemberId);
}
showMessage(voteType === 'upvote' ? CONFIG.MESSAGES.upvoteSuccess : CONFIG.MESSAGES.downvoteSuccess, 'success');
profileContainer.setAttribute('data-submitting', 'false');
} catch (err) {
console.error('MemberScript #190: Error submitting vote form:', err);
showMessage(CONFIG.MESSAGES.submitError, 'error');
setButtonState(upvoteBtn, false);
setButtonState(downvoteBtn, false);
profileContainer.setAttribute('data-submitting', 'false');
}
}
function showMessage(message, type = 'info') {
const colors = { info: '#3b82f6', success: '#10b981', warning: '#f59e0b', error: '#ef4444' };
const msgEl = document.createElement('div');
msgEl.setAttribute('data-ms-code', 'vote-message');
msgEl.textContent = message;
msgEl.style.cssText = `
position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px;
color: white; background: ${colors[type] || colors.info}; z-index: 10000;
font-size: 14px; font-weight: 500; max-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); animation: slideIn 0.3s ease-out;
`;
document.body.appendChild(msgEl);
setTimeout(() => {
msgEl.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => msgEl.remove(), 300);
}, CONFIG.TOAST_MS);
}
function initializeVoting() {
// Attach event listeners
document.querySelectorAll(CONFIG.SELECTORS.UPVOTE).forEach(btn => {
btn.addEventListener('click', (e) => handleVote(e, 'upvote'));
});
document.querySelectorAll(CONFIG.SELECTORS.DOWNVOTE).forEach(btn => {
btn.addEventListener('click', (e) => handleVote(e, 'downvote'));
});
// Restore button states from localStorage (check both upvotes and downvotes separately)
if (currentMember) {
const currentMemberId = currentMember.id || currentMember._id;
document.querySelectorAll(CONFIG.SELECTORS.PROFILE).forEach(profile => {
const targetMemberId = profile.getAttribute('data-target-member-id');
const upvoteBtn = profile.querySelector(CONFIG.SELECTORS.UPVOTE);
const downvoteBtn = profile.querySelector(CONFIG.SELECTORS.DOWNVOTE);
// Check upvotes and downvotes independently
if (hasUpvoted(currentMemberId, targetMemberId)) {
setButtonState(upvoteBtn, true);
}
if (hasDownvoted(currentMemberId, targetMemberId)) {
setButtonState(downvoteBtn, true);
}
});
}
// Enable/disable based on plan compatibility
if (currentPlanIds.length > 0) {
document.querySelectorAll(CONFIG.SELECTORS.PROFILE).forEach(profile => {
const targetPlanId = profile.getAttribute('data-target-plan-id');
const canVoteOnThis = canVote(currentPlanIds, targetPlanId);
const upvoteBtn = profile.querySelector(CONFIG.SELECTORS.UPVOTE);
const downvoteBtn = profile.querySelector(CONFIG.SELECTORS.DOWNVOTE);
if (!canVoteOnThis) {
setButtonState(upvoteBtn, true);
setButtonState(downvoteBtn, true);
if (upvoteBtn) upvoteBtn.style.opacity = '0.5';
if (downvoteBtn) downvoteBtn.style.opacity = '0.5';
}
});
}
}
// Wait for Memberstack
async function waitForMemberstack() {
if (window.$memberstackDom?.getCurrentMember) return;
return new Promise((resolve) => {
if (window.$memberstackDom) {
document.addEventListener('memberstack.ready', resolve);
setTimeout(resolve, 2000);
} else {
const check = setInterval(() => {
if (window.$memberstackDom) {
clearInterval(check);
resolve();
}
}, 100);
setTimeout(() => { clearInterval(check); resolve(); }, 3000);
}
});
}
// Initialize
document.addEventListener('DOMContentLoaded', async function() {
try {
await waitForMemberstack();
currentMember = await getCurrentMemberData();
setTimeout(() => {
initializeVoting();
}, 100);
} catch (error) {
console.error('MemberScript #190: Error initializing:', error);
}
});
})();
</script>
<style>
[data-ms-code="upvote-button"].voted,
[data-ms-code="downvote-button"].voted {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
</style>
<!-- 💙 MEMBERSCRIPT #190 v0.1 💙 - CROSS-PLAN UPVOTE/DOWNVOTE SYSTEM -->
<script>
(function() {
'use strict';
// ═══════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════
const CONFIG = {
PLAN_A_ID: 'pln_voting-privileges--4t4n0w8c',
PLAN_B_ID: 'pln_voting-privileges-b-uz3z01vd',
SELECTORS: {
PROFILE: '[data-ms-code="user-profile"]',
UPVOTE: '[data-ms-code="upvote-button"]',
DOWNVOTE: '[data-ms-code="downvote-button"]',
FORM: 'form'
},
TOAST_MS: 3000,
MESSAGES: {
upvoteSuccess: 'Your vote has been recorded.',
downvoteSuccess: 'Your vote has been removed.',
notLoggedIn: 'You must be logged in with a valid plan to vote.',
selfVote: 'You cannot vote on yourself.',
invalidTarget: 'User profile data is missing.',
wrongPlan: 'You can only vote on members with a different plan.',
notFoundForm: 'Vote form not found.',
submitError: 'Failed to submit. Please try again.'
}
};
let memberstack = null;
let currentMember = null;
let currentPlanIds = [];
// LocalStorage helpers - separate storage for upvotes and downvotes
function hasUpvoted(voterId, targetMemberId) {
try {
const upvotes = JSON.parse(localStorage.getItem('upvotes') || '{}');
return upvotes[voterId]?.includes(targetMemberId) || false;
} catch (_) {
return false;
}
}
function hasDownvoted(voterId, targetMemberId) {
try {
const downvotes = JSON.parse(localStorage.getItem('downvotes') || '{}');
return downvotes[voterId]?.includes(targetMemberId) || false;
} catch (_) {
return false;
}
}
function setUpvoted(voterId, targetMemberId) {
try {
const upvotes = JSON.parse(localStorage.getItem('upvotes') || '{}');
if (!upvotes[voterId]) upvotes[voterId] = [];
if (!upvotes[voterId].includes(targetMemberId)) {
upvotes[voterId].push(targetMemberId);
localStorage.setItem('upvotes', JSON.stringify(upvotes));
}
} catch (_) {}
}
function setDownvoted(voterId, targetMemberId) {
try {
const downvotes = JSON.parse(localStorage.getItem('downvotes') || '{}');
if (!downvotes[voterId]) downvotes[voterId] = [];
if (!downvotes[voterId].includes(targetMemberId)) {
downvotes[voterId].push(targetMemberId);
localStorage.setItem('downvotes', JSON.stringify(downvotes));
}
} catch (_) {}
}
// Button state management
function setButtonState(btn, disabled) {
if (!btn) return;
btn.disabled = disabled;
btn.style.pointerEvents = disabled ? 'none' : 'auto';
if (disabled) {
btn.setAttribute('aria-disabled', 'true');
btn.classList.add('voted');
btn.style.opacity = '0.8';
btn.style.cursor = 'not-allowed';
} else {
btn.removeAttribute('aria-disabled');
btn.classList.remove('voted');
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
}
async function getCurrentMemberData() {
try {
if (!memberstack) {
memberstack = window.$memberstackDom;
if (!memberstack) return null;
}
const result = await memberstack.getCurrentMember();
const member = result?.data;
if (!member) return null;
const planConnections = member.planConnections || member.data?.planConnections || member.plans || [];
currentPlanIds = [];
planConnections.forEach(connection => {
const planId = connection?.planId;
if (planId && (planId === CONFIG.PLAN_A_ID || planId === CONFIG.PLAN_B_ID)) {
currentPlanIds.push(planId);
}
});
return member;
} catch (error) {
console.error('MemberScript #190: Error getting member data:', error);
return null;
}
}
function canVote(voterPlanIds, targetPlanId) {
if (!voterPlanIds || !targetPlanId) return false;
const voterPlans = Array.isArray(voterPlanIds) ? voterPlanIds : [voterPlanIds];
const targetIsPlanA = targetPlanId === CONFIG.PLAN_A_ID;
const targetIsPlanB = targetPlanId === CONFIG.PLAN_B_ID;
const voterHasPlanA = voterPlans.includes(CONFIG.PLAN_A_ID);
const voterHasPlanB = voterPlans.includes(CONFIG.PLAN_B_ID);
return (voterHasPlanA && targetIsPlanB) || (voterHasPlanB && targetIsPlanA);
}
async function handleVote(event, voteType) {
event.preventDefault();
event.stopPropagation();
if (!currentMember || currentPlanIds.length === 0) {
showMessage(CONFIG.MESSAGES.notLoggedIn, 'error');
return;
}
const button = event.currentTarget;
const profileContainer = button.closest(CONFIG.SELECTORS.PROFILE);
if (!profileContainer) return;
const targetMemberId = profileContainer.getAttribute('data-target-member-id');
const targetPlanId = profileContainer.getAttribute('data-target-plan-id');
if (!targetMemberId || !targetPlanId) {
showMessage(CONFIG.MESSAGES.invalidTarget, 'error');
return;
}
if (!canVote(currentPlanIds, targetPlanId)) {
showMessage(CONFIG.MESSAGES.wrongPlan, 'error');
return;
}
const currentMemberId = currentMember.id || currentMember._id;
if (currentMemberId === targetMemberId) {
showMessage(CONFIG.MESSAGES.selfVote, 'warning');
return;
}
const upvoteBtn = profileContainer.querySelector(CONFIG.SELECTORS.UPVOTE);
const downvoteBtn = profileContainer.querySelector(CONFIG.SELECTORS.DOWNVOTE);
// Check if already voted with this specific action
if ((voteType === 'upvote' && hasUpvoted(currentMemberId, targetMemberId)) ||
(voteType === 'downvote' && hasDownvoted(currentMemberId, targetMemberId))) {
return;
}
// Check if button already disabled
if ((voteType === 'upvote' && upvoteBtn?.classList.contains('voted')) ||
(voteType === 'downvote' && downvoteBtn?.classList.contains('voted'))) {
return;
}
// Prevent double submission
if (profileContainer.getAttribute('data-submitting') === 'true') return;
profileContainer.setAttribute('data-submitting', 'true');
setButtonState(upvoteBtn, true);
setButtonState(downvoteBtn, true);
try {
const form = profileContainer.querySelector(CONFIG.SELECTORS.FORM);
if (!form) {
showMessage(CONFIG.MESSAGES.notFoundForm, 'error');
setButtonState(upvoteBtn, false);
setButtonState(downvoteBtn, false);
return;
}
const voterField = form.querySelector('[data-ms-code="voter-member-id"]');
const targetField = form.querySelector('[data-ms-code="target-member-id"]');
const actionField = form.querySelector('[data-ms-code="vote-action"]');
const tsField = form.querySelector('[data-ms-code="vote-timestamp"]');
if (voterField) voterField.value = currentMemberId;
if (targetField) targetField.value = targetMemberId;
if (actionField) actionField.value = voteType;
if (tsField) tsField.value = String(Date.now());
form.submit();
// Update UI: disable the clicked button
setButtonState(voteType === 'upvote' ? upvoteBtn : downvoteBtn, true);
// Only re-enable the other button if it wasn't previously voted on
if (voteType === 'upvote' && !hasDownvoted(currentMemberId, targetMemberId)) {
setButtonState(downvoteBtn, false);
} else if (voteType === 'downvote' && !hasUpvoted(currentMemberId, targetMemberId)) {
setButtonState(upvoteBtn, false);
}
// Save to appropriate localStorage
if (voteType === 'upvote') {
setUpvoted(currentMemberId, targetMemberId);
} else {
setDownvoted(currentMemberId, targetMemberId);
}
showMessage(voteType === 'upvote' ? CONFIG.MESSAGES.upvoteSuccess : CONFIG.MESSAGES.downvoteSuccess, 'success');
profileContainer.setAttribute('data-submitting', 'false');
} catch (err) {
console.error('MemberScript #190: Error submitting vote form:', err);
showMessage(CONFIG.MESSAGES.submitError, 'error');
setButtonState(upvoteBtn, false);
setButtonState(downvoteBtn, false);
profileContainer.setAttribute('data-submitting', 'false');
}
}
function showMessage(message, type = 'info') {
const colors = { info: '#3b82f6', success: '#10b981', warning: '#f59e0b', error: '#ef4444' };
const msgEl = document.createElement('div');
msgEl.setAttribute('data-ms-code', 'vote-message');
msgEl.textContent = message;
msgEl.style.cssText = `
position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px;
color: white; background: ${colors[type] || colors.info}; z-index: 10000;
font-size: 14px; font-weight: 500; max-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); animation: slideIn 0.3s ease-out;
`;
document.body.appendChild(msgEl);
setTimeout(() => {
msgEl.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => msgEl.remove(), 300);
}, CONFIG.TOAST_MS);
}
function initializeVoting() {
// Attach event listeners
document.querySelectorAll(CONFIG.SELECTORS.UPVOTE).forEach(btn => {
btn.addEventListener('click', (e) => handleVote(e, 'upvote'));
});
document.querySelectorAll(CONFIG.SELECTORS.DOWNVOTE).forEach(btn => {
btn.addEventListener('click', (e) => handleVote(e, 'downvote'));
});
// Restore button states from localStorage (check both upvotes and downvotes separately)
if (currentMember) {
const currentMemberId = currentMember.id || currentMember._id;
document.querySelectorAll(CONFIG.SELECTORS.PROFILE).forEach(profile => {
const targetMemberId = profile.getAttribute('data-target-member-id');
const upvoteBtn = profile.querySelector(CONFIG.SELECTORS.UPVOTE);
const downvoteBtn = profile.querySelector(CONFIG.SELECTORS.DOWNVOTE);
// Check upvotes and downvotes independently
if (hasUpvoted(currentMemberId, targetMemberId)) {
setButtonState(upvoteBtn, true);
}
if (hasDownvoted(currentMemberId, targetMemberId)) {
setButtonState(downvoteBtn, true);
}
});
}
// Enable/disable based on plan compatibility
if (currentPlanIds.length > 0) {
document.querySelectorAll(CONFIG.SELECTORS.PROFILE).forEach(profile => {
const targetPlanId = profile.getAttribute('data-target-plan-id');
const canVoteOnThis = canVote(currentPlanIds, targetPlanId);
const upvoteBtn = profile.querySelector(CONFIG.SELECTORS.UPVOTE);
const downvoteBtn = profile.querySelector(CONFIG.SELECTORS.DOWNVOTE);
if (!canVoteOnThis) {
setButtonState(upvoteBtn, true);
setButtonState(downvoteBtn, true);
if (upvoteBtn) upvoteBtn.style.opacity = '0.5';
if (downvoteBtn) downvoteBtn.style.opacity = '0.5';
}
});
}
}
// Wait for Memberstack
async function waitForMemberstack() {
if (window.$memberstackDom?.getCurrentMember) return;
return new Promise((resolve) => {
if (window.$memberstackDom) {
document.addEventListener('memberstack.ready', resolve);
setTimeout(resolve, 2000);
} else {
const check = setInterval(() => {
if (window.$memberstackDom) {
clearInterval(check);
resolve();
}
}, 100);
setTimeout(() => { clearInterval(check); resolve(); }, 3000);
}
});
}
// Initialize
document.addEventListener('DOMContentLoaded', async function() {
try {
await waitForMemberstack();
currentMember = await getCurrentMemberData();
setTimeout(() => {
initializeVoting();
}, 100);
} catch (error) {
console.error('MemberScript #190: Error initializing:', error);
}
});
})();
</script>
<style>
[data-ms-code="upvote-button"].voted,
[data-ms-code="downvote-button"].voted {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
</style>

#189 - Webflow CMS Interactive Quiz
Create an interactive quiz system with progress tracking and answer feedback.
<!-- 💙 MEMBERSCRIPT #189 v0.1 💙 - WEBFLOW CMS INTERACTIVE QUIZ -->
<script>
(function() {
'use strict';
const CSS = {
selectedBorder: '#3b82f6', correctBorder: '#10b981', correctBg: '#d1fae5',
incorrectBorder: '#ef4444', incorrectBg: '#fee2e2', feedbackDelay: 1500,
progressColor: '#3b82f6', msgColors: { info: '#3b82f6', success: '#10b981', warning: '#f59e0b', error: '#ef4444' }
};
let quizContainer, questions, currentQ = 0, answers = {}, score = 0, member = null, quizId = null;
const wait = ms => new Promise(r => setTimeout(r, ms));
const waitForWebflow = () => new Promise(r => {
if (document.querySelectorAll('[data-ms-code="quiz-question"]').length > 0) return setTimeout(r, 100);
if (window.Webflow?.require) window.Webflow.require('ix2').then(() => setTimeout(r, 100));
else {
const i = setInterval(() => {
if (document.querySelectorAll('[data-ms-code="quiz-question"]').length > 0) {
clearInterval(i); setTimeout(r, 100);
}
}, 100);
setTimeout(() => { clearInterval(i); r(); }, 3000);
}
});
const getAnswers = q => {
let opts = q.querySelectorAll('[data-ms-code="answer-option"]');
if (!opts.length) {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) opts = item.querySelectorAll('[data-ms-code="answer-option"]');
}
return opts;
};
const getCorrectAnswer = q => {
let ref = q.querySelector('[data-ms-code="correct-answer"]');
if (!ref) {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) ref = item.querySelector('[data-ms-code="correct-answer"]');
}
return ref;
};
const getNextButton = q => {
let btn = q.querySelector('[data-ms-code="quiz-next"]');
if (!btn) {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) btn = item.querySelector('[data-ms-code="quiz-next"]');
}
return btn;
};
const setButtonState = (btn, enabled) => {
if (!btn) return;
if (btn.tagName === 'A') {
btn.style.pointerEvents = enabled ? 'auto' : 'none';
btn.style.opacity = enabled ? '1' : '0.5';
} else btn.disabled = !enabled;
btn.setAttribute('data-ms-disabled', enabled ? 'false' : 'true');
};
const clearStyles = opts => opts.forEach(opt => {
opt.removeAttribute('data-ms-state');
opt.style.borderWidth = opt.style.borderStyle = opt.style.borderColor = opt.style.backgroundColor = '';
});
const applyFeedback = (opt, isCorrect) => {
opt.style.borderWidth = '2px';
opt.style.borderStyle = 'solid';
if (isCorrect) {
opt.style.borderColor = CSS.correctBorder;
opt.style.backgroundColor = CSS.correctBg;
opt.setAttribute('data-ms-state', 'correct');
} else {
opt.style.borderColor = CSS.incorrectBorder;
opt.style.backgroundColor = CSS.incorrectBg;
opt.setAttribute('data-ms-state', 'incorrect');
}
};
const randomizeAnswers = q => {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
const container = item ? item.querySelector('[data-ms-code="quiz-answers"]') : q.querySelector('[data-ms-code="quiz-answers"]');
if (!container) return;
const opts = Array.from(getAnswers(q));
if (opts.length <= 1) return;
for (let i = opts.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[opts[i], opts[j]] = [opts[j], opts[i]];
}
opts.forEach(opt => container.appendChild(opt));
};
const getJSON = async () => {
try {
const ms = window.$memberstackDom;
if (!ms) return {};
const json = await ms.getMemberJSON();
return json?.data || json || {};
} catch (e) {
return {};
}
};
const generateQuizId = qs => {
if (!qs || !qs.length) return `quiz-${Date.now()}`;
const text = qs[0].querySelector('[data-ms-code="question-text"]')?.textContent || '';
const hash = text.split('').reduce((a, c) => ((a << 5) - a) + c.charCodeAt(0), 0);
return `quiz-${qs.length}-${Math.abs(hash)}`;
};
const loadSavedAnswers = async qId => {
try {
if (!window.$memberstackDom || !member) return null;
const data = await getJSON();
return data.quizData?.[qId]?.questions || null;
} catch (e) {
return null;
}
};
const restoreAnswers = saved => {
if (!saved) return;
questions.forEach((q, qi) => {
const qId = q.getAttribute('data-question-id') || `q-${qi}`;
const ans = saved[qId];
if (ans?.answer) {
const opts = getAnswers(q);
opts.forEach(opt => {
if ((opt.textContent || '').trim() === ans.answer.trim()) {
opt.setAttribute('data-ms-state', 'selected');
opt.style.borderWidth = '2px';
opt.style.borderStyle = 'solid';
opt.style.borderColor = CSS.selectedBorder;
answers[qId] = { answer: ans.answer.trim(), correct: ans.correct, element: opt };
if (qi === currentQ) setButtonState(getNextButton(q), true);
}
});
}
});
};
const saveQuestionAnswer = async (qId, questionId, answer, isCorrect) => {
try {
const ms = window.$memberstackDom;
if (!ms || !member) return;
const data = await getJSON();
if (!data.quizData) data.quizData = {};
if (!data.quizData[qId]) data.quizData[qId] = { questions: {}, completed: false };
data.quizData[qId].questions[questionId] = { answer, correct: isCorrect, answeredAt: new Date().toISOString() };
await ms.updateMemberJSON({ json: data });
} catch (e) {}
};
const showMsg = (msg, type = 'info') => {
const el = document.createElement('div');
el.setAttribute('data-ms-code', 'quiz-message');
el.textContent = msg;
el.style.cssText = `position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:8px;color:white;z-index:10000;font-size:14px;font-weight:500;max-width:300px;box-shadow:0 4px 12px rgba(0,0,0,0.15);background:${CSS.msgColors[type] || CSS.msgColors.info};`;
document.body.appendChild(el);
setTimeout(() => el.remove(), 3000);
};
const updateProgress = (curr, total) => {
const bar = quizContainer.querySelector('[data-ms-code="quiz-progress"]');
const text = quizContainer.querySelector('[data-ms-code="quiz-progress-text"]');
if (bar) {
bar.style.width = (curr / total * 100) + '%';
bar.style.backgroundColor = CSS.progressColor;
}
if (text) text.textContent = `Question ${curr} of ${total}`;
};
const restartQuiz = () => {
currentQ = 0; score = 0; answers = {};
const results = quizContainer.querySelector('[data-ms-code="quiz-results"]');
if (results) results.style.display = 'none';
questions.forEach((q, i) => {
const visible = i === 0;
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) {
item.style.display = visible ? 'block' : 'none';
item.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
}
q.style.display = visible ? 'block' : 'none';
q.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
clearStyles(getAnswers(q));
if (i === 0) setButtonState(getNextButton(q), false);
});
if (window.$memberstackDom && member && quizId) {
loadSavedAnswers(quizId).then(saved => { if (saved) restoreAnswers(saved); });
}
updateProgress(1, questions.length);
quizContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
showMsg('Quiz restarted!', 'info');
};
document.addEventListener("DOMContentLoaded", async function() {
try {
await waitForWebflow();
if (window.$memberstackDom) {
const ms = window.$memberstackDom;
await (ms.onReady || Promise.resolve());
const data = await ms.getCurrentMember();
member = data?.data || data;
}
await init();
} catch (e) {
console.error('MemberScript #189: Error:', e);
}
});
async function init() {
quizContainer = document.querySelector('[data-ms-code="quiz-container"]');
if (!quizContainer) return console.warn('MemberScript #189: Quiz container not found.');
questions = Array.from(quizContainer.querySelectorAll('[data-ms-code="quiz-question"]'));
if (!questions.length) return console.warn('MemberScript #189: No questions found.');
quizId = quizContainer.getAttribute('data-quiz-id') || generateQuizId(questions);
let savedAnswers = null;
if (window.$memberstackDom && member) savedAnswers = await loadSavedAnswers(quizId);
const noRandom = quizContainer.getAttribute('data-randomize-answers') === 'false';
questions.forEach((q, i) => {
const visible = i === 0;
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) {
item.style.display = visible ? 'block' : 'none';
item.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
}
q.style.display = visible ? 'block' : 'none';
q.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
if (!noRandom) randomizeAnswers(q);
const correctRef = getCorrectAnswer(q);
if (correctRef) {
correctRef.style.display = 'none';
correctRef.style.visibility = 'hidden';
const opts = getAnswers(q);
if (opts.length) {
const correctText = (correctRef.textContent || '').replace(/\s+/g, ' ').trim();
opts.forEach(opt => {
if ((opt.textContent || '').replace(/\s+/g, ' ').trim() === correctText) {
opt.setAttribute('data-is-correct', 'true');
}
});
}
}
});
const results = quizContainer.querySelector('[data-ms-code="quiz-results"]');
if (results) results.style.display = 'none';
currentQ = 0; score = 0; answers = {};
if (savedAnswers) restoreAnswers(savedAnswers);
questions.forEach((q, qi) => {
getAnswers(q).forEach(opt => {
opt.addEventListener('click', function(e) {
if (this.tagName === 'A') { e.preventDefault(); e.stopPropagation(); }
clearStyles(getAnswers(q));
this.setAttribute('data-ms-state', 'selected');
this.style.borderWidth = '2px';
this.style.borderStyle = 'solid';
this.style.borderColor = CSS.selectedBorder;
const qId = q.getAttribute('data-question-id') || `q-${qi}`;
const answerText = this.textContent.trim();
const isCorrect = this.getAttribute('data-is-correct') === 'true';
answers[qId] = { answer: answerText, correct: isCorrect, element: this };
if (quizId && window.$memberstackDom && member) saveQuestionAnswer(quizId, qId, answerText, isCorrect);
setButtonState(getNextButton(q), true);
});
});
});
quizContainer.querySelectorAll('[data-ms-code="quiz-next"], [data-ms-code="quiz-submit"]').forEach(btn => {
btn.addEventListener('click', function(e) {
if (this.tagName === 'A') { e.preventDefault(); e.stopPropagation(); }
const q = questions[currentQ];
const qId = q.getAttribute('data-question-id') || `q-${currentQ}`;
if (!answers[qId]) {
showMsg('Please select an answer before continuing.', 'warning');
return;
}
showFeedback(q, answers[qId]);
setTimeout(() => moveNext(), CSS.feedbackDelay);
});
});
document.querySelectorAll('[data-ms-code="restart-quiz"]').forEach(btn => {
btn.addEventListener('click', function(e) {
if (this.tagName === 'A') { e.preventDefault(); e.stopPropagation(); }
restartQuiz();
});
});
function showFeedback(q, data) {
const opts = getAnswers(q);
clearStyles(opts);
const selected = data.element || Array.from(opts).find(o => o.textContent.trim() === data.answer);
if (selected) {
applyFeedback(selected, data.correct);
if (!data.correct) {
opts.forEach(opt => {
if (opt.getAttribute('data-is-correct') === 'true') {
applyFeedback(opt, true);
opt.setAttribute('data-ms-state', 'highlight');
}
});
}
}
}
function moveNext() {
const q = questions[currentQ];
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) {
item.style.display = 'none';
item.setAttribute('data-ms-display', 'hidden');
}
q.style.display = 'none';
q.setAttribute('data-ms-display', 'hidden');
if (currentQ < questions.length - 1) {
currentQ++;
const nextQ = questions[currentQ];
const nextItem = nextQ.closest('[data-ms-code="quiz-item"]') || nextQ.closest('.w-dyn-item');
if (nextItem) {
nextItem.style.display = 'block';
nextItem.setAttribute('data-ms-display', 'visible');
}
nextQ.style.display = 'block';
nextQ.setAttribute('data-ms-display', 'visible');
setButtonState(getNextButton(nextQ), false);
clearStyles(getAnswers(nextQ));
updateProgress(currentQ + 1, questions.length);
nextQ.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else finish();
}
function finish() {
score = Object.values(answers).filter(a => a.correct).length;
const total = questions.length;
const pct = Math.round((score / total) * 100);
questions.forEach(q => {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) item.style.display = 'none';
q.style.display = 'none';
});
const results = quizContainer.querySelector('[data-ms-code="quiz-results"]');
if (!results) return console.warn('MemberScript #189: Results container not found.');
const s = results.querySelector('[data-ms-code="quiz-score"]');
const t = results.querySelector('[data-ms-code="quiz-total"]');
const p = results.querySelector('[data-ms-code="quiz-percentage"]');
if (s) s.textContent = score;
if (t) t.textContent = total;
if (p) p.textContent = pct + '%';
results.style.display = 'flex';
results.scrollIntoView({ behavior: 'smooth', block: 'start' });
if (window.$memberstackDom) saveScore(score, total, pct);
}
async function saveScore(score, total, pct) {
try {
const ms = window.$memberstackDom;
if (!ms) return;
const currentMember = await ms.getCurrentMember();
if (!currentMember || !currentMember.data) return;
const existingData = await getJSON();
const existingScores = Array.isArray(existingData.quizScores) ? existingData.quizScores : [];
if (!existingData.quizData) existingData.quizData = {};
if (!existingData.quizData[quizId]) existingData.quizData[quizId] = { questions: {}, completed: false };
existingData.quizData[quizId].completed = true;
existingData.quizData[quizId].score = score;
existingData.quizData[quizId].total = total;
existingData.quizData[quizId].percentage = pct;
existingData.quizData[quizId].completedAt = new Date().toISOString();
const updatedData = {
...existingData,
quizScores: [...existingScores, { score, total, percentage: pct, completedAt: new Date().toISOString() }],
lastQuizScore: score, lastQuizTotal: total, lastQuizPercentage: pct
};
await ms.updateMemberJSON({ json: updatedData });
showMsg('Score saved to your profile!', 'success');
} catch (e) {
console.error('MemberScript #189: Error saving score:', e);
showMsg('Error saving score. Please try again.', 'error');
}
}
updateProgress(1, questions.length);
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #189 v0.1 💙 - WEBFLOW CMS INTERACTIVE QUIZ -->
<script>
(function() {
'use strict';
const CSS = {
selectedBorder: '#3b82f6', correctBorder: '#10b981', correctBg: '#d1fae5',
incorrectBorder: '#ef4444', incorrectBg: '#fee2e2', feedbackDelay: 1500,
progressColor: '#3b82f6', msgColors: { info: '#3b82f6', success: '#10b981', warning: '#f59e0b', error: '#ef4444' }
};
let quizContainer, questions, currentQ = 0, answers = {}, score = 0, member = null, quizId = null;
const wait = ms => new Promise(r => setTimeout(r, ms));
const waitForWebflow = () => new Promise(r => {
if (document.querySelectorAll('[data-ms-code="quiz-question"]').length > 0) return setTimeout(r, 100);
if (window.Webflow?.require) window.Webflow.require('ix2').then(() => setTimeout(r, 100));
else {
const i = setInterval(() => {
if (document.querySelectorAll('[data-ms-code="quiz-question"]').length > 0) {
clearInterval(i); setTimeout(r, 100);
}
}, 100);
setTimeout(() => { clearInterval(i); r(); }, 3000);
}
});
const getAnswers = q => {
let opts = q.querySelectorAll('[data-ms-code="answer-option"]');
if (!opts.length) {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) opts = item.querySelectorAll('[data-ms-code="answer-option"]');
}
return opts;
};
const getCorrectAnswer = q => {
let ref = q.querySelector('[data-ms-code="correct-answer"]');
if (!ref) {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) ref = item.querySelector('[data-ms-code="correct-answer"]');
}
return ref;
};
const getNextButton = q => {
let btn = q.querySelector('[data-ms-code="quiz-next"]');
if (!btn) {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) btn = item.querySelector('[data-ms-code="quiz-next"]');
}
return btn;
};
const setButtonState = (btn, enabled) => {
if (!btn) return;
if (btn.tagName === 'A') {
btn.style.pointerEvents = enabled ? 'auto' : 'none';
btn.style.opacity = enabled ? '1' : '0.5';
} else btn.disabled = !enabled;
btn.setAttribute('data-ms-disabled', enabled ? 'false' : 'true');
};
const clearStyles = opts => opts.forEach(opt => {
opt.removeAttribute('data-ms-state');
opt.style.borderWidth = opt.style.borderStyle = opt.style.borderColor = opt.style.backgroundColor = '';
});
const applyFeedback = (opt, isCorrect) => {
opt.style.borderWidth = '2px';
opt.style.borderStyle = 'solid';
if (isCorrect) {
opt.style.borderColor = CSS.correctBorder;
opt.style.backgroundColor = CSS.correctBg;
opt.setAttribute('data-ms-state', 'correct');
} else {
opt.style.borderColor = CSS.incorrectBorder;
opt.style.backgroundColor = CSS.incorrectBg;
opt.setAttribute('data-ms-state', 'incorrect');
}
};
const randomizeAnswers = q => {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
const container = item ? item.querySelector('[data-ms-code="quiz-answers"]') : q.querySelector('[data-ms-code="quiz-answers"]');
if (!container) return;
const opts = Array.from(getAnswers(q));
if (opts.length <= 1) return;
for (let i = opts.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[opts[i], opts[j]] = [opts[j], opts[i]];
}
opts.forEach(opt => container.appendChild(opt));
};
const getJSON = async () => {
try {
const ms = window.$memberstackDom;
if (!ms) return {};
const json = await ms.getMemberJSON();
return json?.data || json || {};
} catch (e) {
return {};
}
};
const generateQuizId = qs => {
if (!qs || !qs.length) return `quiz-${Date.now()}`;
const text = qs[0].querySelector('[data-ms-code="question-text"]')?.textContent || '';
const hash = text.split('').reduce((a, c) => ((a << 5) - a) + c.charCodeAt(0), 0);
return `quiz-${qs.length}-${Math.abs(hash)}`;
};
const loadSavedAnswers = async qId => {
try {
if (!window.$memberstackDom || !member) return null;
const data = await getJSON();
return data.quizData?.[qId]?.questions || null;
} catch (e) {
return null;
}
};
const restoreAnswers = saved => {
if (!saved) return;
questions.forEach((q, qi) => {
const qId = q.getAttribute('data-question-id') || `q-${qi}`;
const ans = saved[qId];
if (ans?.answer) {
const opts = getAnswers(q);
opts.forEach(opt => {
if ((opt.textContent || '').trim() === ans.answer.trim()) {
opt.setAttribute('data-ms-state', 'selected');
opt.style.borderWidth = '2px';
opt.style.borderStyle = 'solid';
opt.style.borderColor = CSS.selectedBorder;
answers[qId] = { answer: ans.answer.trim(), correct: ans.correct, element: opt };
if (qi === currentQ) setButtonState(getNextButton(q), true);
}
});
}
});
};
const saveQuestionAnswer = async (qId, questionId, answer, isCorrect) => {
try {
const ms = window.$memberstackDom;
if (!ms || !member) return;
const data = await getJSON();
if (!data.quizData) data.quizData = {};
if (!data.quizData[qId]) data.quizData[qId] = { questions: {}, completed: false };
data.quizData[qId].questions[questionId] = { answer, correct: isCorrect, answeredAt: new Date().toISOString() };
await ms.updateMemberJSON({ json: data });
} catch (e) {}
};
const showMsg = (msg, type = 'info') => {
const el = document.createElement('div');
el.setAttribute('data-ms-code', 'quiz-message');
el.textContent = msg;
el.style.cssText = `position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:8px;color:white;z-index:10000;font-size:14px;font-weight:500;max-width:300px;box-shadow:0 4px 12px rgba(0,0,0,0.15);background:${CSS.msgColors[type] || CSS.msgColors.info};`;
document.body.appendChild(el);
setTimeout(() => el.remove(), 3000);
};
const updateProgress = (curr, total) => {
const bar = quizContainer.querySelector('[data-ms-code="quiz-progress"]');
const text = quizContainer.querySelector('[data-ms-code="quiz-progress-text"]');
if (bar) {
bar.style.width = (curr / total * 100) + '%';
bar.style.backgroundColor = CSS.progressColor;
}
if (text) text.textContent = `Question ${curr} of ${total}`;
};
const restartQuiz = () => {
currentQ = 0; score = 0; answers = {};
const results = quizContainer.querySelector('[data-ms-code="quiz-results"]');
if (results) results.style.display = 'none';
questions.forEach((q, i) => {
const visible = i === 0;
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) {
item.style.display = visible ? 'block' : 'none';
item.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
}
q.style.display = visible ? 'block' : 'none';
q.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
clearStyles(getAnswers(q));
if (i === 0) setButtonState(getNextButton(q), false);
});
if (window.$memberstackDom && member && quizId) {
loadSavedAnswers(quizId).then(saved => { if (saved) restoreAnswers(saved); });
}
updateProgress(1, questions.length);
quizContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
showMsg('Quiz restarted!', 'info');
};
document.addEventListener("DOMContentLoaded", async function() {
try {
await waitForWebflow();
if (window.$memberstackDom) {
const ms = window.$memberstackDom;
await (ms.onReady || Promise.resolve());
const data = await ms.getCurrentMember();
member = data?.data || data;
}
await init();
} catch (e) {
console.error('MemberScript #189: Error:', e);
}
});
async function init() {
quizContainer = document.querySelector('[data-ms-code="quiz-container"]');
if (!quizContainer) return console.warn('MemberScript #189: Quiz container not found.');
questions = Array.from(quizContainer.querySelectorAll('[data-ms-code="quiz-question"]'));
if (!questions.length) return console.warn('MemberScript #189: No questions found.');
quizId = quizContainer.getAttribute('data-quiz-id') || generateQuizId(questions);
let savedAnswers = null;
if (window.$memberstackDom && member) savedAnswers = await loadSavedAnswers(quizId);
const noRandom = quizContainer.getAttribute('data-randomize-answers') === 'false';
questions.forEach((q, i) => {
const visible = i === 0;
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) {
item.style.display = visible ? 'block' : 'none';
item.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
}
q.style.display = visible ? 'block' : 'none';
q.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
if (!noRandom) randomizeAnswers(q);
const correctRef = getCorrectAnswer(q);
if (correctRef) {
correctRef.style.display = 'none';
correctRef.style.visibility = 'hidden';
const opts = getAnswers(q);
if (opts.length) {
const correctText = (correctRef.textContent || '').replace(/\s+/g, ' ').trim();
opts.forEach(opt => {
if ((opt.textContent || '').replace(/\s+/g, ' ').trim() === correctText) {
opt.setAttribute('data-is-correct', 'true');
}
});
}
}
});
const results = quizContainer.querySelector('[data-ms-code="quiz-results"]');
if (results) results.style.display = 'none';
currentQ = 0; score = 0; answers = {};
if (savedAnswers) restoreAnswers(savedAnswers);
questions.forEach((q, qi) => {
getAnswers(q).forEach(opt => {
opt.addEventListener('click', function(e) {
if (this.tagName === 'A') { e.preventDefault(); e.stopPropagation(); }
clearStyles(getAnswers(q));
this.setAttribute('data-ms-state', 'selected');
this.style.borderWidth = '2px';
this.style.borderStyle = 'solid';
this.style.borderColor = CSS.selectedBorder;
const qId = q.getAttribute('data-question-id') || `q-${qi}`;
const answerText = this.textContent.trim();
const isCorrect = this.getAttribute('data-is-correct') === 'true';
answers[qId] = { answer: answerText, correct: isCorrect, element: this };
if (quizId && window.$memberstackDom && member) saveQuestionAnswer(quizId, qId, answerText, isCorrect);
setButtonState(getNextButton(q), true);
});
});
});
quizContainer.querySelectorAll('[data-ms-code="quiz-next"], [data-ms-code="quiz-submit"]').forEach(btn => {
btn.addEventListener('click', function(e) {
if (this.tagName === 'A') { e.preventDefault(); e.stopPropagation(); }
const q = questions[currentQ];
const qId = q.getAttribute('data-question-id') || `q-${currentQ}`;
if (!answers[qId]) {
showMsg('Please select an answer before continuing.', 'warning');
return;
}
showFeedback(q, answers[qId]);
setTimeout(() => moveNext(), CSS.feedbackDelay);
});
});
document.querySelectorAll('[data-ms-code="restart-quiz"]').forEach(btn => {
btn.addEventListener('click', function(e) {
if (this.tagName === 'A') { e.preventDefault(); e.stopPropagation(); }
restartQuiz();
});
});
function showFeedback(q, data) {
const opts = getAnswers(q);
clearStyles(opts);
const selected = data.element || Array.from(opts).find(o => o.textContent.trim() === data.answer);
if (selected) {
applyFeedback(selected, data.correct);
if (!data.correct) {
opts.forEach(opt => {
if (opt.getAttribute('data-is-correct') === 'true') {
applyFeedback(opt, true);
opt.setAttribute('data-ms-state', 'highlight');
}
});
}
}
}
function moveNext() {
const q = questions[currentQ];
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) {
item.style.display = 'none';
item.setAttribute('data-ms-display', 'hidden');
}
q.style.display = 'none';
q.setAttribute('data-ms-display', 'hidden');
if (currentQ < questions.length - 1) {
currentQ++;
const nextQ = questions[currentQ];
const nextItem = nextQ.closest('[data-ms-code="quiz-item"]') || nextQ.closest('.w-dyn-item');
if (nextItem) {
nextItem.style.display = 'block';
nextItem.setAttribute('data-ms-display', 'visible');
}
nextQ.style.display = 'block';
nextQ.setAttribute('data-ms-display', 'visible');
setButtonState(getNextButton(nextQ), false);
clearStyles(getAnswers(nextQ));
updateProgress(currentQ + 1, questions.length);
nextQ.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else finish();
}
function finish() {
score = Object.values(answers).filter(a => a.correct).length;
const total = questions.length;
const pct = Math.round((score / total) * 100);
questions.forEach(q => {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('.w-dyn-item');
if (item) item.style.display = 'none';
q.style.display = 'none';
});
const results = quizContainer.querySelector('[data-ms-code="quiz-results"]');
if (!results) return console.warn('MemberScript #189: Results container not found.');
const s = results.querySelector('[data-ms-code="quiz-score"]');
const t = results.querySelector('[data-ms-code="quiz-total"]');
const p = results.querySelector('[data-ms-code="quiz-percentage"]');
if (s) s.textContent = score;
if (t) t.textContent = total;
if (p) p.textContent = pct + '%';
results.style.display = 'flex';
results.scrollIntoView({ behavior: 'smooth', block: 'start' });
if (window.$memberstackDom) saveScore(score, total, pct);
}
async function saveScore(score, total, pct) {
try {
const ms = window.$memberstackDom;
if (!ms) return;
const currentMember = await ms.getCurrentMember();
if (!currentMember || !currentMember.data) return;
const existingData = await getJSON();
const existingScores = Array.isArray(existingData.quizScores) ? existingData.quizScores : [];
if (!existingData.quizData) existingData.quizData = {};
if (!existingData.quizData[quizId]) existingData.quizData[quizId] = { questions: {}, completed: false };
existingData.quizData[quizId].completed = true;
existingData.quizData[quizId].score = score;
existingData.quizData[quizId].total = total;
existingData.quizData[quizId].percentage = pct;
existingData.quizData[quizId].completedAt = new Date().toISOString();
const updatedData = {
...existingData,
quizScores: [...existingScores, { score, total, percentage: pct, completedAt: new Date().toISOString() }],
lastQuizScore: score, lastQuizTotal: total, lastQuizPercentage: pct
};
await ms.updateMemberJSON({ json: updatedData });
showMsg('Score saved to your profile!', 'success');
} catch (e) {
console.error('MemberScript #189: Error saving score:', e);
showMsg('Error saving score. Please try again.', 'error');
}
}
updateProgress(1, questions.length);
}
})();
</script>

#188 - Show/hide content based on a plan instead of gated content
Show or hide content dynamically based on your member's current subscription plan tier.
<!-- 💙 MEMBERSCRIPT #188 v0.1 - SHOW/HIDE CONTENT BASED ON PLAN 💙 -->
<script>
(function() {
'use strict';
document.addEventListener("DOMContentLoaded", async function() {
try {
// Check if Memberstack is loaded
if (!window.$memberstackDom) {
console.error('MemberScript #188: Memberstack DOM package is not loaded.');
return;
}
const memberstack = window.$memberstackDom;
// Wait for Memberstack to be ready
await waitForMemberstack();
// Get current member
const { data: member } = await memberstack.getCurrentMember();
// If no member is logged in, remove all plan-specific content
if (!member) {
removeAllPlanContent();
return;
}
// Get the current plan connection
let planConnections = null;
if (member.planConnections) {
planConnections = member.planConnections;
} else if (member.data && member.data.planConnections) {
planConnections = member.data.planConnections;
} else if (member.plans) {
planConnections = member.plans;
}
// If no plan connections, remove all plan-specific content
if (!planConnections || planConnections.length === 0) {
removeAllPlanContent();
return;
}
// Get the current plan ID from the most recent active plan
const currentPlanConnection = planConnections[0];
const currentPlanId = currentPlanConnection?.planId;
const currentPriceId = currentPlanConnection?.payment?.priceId;
// Collect all possible IDs to match against
const matchingIds = [];
if (currentPlanId) matchingIds.push(currentPlanId);
if (currentPriceId) matchingIds.push(currentPriceId);
if (matchingIds.length === 0) {
removeAllPlanContent();
return;
}
// Show/remove content based on plan/price IDs
showPlanSpecificContent(matchingIds);
} catch (error) {
console.error('MemberScript #188: Error showing/hiding plan content:', error);
// On error, remove all plan-specific content for security
removeAllPlanContent();
}
});
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackReady) {
resolve();
} else {
document.addEventListener('memberstack.ready', resolve);
// Fallback timeout
setTimeout(resolve, 2000);
}
});
}
function showPlanSpecificContent(matchingIds) {
// Find all elements with data-ms-code attributes (plan-specific content)
const allPlanElements = document.querySelectorAll('[data-ms-code]');
allPlanElements.forEach(element => {
const elementPlanId = element.getAttribute('data-ms-code');
// Check if element's plan ID matches any of the member's IDs (planId or priceId)
const shouldShow = matchingIds.includes(elementPlanId);
if (shouldShow) {
element.classList.remove('ms-plan-hidden');
element.classList.add('ms-plan-visible');
} else {
element.remove();
}
});
if (allPlanElements.length === 0) {
console.warn('MemberScript #188: No elements with data-ms-code attributes found. Add data-ms-code="PLAN_ID" to elements you want to show/hide based on plan.');
}
}
function removeAllPlanContent() {
// Completely remove all elements with data-ms-code attributes if no member is logged in
const allPlanElements = document.querySelectorAll('[data-ms-code]');
allPlanElements.forEach(element => {
element.remove();
});
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #188 v0.1 - SHOW/HIDE CONTENT BASED ON PLAN 💙 -->
<script>
(function() {
'use strict';
document.addEventListener("DOMContentLoaded", async function() {
try {
// Check if Memberstack is loaded
if (!window.$memberstackDom) {
console.error('MemberScript #188: Memberstack DOM package is not loaded.');
return;
}
const memberstack = window.$memberstackDom;
// Wait for Memberstack to be ready
await waitForMemberstack();
// Get current member
const { data: member } = await memberstack.getCurrentMember();
// If no member is logged in, remove all plan-specific content
if (!member) {
removeAllPlanContent();
return;
}
// Get the current plan connection
let planConnections = null;
if (member.planConnections) {
planConnections = member.planConnections;
} else if (member.data && member.data.planConnections) {
planConnections = member.data.planConnections;
} else if (member.plans) {
planConnections = member.plans;
}
// If no plan connections, remove all plan-specific content
if (!planConnections || planConnections.length === 0) {
removeAllPlanContent();
return;
}
// Get the current plan ID from the most recent active plan
const currentPlanConnection = planConnections[0];
const currentPlanId = currentPlanConnection?.planId;
const currentPriceId = currentPlanConnection?.payment?.priceId;
// Collect all possible IDs to match against
const matchingIds = [];
if (currentPlanId) matchingIds.push(currentPlanId);
if (currentPriceId) matchingIds.push(currentPriceId);
if (matchingIds.length === 0) {
removeAllPlanContent();
return;
}
// Show/remove content based on plan/price IDs
showPlanSpecificContent(matchingIds);
} catch (error) {
console.error('MemberScript #188: Error showing/hiding plan content:', error);
// On error, remove all plan-specific content for security
removeAllPlanContent();
}
});
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackReady) {
resolve();
} else {
document.addEventListener('memberstack.ready', resolve);
// Fallback timeout
setTimeout(resolve, 2000);
}
});
}
function showPlanSpecificContent(matchingIds) {
// Find all elements with data-ms-code attributes (plan-specific content)
const allPlanElements = document.querySelectorAll('[data-ms-code]');
allPlanElements.forEach(element => {
const elementPlanId = element.getAttribute('data-ms-code');
// Check if element's plan ID matches any of the member's IDs (planId or priceId)
const shouldShow = matchingIds.includes(elementPlanId);
if (shouldShow) {
element.classList.remove('ms-plan-hidden');
element.classList.add('ms-plan-visible');
} else {
element.remove();
}
});
if (allPlanElements.length === 0) {
console.warn('MemberScript #188: No elements with data-ms-code attributes found. Add data-ms-code="PLAN_ID" to elements you want to show/hide based on plan.');
}
}
function removeAllPlanContent() {
// Completely remove all elements with data-ms-code attributes if no member is logged in
const allPlanElements = document.querySelectorAll('[data-ms-code]');
allPlanElements.forEach(element => {
element.remove();
});
}
})();
</script>

#187 - Change the button in a pricing table to "current plan"
Automatically highlight your member's current plan in pricing tables with visual indicators.
<!-- 💙 MEMBERSCRIPT #187 v0.1 💙 - SHOW MEMBER CURRENT PLAN BUTTON -->
<script>
(function() {
'use strict';
document.addEventListener("DOMContentLoaded", async function() {
try {
// Check if Memberstack is loaded
if (!window.$memberstackDom) {
console.error('MemberScript #187: Memberstack DOM package is not loaded.');
return;
}
const memberstack = window.$memberstackDom;
// Wait for Memberstack to be ready
await waitForMemberstack();
// Get current member
const { data: member } = await memberstack.getCurrentMember();
if (!member) return;
// Get the current plan connection
let planConnections = null;
if (member.planConnections) planConnections = member.planConnections;
else if (member.data && member.data.planConnections) planConnections = member.data.planConnections;
else if (member.plans) planConnections = member.plans;
if (!planConnections || planConnections.length === 0) return;
const planConnection = planConnections[0];
const currentPlanId = planConnection?.planId;
const currentPriceId = planConnection?.payment?.priceId;
if (!currentPlanId) return;
// Find all buttons with Memberstack plan attributes
const buttons = document.querySelectorAll(
'[data-ms-price\\:update], [data-ms-price\\:add]'
);
// Update matching button to say "Current Plan"
buttons.forEach(button => {
const planIdFromUpdate = button.getAttribute('data-ms-price:update');
const planIdFromAdd = button.getAttribute('data-ms-price:add');
const buttonPlanId = planIdFromUpdate || planIdFromAdd;
if (buttonPlanId === currentPriceId || buttonPlanId === currentPlanId) {
button.textContent = 'Current Plan';
if (button.disabled !== undefined) button.disabled = true;
button.classList.add('current-plan-button');
if (button.tagName.toLowerCase() === 'a') {
button.setAttribute('href', 'javascript:void(0)');
button.style.cursor = 'not-allowed';
button.style.opacity = '0.6';
button.setAttribute('onclick', 'return false;');
}
button.style.pointerEvents = 'none';
}
});
} catch (error) {
console.error('MemberScript #187: Error updating button:', error);
}
});
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackReady) resolve();
else {
document.addEventListener('memberstack.ready', resolve);
setTimeout(resolve, 2000);
}
});
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #187 v0.1 💙 - SHOW MEMBER CURRENT PLAN BUTTON -->
<script>
(function() {
'use strict';
document.addEventListener("DOMContentLoaded", async function() {
try {
// Check if Memberstack is loaded
if (!window.$memberstackDom) {
console.error('MemberScript #187: Memberstack DOM package is not loaded.');
return;
}
const memberstack = window.$memberstackDom;
// Wait for Memberstack to be ready
await waitForMemberstack();
// Get current member
const { data: member } = await memberstack.getCurrentMember();
if (!member) return;
// Get the current plan connection
let planConnections = null;
if (member.planConnections) planConnections = member.planConnections;
else if (member.data && member.data.planConnections) planConnections = member.data.planConnections;
else if (member.plans) planConnections = member.plans;
if (!planConnections || planConnections.length === 0) return;
const planConnection = planConnections[0];
const currentPlanId = planConnection?.planId;
const currentPriceId = planConnection?.payment?.priceId;
if (!currentPlanId) return;
// Find all buttons with Memberstack plan attributes
const buttons = document.querySelectorAll(
'[data-ms-price\\:update], [data-ms-price\\:add]'
);
// Update matching button to say "Current Plan"
buttons.forEach(button => {
const planIdFromUpdate = button.getAttribute('data-ms-price:update');
const planIdFromAdd = button.getAttribute('data-ms-price:add');
const buttonPlanId = planIdFromUpdate || planIdFromAdd;
if (buttonPlanId === currentPriceId || buttonPlanId === currentPlanId) {
button.textContent = 'Current Plan';
if (button.disabled !== undefined) button.disabled = true;
button.classList.add('current-plan-button');
if (button.tagName.toLowerCase() === 'a') {
button.setAttribute('href', 'javascript:void(0)');
button.style.cursor = 'not-allowed';
button.style.opacity = '0.6';
button.setAttribute('onclick', 'return false;');
}
button.style.pointerEvents = 'none';
}
});
} catch (error) {
console.error('MemberScript #187: Error updating button:', error);
}
});
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackReady) resolve();
else {
document.addEventListener('memberstack.ready', resolve);
setTimeout(resolve, 2000);
}
});
}
})();
</script>

#186 - Display a Members Current Plan/Price/Billing Interval
Display your member's current plan details including price, billing interval, and next billing date.
<!-- 💙 MEMBERSCRIPT #186 v0.1 💙 - DISPLAY A MEMBER’S CURRENT PLAN/PRICE/BILLING INTERVAL -->
<script>
(function() {
'use strict';
document.addEventListener("DOMContentLoaded", async function() {
try {
// Wait for Memberstack to be ready
await waitForMemberstack();
const memberstack = window.$memberstackDom;
const member = await memberstack.getCurrentMember();
// Check various possible locations for plan connections
let planConnections = null;
if (member && member.planConnections) {
planConnections = member.planConnections;
} else if (member && member.data && member.data.planConnections) {
planConnections = member.data.planConnections;
} else if (member && member.plans) {
planConnections = member.plans;
}
if (!planConnections || planConnections.length === 0) {
showNoPlanState();
return;
}
// Get the first active plan
const planConnection = planConnections[0];
const planId = planConnection.planId;
if (!planId) {
showNoPlanState();
return;
}
// Try to get the plan details
let plan = null;
try {
if (memberstack.getPlan) {
plan = await memberstack.getPlan({ planId });
}
} catch (e) {
// Plan details will be extracted from planConnection
}
// Display plan information
displayPlanInfo(plan, planConnection, member);
} catch (error) {
console.error("MemberScript #186: Error loading plan information:", error);
showError();
}
});
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackReady) {
resolve();
} else {
document.addEventListener('memberstack.ready', resolve);
// Fallback timeout
setTimeout(resolve, 2000);
}
});
}
function displayPlanInfo(plan, planConnection, member) {
// Hide loading and no-plan states, show plan container
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const planContainer = document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'none';
if (planContainer) planContainer.style.display = 'block';
// Update plan name
let planName = plan?.data?.name || plan?.data?.planName || plan?.data?.label || plan?.name || plan?.planName || plan?.label || planConnection.planId || 'Your Plan';
updateElement('[data-ms-code="plan-name"]', planName);
// Update plan price
let priceValue = null;
if (planConnection.payment && planConnection.payment.amount !== undefined) {
priceValue = planConnection.payment.amount;
} else if (plan?.data && plan.data.amount !== undefined) {
priceValue = plan.data.amount;
} else if (plan && plan.amount !== undefined) {
priceValue = plan.amount;
} else if (plan?.data && plan.data.price !== undefined) {
priceValue = plan.data.price / 100;
} else if (plan && plan.price !== undefined) {
priceValue = plan.price / 100;
}
if (priceValue !== null) {
const currency = planConnection.payment?.currency || plan?.data?.currency || plan?.currency || 'usd';
const symbol = currency === 'usd' ? '$' : currency.toUpperCase();
const formattedPrice = priceValue.toFixed(2);
updateElement('[data-ms-code="plan-price"]', `${symbol}${formattedPrice}`);
} else {
updateElement('[data-ms-code="plan-price"]', 'N/A');
}
// Update billing interval
if (planConnection.type) {
const type = planConnection.type.charAt(0).toUpperCase() + planConnection.type.slice(1).toLowerCase();
updateElement('[data-ms-code="plan-interval"]', type);
} else {
updateElement('[data-ms-code="plan-interval"]', 'N/A');
}
// Update status
const statusEl = document.querySelector('[data-ms-code="plan-status"]');
if (statusEl) {
const status = planConnection.status || 'Active';
statusEl.textContent = status;
if (status && (status.toLowerCase() === 'canceled' || status.toLowerCase() === 'cancelled')) {
statusEl.classList.add('cancelled');
}
}
// Update next billing date
let billingDate = planConnection.payment?.nextBillingDate;
if (billingDate) {
const date = new Date(billingDate < 10000000000 ? billingDate * 1000 : billingDate);
updateElement('[data-ms-code="plan-next-billing"]', formatDate(date));
} else {
updateElement('[data-ms-code="plan-next-billing"]', 'N/A');
}
}
function updateElement(selector, text) {
const el = document.querySelector(selector);
if (el) {
el.textContent = text;
}
}
function formatDate(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function showNoPlanState() {
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const planContainer = document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (planContainer) planContainer.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'block';
}
function showError() {
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
if (noPlanState) {
noPlanState.innerHTML = '<div class="empty-state"><div style="font-size: 3rem;">⚠️</div><h3>Error Loading Plan</h3><p>Unable to load your plan information. Please try again later.</p></div>';
noPlanState.style.display = 'block';
}
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #186 v0.1 💙 - DISPLAY A MEMBER’S CURRENT PLAN/PRICE/BILLING INTERVAL -->
<script>
(function() {
'use strict';
document.addEventListener("DOMContentLoaded", async function() {
try {
// Wait for Memberstack to be ready
await waitForMemberstack();
const memberstack = window.$memberstackDom;
const member = await memberstack.getCurrentMember();
// Check various possible locations for plan connections
let planConnections = null;
if (member && member.planConnections) {
planConnections = member.planConnections;
} else if (member && member.data && member.data.planConnections) {
planConnections = member.data.planConnections;
} else if (member && member.plans) {
planConnections = member.plans;
}
if (!planConnections || planConnections.length === 0) {
showNoPlanState();
return;
}
// Get the first active plan
const planConnection = planConnections[0];
const planId = planConnection.planId;
if (!planId) {
showNoPlanState();
return;
}
// Try to get the plan details
let plan = null;
try {
if (memberstack.getPlan) {
plan = await memberstack.getPlan({ planId });
}
} catch (e) {
// Plan details will be extracted from planConnection
}
// Display plan information
displayPlanInfo(plan, planConnection, member);
} catch (error) {
console.error("MemberScript #186: Error loading plan information:", error);
showError();
}
});
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackReady) {
resolve();
} else {
document.addEventListener('memberstack.ready', resolve);
// Fallback timeout
setTimeout(resolve, 2000);
}
});
}
function displayPlanInfo(plan, planConnection, member) {
// Hide loading and no-plan states, show plan container
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const planContainer = document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'none';
if (planContainer) planContainer.style.display = 'block';
// Update plan name
let planName = plan?.data?.name || plan?.data?.planName || plan?.data?.label || plan?.name || plan?.planName || plan?.label || planConnection.planId || 'Your Plan';
updateElement('[data-ms-code="plan-name"]', planName);
// Update plan price
let priceValue = null;
if (planConnection.payment && planConnection.payment.amount !== undefined) {
priceValue = planConnection.payment.amount;
} else if (plan?.data && plan.data.amount !== undefined) {
priceValue = plan.data.amount;
} else if (plan && plan.amount !== undefined) {
priceValue = plan.amount;
} else if (plan?.data && plan.data.price !== undefined) {
priceValue = plan.data.price / 100;
} else if (plan && plan.price !== undefined) {
priceValue = plan.price / 100;
}
if (priceValue !== null) {
const currency = planConnection.payment?.currency || plan?.data?.currency || plan?.currency || 'usd';
const symbol = currency === 'usd' ? '$' : currency.toUpperCase();
const formattedPrice = priceValue.toFixed(2);
updateElement('[data-ms-code="plan-price"]', `${symbol}${formattedPrice}`);
} else {
updateElement('[data-ms-code="plan-price"]', 'N/A');
}
// Update billing interval
if (planConnection.type) {
const type = planConnection.type.charAt(0).toUpperCase() + planConnection.type.slice(1).toLowerCase();
updateElement('[data-ms-code="plan-interval"]', type);
} else {
updateElement('[data-ms-code="plan-interval"]', 'N/A');
}
// Update status
const statusEl = document.querySelector('[data-ms-code="plan-status"]');
if (statusEl) {
const status = planConnection.status || 'Active';
statusEl.textContent = status;
if (status && (status.toLowerCase() === 'canceled' || status.toLowerCase() === 'cancelled')) {
statusEl.classList.add('cancelled');
}
}
// Update next billing date
let billingDate = planConnection.payment?.nextBillingDate;
if (billingDate) {
const date = new Date(billingDate < 10000000000 ? billingDate * 1000 : billingDate);
updateElement('[data-ms-code="plan-next-billing"]', formatDate(date));
} else {
updateElement('[data-ms-code="plan-next-billing"]', 'N/A');
}
}
function updateElement(selector, text) {
const el = document.querySelector(selector);
if (el) {
el.textContent = text;
}
}
function formatDate(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function showNoPlanState() {
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const planContainer = document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (planContainer) planContainer.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'block';
}
function showError() {
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
if (noPlanState) {
noPlanState.innerHTML = '<div class="empty-state"><div style="font-size: 3rem;">⚠️</div><h3>Error Loading Plan</h3><p>Unable to load your plan information. Please try again later.</p></div>';
noPlanState.style.display = 'block';
}
}
})();
</script>

#185 - Course Progress and Milestone Badge
Track course progress with encouraging messages, milestone badges, and real-time completion percentages.
Site Wide Footer Code
<!-- 💙 MEMBERSCRIPT #185 v1.0 - ENROLL / UNENROLL SYSTEM 💙 -->
<script>
document.addEventListener("DOMContentLoaded", async function () {
const memberstack = window.$memberstackDom;
let memberData = { coursesData: [] };
// ====== CONFIGURATION ======
const SUCCESS_REDIRECT = "/success"; // Optional redirect after enrolling
// ====== FETCH MEMBER JSON ======
async function fetchMemberData() {
try {
const member = await memberstack.getMemberJSON();
memberData = member.data || {};
if (!memberData.coursesData) memberData.coursesData = [];
} catch (error) {
console.error("Error fetching member data:", error);
}
}
// ====== SAVE MEMBER JSON ======
async function saveMemberData() {
try {
await memberstack.updateMemberJSON({ json: memberData });
} catch (error) {
console.error("Error saving member data:", error);
}
}
// ====== ENROLL / UNENROLL ======
async function handleEnrollment(courseSlug) {
const existing = memberData.coursesData.find(c => c.slug === courseSlug);
if (existing) {
// 🔸 Unenroll user
memberData.coursesData = memberData.coursesData.filter(c => c.slug !== courseSlug);
} else {
// 🔹 Enroll user
memberData.coursesData.push({
slug: courseSlug,
enrolledAt: new Date().toISOString()
});
if (SUCCESS_REDIRECT) window.location.href = SUCCESS_REDIRECT;
}
await saveMemberData();
updateEnrollUI();
}
// ====== UPDATE BUTTON UI ======
function updateEnrollUI() {
const buttons = document.querySelectorAll('[ms-code-enroll]');
buttons.forEach(btn => {
const slug = btn.getAttribute('ms-code-enroll');
const isEnrolled = memberData.coursesData.some(c => c.slug === slug);
btn.textContent = isEnrolled ? "Enrolled!" : "Enroll in course";
btn.classList.toggle("enrolled", isEnrolled);
});
const emptyMsg = document.querySelector('[ms-code-enrolled-empty]');
if (emptyMsg) {
const hasCourses = memberData.coursesData.length > 0;
emptyMsg.style.display = hasCourses ? "none" : "block";
}
}
// ====== INIT ======
await fetchMemberData();
updateEnrollUI();
document.addEventListener("click", async e => {
const btn = e.target.closest('[ms-code-enroll]');
if (!btn) return;
e.preventDefault();
const slug = btn.getAttribute('ms-code-enroll');
await handleEnrollment(slug);
});
});
</script>
Add This To Your Enrolled Courses Page
<!-- 💙 SHOW ENROLLED USER COURSES 💙 -->
<script>
(function () {
const memberstack = window.$memberstackDom;
let memberData = { coursesData: [] };
// ====== FETCH MEMBER DATA ======
async function fetchMemberData() {
try {
const member = await memberstack.getMemberJSON();
memberData = member.data || {};
if (!memberData.coursesData) memberData.coursesData = [];
} catch (error) {
console.error("Error fetching member data:", error);
}
}
// ====== FILTER & REORDER COLLECTION ======
function filterAndReorderCollectionItems() {
const list = document.querySelector('[ms-code-enrolled-list]');
if (!list) return;
const items = Array.from(list.querySelectorAll('[ms-code-item]'));
const enrolledSlugs = memberData.coursesData.map(c => c.slug);
const visibleItems = [];
items.forEach(item => {
const itemSlug = item.getAttribute('ms-code-item');
if (enrolledSlugs.includes(itemSlug)) {
item.style.display = ''; // show
visibleItems.push(item);
} else {
item.style.display = 'none'; // hide
}
});
// Re-append visible items to maintain sequence
visibleItems.forEach(item => list.appendChild(item));
}
// ====== HIDE COMPONENT IF EMPTY ======
function hideEnrolledCoursesComponent() {
const component = document.querySelector('.enrolled-courses-component');
if (!component) return;
const hasCourses = memberData.coursesData.length > 0;
component.style.display = hasCourses ? '' : 'none';
}
// ====== WAIT FOR CMS TO RENDER ======
function waitForCMS() {
return new Promise(resolve => {
const check = setInterval(() => {
if (document.querySelector('[ms-code-enrolled-list] [ms-code-item]')) {
clearInterval(check);
resolve();
}
}, 300);
});
}
// ====== INIT ======
document.addEventListener("DOMContentLoaded", async function () {
await fetchMemberData();
await waitForCMS();
filterAndReorderCollectionItems();
hideEnrolledCoursesComponent();
});
})();
</script>
Add This To Your Lessons Collection Page
<!-- 💙 MEMBERSCRIPT #185 v0.1 💙 COURSE PROGRESS + BADGE MILESTONES -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
// Initialize Memberstack
const memberstack = window.$memberstackDom;
let memberData;
try {
const member = await memberstack.getMemberJSON();
memberData = member.data ? member.data : {};
} catch (error) {
console.error("Error fetching member data:", error);
return;
}
// ===== USER CUSTOMIZATION SECTION =====
// Encouraging messages shown on completed lesson buttons
const encouragingMessages = [
"You're crushing this!",
"Way to go!",
"Fantastic progress!",
"Keep up the amazing work!",
"Awesome job!",
"You're on fire!"
];
// Random colors for completed lesson buttons
const buttonColors = [
"#d9e5ff",
"#cef5ca",
"#080331",
"#ffaefe",
"#dd23bb",
"#3c043b"
];
// ===== HELPER FUNCTIONS =====
function getRandomEncouragingMessage() {
return encouragingMessages[Math.floor(Math.random() * encouragingMessages.length)];
}
function getRandomColor() {
return buttonColors[Math.floor(Math.random() * buttonColors.length)];
}
function getTextColor(backgroundColor) {
const hex = backgroundColor.replace("#", "");
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 125 ? "black" : "white";
}
function syncCheckbox(element) {
const checkbox = element.querySelector('.chapter-menu_check');
if (checkbox) {
checkbox.classList.toggle('yes', element.classList.contains('yes'));
}
}
// ===== LESSON COMPLETION FUNCTIONS =====
function updatePageFromMemberJSON(memberData) {
document.querySelectorAll('[ms-code-mark-complete]').forEach(element => {
const lessonKey = element.getAttribute('ms-code-mark-complete');
const parts = lessonKey.split('-');
if (parts.length !== 3) return;
const [course, module, lesson] = parts;
// Find matching course with case-insensitive matching
const memberKeys = Object.keys(memberData || {});
const matchingCourseKey = memberKeys.find(key => key.toLowerCase() === course.toLowerCase());
const isComplete = matchingCourseKey &&
memberData[matchingCourseKey][module] &&
memberData[matchingCourseKey][module][lesson];
if (isComplete) {
element.classList.add("yes");
updateButtonStyling(element, true);
} else {
element.classList.remove("yes");
updateButtonStyling(element, false);
}
syncCheckbox(element);
});
}
function updateButtonStyling(element, isComplete) {
// Check if it's a button element (w-button class or contains 'lesson-button')
const isButton = element.tagName.toLowerCase() === 'a' &&
(element.classList.contains('button') ||
element.classList.contains('w-button') ||
element.classList.contains('lesson-button'));
if (isButton) {
if (isComplete) {
element.textContent = getRandomEncouragingMessage();
const bgColor = getRandomColor();
element.style.backgroundColor = bgColor;
element.style.color = getTextColor(bgColor);
element.classList.add('is-complete');
} else {
element.textContent = "Complete lesson";
element.style.backgroundColor = "";
element.style.color = "";
element.classList.remove('is-complete');
}
}
}
async function markLessonComplete(lessonKey, memberData) {
const [course, module, lesson] = lessonKey.split('-');
// Find matching course with case-insensitive matching
const memberKeys = Object.keys(memberData);
let matchingCourseKey = memberKeys.find(key => key.toLowerCase() === course.toLowerCase());
// If no match found, create new entry with lowercase course name
if (!matchingCourseKey) {
matchingCourseKey = course.toLowerCase();
memberData[matchingCourseKey] = {};
}
if (!memberData[matchingCourseKey][module]) memberData[matchingCourseKey][module] = {};
memberData[matchingCourseKey][module][lesson] = true;
await memberstack.updateMemberJSON({ json: memberData });
document.querySelectorAll(`[ms-code-mark-complete="${lessonKey}"]`).forEach(el => {
el.classList.add("yes");
updateButtonStyling(el, true);
});
updateBadgeProgress(matchingCourseKey, memberData);
}
async function markLessonIncomplete(lessonKey, memberData) {
const [course, module, lesson] = lessonKey.split('-');
// Find matching course with case-insensitive matching
const memberKeys = Object.keys(memberData);
const matchingCourseKey = memberKeys.find(key => key.toLowerCase() === course.toLowerCase());
if (matchingCourseKey && memberData[matchingCourseKey] &&
memberData[matchingCourseKey][module] &&
memberData[matchingCourseKey][module][lesson]) {
delete memberData[matchingCourseKey][module][lesson];
if (Object.keys(memberData[matchingCourseKey][module]).length === 0) {
delete memberData[matchingCourseKey][module];
}
if (Object.keys(memberData[matchingCourseKey]).length === 0) {
delete memberData[matchingCourseKey];
}
await memberstack.updateMemberJSON({ json: memberData });
}
document.querySelectorAll(`[ms-code-mark-complete="${lessonKey}"]`).forEach(el => {
el.classList.remove("yes");
updateButtonStyling(el, false);
});
updateBadgeProgress(matchingCourseKey || course, memberData);
}
// ===== EVENT HANDLERS =====
document.addEventListener("click", async function(event) {
const target = event.target;
const completeElement = target.closest('[ms-code-mark-complete]');
if (completeElement) {
event.preventDefault();
const lessonKey = completeElement.getAttribute('ms-code-mark-complete');
if (completeElement.classList.contains('yes')) {
await markLessonIncomplete(lessonKey, memberData);
} else {
await markLessonComplete(lessonKey, memberData);
}
}
});
const elements = document.querySelectorAll('[ms-code-mark-complete]');
const config = { attributes: true, attributeFilter: ['class'] };
const observer = new MutationObserver(function(mutationsList) {
mutationsList.forEach(mutation => {
syncCheckbox(mutation.target);
});
});
elements.forEach(el => observer.observe(el, config));
// ===== BADGE PROGRESS SYSTEM =====
function updateBadgeProgress(courseId, memberData) {
// Try both the original courseId and case variations to handle data inconsistencies
const lowerCourseId = courseId.toLowerCase();
const allLessonElements = document.querySelectorAll('[ms-code-mark-complete]');
const uniqueLessons = new Set();
allLessonElements.forEach(element => {
const lessonKey = element.getAttribute('ms-code-mark-complete');
if (lessonKey) {
const keyParts = lessonKey.split('-');
if (keyParts.length >= 1 && keyParts[0].toLowerCase() === lowerCourseId) {
uniqueLessons.add(lessonKey);
}
}
});
const totalLessons = uniqueLessons.size;
// Check memberData with case-insensitive matching
let completedLessons = 0;
const memberKeys = Object.keys(memberData || {});
const matchingCourseKey = memberKeys.find(key => key.toLowerCase() === lowerCourseId);
if (matchingCourseKey) {
const course = memberData[matchingCourseKey];
if (course && typeof course === 'object') {
Object.entries(course).forEach(([moduleKey, module]) => {
// Skip non-module data like 'coursesData'
if (module && typeof module === 'object' && !Array.isArray(module)) {
Object.values(module).forEach(isComplete => {
if (isComplete === true) {
completedLessons++;
}
});
}
});
}
}
const progress = totalLessons ? Math.round((completedLessons / totalLessons) * 100) : 0;
// Update badge text
const badgeText = document.querySelector('[data-ms-code="badge-text"]');
if (badgeText) {
if (progress === 0) {
badgeText.textContent = "Not started";
} else if (progress === 100) {
badgeText.textContent = "Course complete!";
} else {
badgeText.textContent = `${progress}% Complete`;
}
}
// Update progress bar with smooth animation
const progressBar = document.querySelector('[data-ms-code="progress-bar"]');
if (progressBar) {
progressBar.style.width = progress + "%";
// Add transition for smooth animation
progressBar.style.transition = "width 0.5s ease";
}
// Update progress text with lesson count
const progressText = document.querySelector('[data-ms-code="progress-text"]');
if (progressText) {
progressText.textContent = `${completedLessons} of ${totalLessons} lessons complete`;
}
// Handle badge status milestone messages
const completionBadge = document.querySelector('[data-ms-code="completion-badge"]');
if (completionBadge && progress >= 100) {
completionBadge.classList.add('unlocked');
} else if (completionBadge) {
completionBadge.classList.remove('unlocked');
}
}
// ===== INITIALIZATION =====
updatePageFromMemberJSON(memberData);
// Initialize badge progress for all courses
// Always detect courses from HTML to ensure all courses get their badges initialized
const allLessons = document.querySelectorAll('[ms-code-mark-complete]');
const detectedCourses = new Set();
allLessons.forEach(element => {
const lessonKey = element.getAttribute('ms-code-mark-complete');
if (lessonKey) {
const parts = lessonKey.split('-');
if (parts.length >= 1) {
detectedCourses.add(parts[0]);
}
}
});
// Update badge for all detected courses
detectedCourses.forEach(courseId => {
updateBadgeProgress(courseId, memberData);
});
});
</script>
Site Wide Footer Code
<!-- 💙 MEMBERSCRIPT #185 v1.0 - ENROLL / UNENROLL SYSTEM 💙 -->
<script>
document.addEventListener("DOMContentLoaded", async function () {
const memberstack = window.$memberstackDom;
let memberData = { coursesData: [] };
// ====== CONFIGURATION ======
const SUCCESS_REDIRECT = "/success"; // Optional redirect after enrolling
// ====== FETCH MEMBER JSON ======
async function fetchMemberData() {
try {
const member = await memberstack.getMemberJSON();
memberData = member.data || {};
if (!memberData.coursesData) memberData.coursesData = [];
} catch (error) {
console.error("Error fetching member data:", error);
}
}
// ====== SAVE MEMBER JSON ======
async function saveMemberData() {
try {
await memberstack.updateMemberJSON({ json: memberData });
} catch (error) {
console.error("Error saving member data:", error);
}
}
// ====== ENROLL / UNENROLL ======
async function handleEnrollment(courseSlug) {
const existing = memberData.coursesData.find(c => c.slug === courseSlug);
if (existing) {
// 🔸 Unenroll user
memberData.coursesData = memberData.coursesData.filter(c => c.slug !== courseSlug);
} else {
// 🔹 Enroll user
memberData.coursesData.push({
slug: courseSlug,
enrolledAt: new Date().toISOString()
});
if (SUCCESS_REDIRECT) window.location.href = SUCCESS_REDIRECT;
}
await saveMemberData();
updateEnrollUI();
}
// ====== UPDATE BUTTON UI ======
function updateEnrollUI() {
const buttons = document.querySelectorAll('[ms-code-enroll]');
buttons.forEach(btn => {
const slug = btn.getAttribute('ms-code-enroll');
const isEnrolled = memberData.coursesData.some(c => c.slug === slug);
btn.textContent = isEnrolled ? "Enrolled!" : "Enroll in course";
btn.classList.toggle("enrolled", isEnrolled);
});
const emptyMsg = document.querySelector('[ms-code-enrolled-empty]');
if (emptyMsg) {
const hasCourses = memberData.coursesData.length > 0;
emptyMsg.style.display = hasCourses ? "none" : "block";
}
}
// ====== INIT ======
await fetchMemberData();
updateEnrollUI();
document.addEventListener("click", async e => {
const btn = e.target.closest('[ms-code-enroll]');
if (!btn) return;
e.preventDefault();
const slug = btn.getAttribute('ms-code-enroll');
await handleEnrollment(slug);
});
});
</script>
Add This To Your Enrolled Courses Page
<!-- 💙 SHOW ENROLLED USER COURSES 💙 -->
<script>
(function () {
const memberstack = window.$memberstackDom;
let memberData = { coursesData: [] };
// ====== FETCH MEMBER DATA ======
async function fetchMemberData() {
try {
const member = await memberstack.getMemberJSON();
memberData = member.data || {};
if (!memberData.coursesData) memberData.coursesData = [];
} catch (error) {
console.error("Error fetching member data:", error);
}
}
// ====== FILTER & REORDER COLLECTION ======
function filterAndReorderCollectionItems() {
const list = document.querySelector('[ms-code-enrolled-list]');
if (!list) return;
const items = Array.from(list.querySelectorAll('[ms-code-item]'));
const enrolledSlugs = memberData.coursesData.map(c => c.slug);
const visibleItems = [];
items.forEach(item => {
const itemSlug = item.getAttribute('ms-code-item');
if (enrolledSlugs.includes(itemSlug)) {
item.style.display = ''; // show
visibleItems.push(item);
} else {
item.style.display = 'none'; // hide
}
});
// Re-append visible items to maintain sequence
visibleItems.forEach(item => list.appendChild(item));
}
// ====== HIDE COMPONENT IF EMPTY ======
function hideEnrolledCoursesComponent() {
const component = document.querySelector('.enrolled-courses-component');
if (!component) return;
const hasCourses = memberData.coursesData.length > 0;
component.style.display = hasCourses ? '' : 'none';
}
// ====== WAIT FOR CMS TO RENDER ======
function waitForCMS() {
return new Promise(resolve => {
const check = setInterval(() => {
if (document.querySelector('[ms-code-enrolled-list] [ms-code-item]')) {
clearInterval(check);
resolve();
}
}, 300);
});
}
// ====== INIT ======
document.addEventListener("DOMContentLoaded", async function () {
await fetchMemberData();
await waitForCMS();
filterAndReorderCollectionItems();
hideEnrolledCoursesComponent();
});
})();
</script>
Add This To Your Lessons Collection Page
<!-- 💙 MEMBERSCRIPT #185 v0.1 💙 COURSE PROGRESS + BADGE MILESTONES -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
// Initialize Memberstack
const memberstack = window.$memberstackDom;
let memberData;
try {
const member = await memberstack.getMemberJSON();
memberData = member.data ? member.data : {};
} catch (error) {
console.error("Error fetching member data:", error);
return;
}
// ===== USER CUSTOMIZATION SECTION =====
// Encouraging messages shown on completed lesson buttons
const encouragingMessages = [
"You're crushing this!",
"Way to go!",
"Fantastic progress!",
"Keep up the amazing work!",
"Awesome job!",
"You're on fire!"
];
// Random colors for completed lesson buttons
const buttonColors = [
"#d9e5ff",
"#cef5ca",
"#080331",
"#ffaefe",
"#dd23bb",
"#3c043b"
];
// ===== HELPER FUNCTIONS =====
function getRandomEncouragingMessage() {
return encouragingMessages[Math.floor(Math.random() * encouragingMessages.length)];
}
function getRandomColor() {
return buttonColors[Math.floor(Math.random() * buttonColors.length)];
}
function getTextColor(backgroundColor) {
const hex = backgroundColor.replace("#", "");
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 125 ? "black" : "white";
}
function syncCheckbox(element) {
const checkbox = element.querySelector('.chapter-menu_check');
if (checkbox) {
checkbox.classList.toggle('yes', element.classList.contains('yes'));
}
}
// ===== LESSON COMPLETION FUNCTIONS =====
function updatePageFromMemberJSON(memberData) {
document.querySelectorAll('[ms-code-mark-complete]').forEach(element => {
const lessonKey = element.getAttribute('ms-code-mark-complete');
const parts = lessonKey.split('-');
if (parts.length !== 3) return;
const [course, module, lesson] = parts;
// Find matching course with case-insensitive matching
const memberKeys = Object.keys(memberData || {});
const matchingCourseKey = memberKeys.find(key => key.toLowerCase() === course.toLowerCase());
const isComplete = matchingCourseKey &&
memberData[matchingCourseKey][module] &&
memberData[matchingCourseKey][module][lesson];
if (isComplete) {
element.classList.add("yes");
updateButtonStyling(element, true);
} else {
element.classList.remove("yes");
updateButtonStyling(element, false);
}
syncCheckbox(element);
});
}
function updateButtonStyling(element, isComplete) {
// Check if it's a button element (w-button class or contains 'lesson-button')
const isButton = element.tagName.toLowerCase() === 'a' &&
(element.classList.contains('button') ||
element.classList.contains('w-button') ||
element.classList.contains('lesson-button'));
if (isButton) {
if (isComplete) {
element.textContent = getRandomEncouragingMessage();
const bgColor = getRandomColor();
element.style.backgroundColor = bgColor;
element.style.color = getTextColor(bgColor);
element.classList.add('is-complete');
} else {
element.textContent = "Complete lesson";
element.style.backgroundColor = "";
element.style.color = "";
element.classList.remove('is-complete');
}
}
}
async function markLessonComplete(lessonKey, memberData) {
const [course, module, lesson] = lessonKey.split('-');
// Find matching course with case-insensitive matching
const memberKeys = Object.keys(memberData);
let matchingCourseKey = memberKeys.find(key => key.toLowerCase() === course.toLowerCase());
// If no match found, create new entry with lowercase course name
if (!matchingCourseKey) {
matchingCourseKey = course.toLowerCase();
memberData[matchingCourseKey] = {};
}
if (!memberData[matchingCourseKey][module]) memberData[matchingCourseKey][module] = {};
memberData[matchingCourseKey][module][lesson] = true;
await memberstack.updateMemberJSON({ json: memberData });
document.querySelectorAll(`[ms-code-mark-complete="${lessonKey}"]`).forEach(el => {
el.classList.add("yes");
updateButtonStyling(el, true);
});
updateBadgeProgress(matchingCourseKey, memberData);
}
async function markLessonIncomplete(lessonKey, memberData) {
const [course, module, lesson] = lessonKey.split('-');
// Find matching course with case-insensitive matching
const memberKeys = Object.keys(memberData);
const matchingCourseKey = memberKeys.find(key => key.toLowerCase() === course.toLowerCase());
if (matchingCourseKey && memberData[matchingCourseKey] &&
memberData[matchingCourseKey][module] &&
memberData[matchingCourseKey][module][lesson]) {
delete memberData[matchingCourseKey][module][lesson];
if (Object.keys(memberData[matchingCourseKey][module]).length === 0) {
delete memberData[matchingCourseKey][module];
}
if (Object.keys(memberData[matchingCourseKey]).length === 0) {
delete memberData[matchingCourseKey];
}
await memberstack.updateMemberJSON({ json: memberData });
}
document.querySelectorAll(`[ms-code-mark-complete="${lessonKey}"]`).forEach(el => {
el.classList.remove("yes");
updateButtonStyling(el, false);
});
updateBadgeProgress(matchingCourseKey || course, memberData);
}
// ===== EVENT HANDLERS =====
document.addEventListener("click", async function(event) {
const target = event.target;
const completeElement = target.closest('[ms-code-mark-complete]');
if (completeElement) {
event.preventDefault();
const lessonKey = completeElement.getAttribute('ms-code-mark-complete');
if (completeElement.classList.contains('yes')) {
await markLessonIncomplete(lessonKey, memberData);
} else {
await markLessonComplete(lessonKey, memberData);
}
}
});
const elements = document.querySelectorAll('[ms-code-mark-complete]');
const config = { attributes: true, attributeFilter: ['class'] };
const observer = new MutationObserver(function(mutationsList) {
mutationsList.forEach(mutation => {
syncCheckbox(mutation.target);
});
});
elements.forEach(el => observer.observe(el, config));
// ===== BADGE PROGRESS SYSTEM =====
function updateBadgeProgress(courseId, memberData) {
// Try both the original courseId and case variations to handle data inconsistencies
const lowerCourseId = courseId.toLowerCase();
const allLessonElements = document.querySelectorAll('[ms-code-mark-complete]');
const uniqueLessons = new Set();
allLessonElements.forEach(element => {
const lessonKey = element.getAttribute('ms-code-mark-complete');
if (lessonKey) {
const keyParts = lessonKey.split('-');
if (keyParts.length >= 1 && keyParts[0].toLowerCase() === lowerCourseId) {
uniqueLessons.add(lessonKey);
}
}
});
const totalLessons = uniqueLessons.size;
// Check memberData with case-insensitive matching
let completedLessons = 0;
const memberKeys = Object.keys(memberData || {});
const matchingCourseKey = memberKeys.find(key => key.toLowerCase() === lowerCourseId);
if (matchingCourseKey) {
const course = memberData[matchingCourseKey];
if (course && typeof course === 'object') {
Object.entries(course).forEach(([moduleKey, module]) => {
// Skip non-module data like 'coursesData'
if (module && typeof module === 'object' && !Array.isArray(module)) {
Object.values(module).forEach(isComplete => {
if (isComplete === true) {
completedLessons++;
}
});
}
});
}
}
const progress = totalLessons ? Math.round((completedLessons / totalLessons) * 100) : 0;
// Update badge text
const badgeText = document.querySelector('[data-ms-code="badge-text"]');
if (badgeText) {
if (progress === 0) {
badgeText.textContent = "Not started";
} else if (progress === 100) {
badgeText.textContent = "Course complete!";
} else {
badgeText.textContent = `${progress}% Complete`;
}
}
// Update progress bar with smooth animation
const progressBar = document.querySelector('[data-ms-code="progress-bar"]');
if (progressBar) {
progressBar.style.width = progress + "%";
// Add transition for smooth animation
progressBar.style.transition = "width 0.5s ease";
}
// Update progress text with lesson count
const progressText = document.querySelector('[data-ms-code="progress-text"]');
if (progressText) {
progressText.textContent = `${completedLessons} of ${totalLessons} lessons complete`;
}
// Handle badge status milestone messages
const completionBadge = document.querySelector('[data-ms-code="completion-badge"]');
if (completionBadge && progress >= 100) {
completionBadge.classList.add('unlocked');
} else if (completionBadge) {
completionBadge.classList.remove('unlocked');
}
}
// ===== INITIALIZATION =====
updatePageFromMemberJSON(memberData);
// Initialize badge progress for all courses
// Always detect courses from HTML to ensure all courses get their badges initialized
const allLessons = document.querySelectorAll('[ms-code-mark-complete]');
const detectedCourses = new Set();
allLessons.forEach(element => {
const lessonKey = element.getAttribute('ms-code-mark-complete');
if (lessonKey) {
const parts = lessonKey.split('-');
if (parts.length >= 1) {
detectedCourses.add(parts[0]);
}
}
});
// Update badge for all detected courses
detectedCourses.forEach(courseId => {
updateBadgeProgress(courseId, memberData);
});
});
</script>

#184 - Course Enrollment and Drip Content
Manage course enrollments and progressively unlock course content on a timed schedule.
Site Wide Footer Code
<!-- 💙 MEMBERSCRIPT #184 v1.0 - ENROLLMENT + DRIP CONTENT 💙 -->
<script>
document.addEventListener("DOMContentLoaded", async function () {
const memberstack = window.$memberstackDom;
let memberData = { coursesData: [] };
// ====== SECURITY MEASURES ======
// Anti-tampering protection
let securityViolations = 0;
const MAX_VIOLATIONS = 3;
// Detect developer tools
function detectDevTools() {
let devtools = false;
let consecutiveDetections = 0;
const threshold = 160;
const requiredConsecutive = 3; // Require 3 consecutive detections
setInterval(() => {
const heightDiff = window.outerHeight - window.innerHeight;
const widthDiff = window.outerWidth - window.innerWidth;
if (heightDiff > threshold || widthDiff > threshold) {
consecutiveDetections++;
// Only trigger if we have consecutive detections
if (consecutiveDetections >= requiredConsecutive) {
if (!devtools) {
devtools = true;
securityViolations++;
console.warn(`Security violation detected (${securityViolations}/${MAX_VIOLATIONS})`);
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
}
}
} else {
consecutiveDetections = 0;
devtools = false;
}
}, 1000); // Check every second instead of every 500ms
}
// Track user activity
let lastActivity = Date.now();
let userActive = true;
function isUserActive() {
const now = Date.now();
const timeSinceActivity = now - lastActivity;
// Consider user active if they've interacted within the last 5 minutes
return timeSinceActivity < 300000; // 5 minutes = 300,000ms
}
// Update activity on user interaction
function updateUserActivity() {
lastActivity = Date.now();
userActive = true;
}
// Listen for user activity
['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'].forEach(event => {
document.addEventListener(event, updateUserActivity, true);
});
// Handle security violations
function handleSecurityViolation() {
// Hide all drip content
const allContent = document.querySelectorAll('[data-ms-code="drip-item"]');
allContent.forEach(item => {
item.style.display = 'none';
});
// Show toast notification
showToast('Access restricted. Please refresh the page.', 'error');
}
// Toast notification function
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'error' ? '#dc3545' : '#007bff'};
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 10000;
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: 500;
max-width: 300px;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
`;
toast.textContent = message;
document.body.appendChild(toast);
// Animate in
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateX(0)';
}, 100);
// Auto remove after 5 seconds
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}, 5000);
}
// Disable right-click and keyboard shortcuts
function disableInspection() {
// Disable right-click site-wide
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
securityViolations++;
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
return false;
});
// Disable common dev tools shortcuts site-wide
document.addEventListener('keydown', function(e) {
if (e.keyCode === 123 || // F12
(e.ctrlKey && e.shiftKey && (e.keyCode === 73 || e.keyCode === 74)) || // Ctrl+Shift+I/J
(e.ctrlKey && e.keyCode === 85)) { // Ctrl+U
e.preventDefault();
securityViolations++;
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
return false;
}
});
}
// Obfuscate member data
function obfuscateMemberData() {
if (memberData && memberData.coursesData) {
// Add random noise to make tampering harder
memberData._securityHash = btoa(JSON.stringify(memberData.coursesData) + Date.now());
}
}
// Validate member data integrity
function validateMemberData() {
if (memberData && memberData._securityHash) {
const expectedHash = btoa(JSON.stringify(memberData.coursesData) + 'validated');
if (memberData._securityHash !== expectedHash) {
securityViolations++;
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
}
}
}
// Initialize security measures
detectDevTools();
disableInspection();
// ====== CONFIGURATION ======
// DRIP SETTINGS
// Set your preferred unlock interval here.
// Default: 7 days (1 week per lesson).
// Example: change to 3 for every 3 days, or 14 for every 2 weeks.
const DRIP_INTERVAL_DAYS = 7;
// OPTIONAL REDIRECT PAGE
// Where to send users after enrolling
const SUCCESS_REDIRECT = "/success";
// ====== FETCH MEMBER JSON ======
async function fetchMemberData() {
try {
const member = await memberstack.getMemberJSON();
memberData = member.data || {};
if (!memberData.coursesData) memberData.coursesData = [];
// Add security validation
obfuscateMemberData();
validateMemberData();
} catch (error) {
console.error("Error fetching member data:", error);
}
}
// ====== UPDATE MEMBER JSON ======
async function saveMemberData() {
try {
await memberstack.updateMemberJSON({ json: memberData });
} catch (error) {
console.error("Error saving member data:", error);
}
}
// ====== ENROLL / UNENROLL ======
async function handleEnrollment(courseSlug) {
const existing = memberData.coursesData.find(c => c.slug === courseSlug);
if (existing) {
// Optional: Unenroll user (toggle off)
memberData.coursesData = memberData.coursesData.filter(c => c.slug !== courseSlug);
} else {
// Enroll user and record timestamp
memberData.coursesData.push({
slug: courseSlug,
enrolledAt: new Date().toISOString()
});
window.location.href = SUCCESS_REDIRECT;
}
await saveMemberData();
updateEnrollUI();
}
// ====== ENROLL BUTTON UI ======
function updateEnrollUI() {
const buttons = document.querySelectorAll('[ms-code-enroll]');
buttons.forEach(btn => {
const slug = btn.getAttribute('ms-code-enroll');
const isEnrolled = memberData.coursesData.some(c => c.slug === slug);
btn.textContent = isEnrolled ? "Enrolled!" : "Enroll in course";
btn.classList.toggle("enrolled", isEnrolled);
});
const emptyMsg = document.querySelector('[ms-code-enrolled-empty]');
if (emptyMsg) {
const hasCourses = memberData.coursesData.length > 0;
emptyMsg.style.display = hasCourses ? "none" : "block";
}
}
// ====== DRIP UNLOCK LOGIC ======
async function waitForCMS() {
return new Promise(resolve => {
const check = setInterval(() => {
if (document.querySelector('[data-ms-code="drip-course"] [data-ms-code="drip-item"]')) {
clearInterval(check);
resolve();
}
}, 300);
});
}
function handleDripUnlock() {
// Security check before processing
if (securityViolations >= MAX_VIOLATIONS) {
return; // Don't process if security violations detected
}
const now = new Date();
const wrappers = document.querySelectorAll('[data-ms-code="drip-course"]');
wrappers.forEach(wrapper => {
const courseSlug = wrapper.getAttribute('data-course');
const course = memberData.coursesData.find(c => c.slug === courseSlug);
const items = wrapper.querySelectorAll('[data-ms-code="drip-item"]');
if (!course) {
// Not enrolled: lock everything
items.forEach(item => lockItem(item));
return;
}
const enrolledAt = new Date(course.enrolledAt);
const daysSince = Math.floor((now - enrolledAt) / (24 * 60 * 60 * 1000));
items.forEach(item => {
// Each drip item should have data-week OR data-delay (in days)
const week = parseInt(item.getAttribute('data-week'), 10);
const customDelay = parseInt(item.getAttribute('data-delay'), 10); // optional per-item override
const unlockAfterDays = customDelay || (week * DRIP_INTERVAL_DAYS);
if (daysSince >= unlockAfterDays) unlockItem(item);
else lockItem(item, enrolledAt, unlockAfterDays, now);
});
});
// Re-validate data after processing
validateMemberData();
}
// ====== HELPERS ======
function unlockItem(item) {
item.style.opacity = "1";
item.style.pointerEvents = "auto";
const overlay = item.querySelector('[data-ms-code="locked-overlay"]');
if (overlay) overlay.style.display = "none";
}
function lockItem(item, enrolledAt, unlockAfterDays, now) {
item.style.opacity = "0.6";
item.style.pointerEvents = "none";
const overlay = item.querySelector('[data-ms-code="locked-overlay"]');
if (overlay) {
overlay.style.display = "block";
const daysSpan = overlay.querySelector('[data-ms-code="days-remaining"]');
if (daysSpan && enrolledAt) {
const unlockDate = new Date(enrolledAt.getTime() + unlockAfterDays * 24 * 60 * 60 * 1000);
const daysLeft = Math.ceil((unlockDate - now) / (1000 * 60 * 60 * 24));
daysSpan.textContent = daysLeft > 0 ? daysLeft : 0;
}
}
}
// ====== INIT ======
await fetchMemberData();
updateEnrollUI();
document.addEventListener("click", async e => {
const btn = e.target.closest('[ms-code-enroll]');
if (!btn) return;
e.preventDefault();
const slug = btn.getAttribute('ms-code-enroll');
await handleEnrollment(slug);
});
await waitForCMS();
handleDripUnlock();
// Periodic security check
setInterval(() => {
validateMemberData();
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
}, 300000); // Check every 5 minutes
});
</script>
Add This To Your Enrolled Courses Page
<!-- 💙 SHOW ENROLLED USER COURSES 💙 -->
<script>
(function () {
const memberstack = window.$memberstackDom;
let memberData = { coursesData: [] };
// ====== FETCH MEMBER DATA ======
async function fetchMemberData() {
try {
const member = await memberstack.getMemberJSON();
memberData = member.data || {};
if (!memberData.coursesData) memberData.coursesData = [];
} catch (error) {
console.error("Error fetching member data:", error);
}
}
// ====== FILTER & REORDER COLLECTION ======
function filterAndReorderCollectionItems() {
const list = document.querySelector('[ms-code-enrolled-list]');
if (!list) return;
const items = Array.from(list.querySelectorAll('[ms-code-item]'));
const enrolledSlugs = memberData.coursesData.map(c => c.slug);
const visibleItems = [];
items.forEach(item => {
const itemSlug = item.getAttribute('ms-code-item');
if (enrolledSlugs.includes(itemSlug)) {
item.style.display = ''; // show
visibleItems.push(item);
} else {
item.style.display = 'none'; // hide
}
});
// Re-append visible items to maintain sequence
visibleItems.forEach(item => list.appendChild(item));
}
// ====== HIDE COMPONENT IF EMPTY ======
function hideEnrolledCoursesComponent() {
const component = document.querySelector('.enrolled-courses-component');
if (!component) return;
const hasCourses = memberData.coursesData.length > 0;
component.style.display = hasCourses ? '' : 'none';
}
// ====== WAIT FOR CMS TO RENDER ======
function waitForCMS() {
return new Promise(resolve => {
const check = setInterval(() => {
if (document.querySelector('[ms-code-enrolled-list] [ms-code-item]')) {
clearInterval(check);
resolve();
}
}, 300);
});
}
// ====== INIT ======
document.addEventListener("DOMContentLoaded", async function () {
await fetchMemberData();
await waitForCMS();
filterAndReorderCollectionItems();
hideEnrolledCoursesComponent();
});
})();
</script>
Site Wide Footer Code
<!-- 💙 MEMBERSCRIPT #184 v1.0 - ENROLLMENT + DRIP CONTENT 💙 -->
<script>
document.addEventListener("DOMContentLoaded", async function () {
const memberstack = window.$memberstackDom;
let memberData = { coursesData: [] };
// ====== SECURITY MEASURES ======
// Anti-tampering protection
let securityViolations = 0;
const MAX_VIOLATIONS = 3;
// Detect developer tools
function detectDevTools() {
let devtools = false;
let consecutiveDetections = 0;
const threshold = 160;
const requiredConsecutive = 3; // Require 3 consecutive detections
setInterval(() => {
const heightDiff = window.outerHeight - window.innerHeight;
const widthDiff = window.outerWidth - window.innerWidth;
if (heightDiff > threshold || widthDiff > threshold) {
consecutiveDetections++;
// Only trigger if we have consecutive detections
if (consecutiveDetections >= requiredConsecutive) {
if (!devtools) {
devtools = true;
securityViolations++;
console.warn(`Security violation detected (${securityViolations}/${MAX_VIOLATIONS})`);
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
}
}
} else {
consecutiveDetections = 0;
devtools = false;
}
}, 1000); // Check every second instead of every 500ms
}
// Track user activity
let lastActivity = Date.now();
let userActive = true;
function isUserActive() {
const now = Date.now();
const timeSinceActivity = now - lastActivity;
// Consider user active if they've interacted within the last 5 minutes
return timeSinceActivity < 300000; // 5 minutes = 300,000ms
}
// Update activity on user interaction
function updateUserActivity() {
lastActivity = Date.now();
userActive = true;
}
// Listen for user activity
['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'].forEach(event => {
document.addEventListener(event, updateUserActivity, true);
});
// Handle security violations
function handleSecurityViolation() {
// Hide all drip content
const allContent = document.querySelectorAll('[data-ms-code="drip-item"]');
allContent.forEach(item => {
item.style.display = 'none';
});
// Show toast notification
showToast('Access restricted. Please refresh the page.', 'error');
}
// Toast notification function
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'error' ? '#dc3545' : '#007bff'};
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 10000;
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: 500;
max-width: 300px;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
`;
toast.textContent = message;
document.body.appendChild(toast);
// Animate in
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateX(0)';
}, 100);
// Auto remove after 5 seconds
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}, 5000);
}
// Disable right-click and keyboard shortcuts
function disableInspection() {
// Disable right-click site-wide
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
securityViolations++;
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
return false;
});
// Disable common dev tools shortcuts site-wide
document.addEventListener('keydown', function(e) {
if (e.keyCode === 123 || // F12
(e.ctrlKey && e.shiftKey && (e.keyCode === 73 || e.keyCode === 74)) || // Ctrl+Shift+I/J
(e.ctrlKey && e.keyCode === 85)) { // Ctrl+U
e.preventDefault();
securityViolations++;
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
return false;
}
});
}
// Obfuscate member data
function obfuscateMemberData() {
if (memberData && memberData.coursesData) {
// Add random noise to make tampering harder
memberData._securityHash = btoa(JSON.stringify(memberData.coursesData) + Date.now());
}
}
// Validate member data integrity
function validateMemberData() {
if (memberData && memberData._securityHash) {
const expectedHash = btoa(JSON.stringify(memberData.coursesData) + 'validated');
if (memberData._securityHash !== expectedHash) {
securityViolations++;
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
}
}
}
// Initialize security measures
detectDevTools();
disableInspection();
// ====== CONFIGURATION ======
// DRIP SETTINGS
// Set your preferred unlock interval here.
// Default: 7 days (1 week per lesson).
// Example: change to 3 for every 3 days, or 14 for every 2 weeks.
const DRIP_INTERVAL_DAYS = 7;
// OPTIONAL REDIRECT PAGE
// Where to send users after enrolling
const SUCCESS_REDIRECT = "/success";
// ====== FETCH MEMBER JSON ======
async function fetchMemberData() {
try {
const member = await memberstack.getMemberJSON();
memberData = member.data || {};
if (!memberData.coursesData) memberData.coursesData = [];
// Add security validation
obfuscateMemberData();
validateMemberData();
} catch (error) {
console.error("Error fetching member data:", error);
}
}
// ====== UPDATE MEMBER JSON ======
async function saveMemberData() {
try {
await memberstack.updateMemberJSON({ json: memberData });
} catch (error) {
console.error("Error saving member data:", error);
}
}
// ====== ENROLL / UNENROLL ======
async function handleEnrollment(courseSlug) {
const existing = memberData.coursesData.find(c => c.slug === courseSlug);
if (existing) {
// Optional: Unenroll user (toggle off)
memberData.coursesData = memberData.coursesData.filter(c => c.slug !== courseSlug);
} else {
// Enroll user and record timestamp
memberData.coursesData.push({
slug: courseSlug,
enrolledAt: new Date().toISOString()
});
window.location.href = SUCCESS_REDIRECT;
}
await saveMemberData();
updateEnrollUI();
}
// ====== ENROLL BUTTON UI ======
function updateEnrollUI() {
const buttons = document.querySelectorAll('[ms-code-enroll]');
buttons.forEach(btn => {
const slug = btn.getAttribute('ms-code-enroll');
const isEnrolled = memberData.coursesData.some(c => c.slug === slug);
btn.textContent = isEnrolled ? "Enrolled!" : "Enroll in course";
btn.classList.toggle("enrolled", isEnrolled);
});
const emptyMsg = document.querySelector('[ms-code-enrolled-empty]');
if (emptyMsg) {
const hasCourses = memberData.coursesData.length > 0;
emptyMsg.style.display = hasCourses ? "none" : "block";
}
}
// ====== DRIP UNLOCK LOGIC ======
async function waitForCMS() {
return new Promise(resolve => {
const check = setInterval(() => {
if (document.querySelector('[data-ms-code="drip-course"] [data-ms-code="drip-item"]')) {
clearInterval(check);
resolve();
}
}, 300);
});
}
function handleDripUnlock() {
// Security check before processing
if (securityViolations >= MAX_VIOLATIONS) {
return; // Don't process if security violations detected
}
const now = new Date();
const wrappers = document.querySelectorAll('[data-ms-code="drip-course"]');
wrappers.forEach(wrapper => {
const courseSlug = wrapper.getAttribute('data-course');
const course = memberData.coursesData.find(c => c.slug === courseSlug);
const items = wrapper.querySelectorAll('[data-ms-code="drip-item"]');
if (!course) {
// Not enrolled: lock everything
items.forEach(item => lockItem(item));
return;
}
const enrolledAt = new Date(course.enrolledAt);
const daysSince = Math.floor((now - enrolledAt) / (24 * 60 * 60 * 1000));
items.forEach(item => {
// Each drip item should have data-week OR data-delay (in days)
const week = parseInt(item.getAttribute('data-week'), 10);
const customDelay = parseInt(item.getAttribute('data-delay'), 10); // optional per-item override
const unlockAfterDays = customDelay || (week * DRIP_INTERVAL_DAYS);
if (daysSince >= unlockAfterDays) unlockItem(item);
else lockItem(item, enrolledAt, unlockAfterDays, now);
});
});
// Re-validate data after processing
validateMemberData();
}
// ====== HELPERS ======
function unlockItem(item) {
item.style.opacity = "1";
item.style.pointerEvents = "auto";
const overlay = item.querySelector('[data-ms-code="locked-overlay"]');
if (overlay) overlay.style.display = "none";
}
function lockItem(item, enrolledAt, unlockAfterDays, now) {
item.style.opacity = "0.6";
item.style.pointerEvents = "none";
const overlay = item.querySelector('[data-ms-code="locked-overlay"]');
if (overlay) {
overlay.style.display = "block";
const daysSpan = overlay.querySelector('[data-ms-code="days-remaining"]');
if (daysSpan && enrolledAt) {
const unlockDate = new Date(enrolledAt.getTime() + unlockAfterDays * 24 * 60 * 60 * 1000);
const daysLeft = Math.ceil((unlockDate - now) / (1000 * 60 * 60 * 24));
daysSpan.textContent = daysLeft > 0 ? daysLeft : 0;
}
}
}
// ====== INIT ======
await fetchMemberData();
updateEnrollUI();
document.addEventListener("click", async e => {
const btn = e.target.closest('[ms-code-enroll]');
if (!btn) return;
e.preventDefault();
const slug = btn.getAttribute('ms-code-enroll');
await handleEnrollment(slug);
});
await waitForCMS();
handleDripUnlock();
// Periodic security check
setInterval(() => {
validateMemberData();
if (securityViolations >= MAX_VIOLATIONS) {
handleSecurityViolation();
}
}, 300000); // Check every 5 minutes
});
</script>
Add This To Your Enrolled Courses Page
<!-- 💙 SHOW ENROLLED USER COURSES 💙 -->
<script>
(function () {
const memberstack = window.$memberstackDom;
let memberData = { coursesData: [] };
// ====== FETCH MEMBER DATA ======
async function fetchMemberData() {
try {
const member = await memberstack.getMemberJSON();
memberData = member.data || {};
if (!memberData.coursesData) memberData.coursesData = [];
} catch (error) {
console.error("Error fetching member data:", error);
}
}
// ====== FILTER & REORDER COLLECTION ======
function filterAndReorderCollectionItems() {
const list = document.querySelector('[ms-code-enrolled-list]');
if (!list) return;
const items = Array.from(list.querySelectorAll('[ms-code-item]'));
const enrolledSlugs = memberData.coursesData.map(c => c.slug);
const visibleItems = [];
items.forEach(item => {
const itemSlug = item.getAttribute('ms-code-item');
if (enrolledSlugs.includes(itemSlug)) {
item.style.display = ''; // show
visibleItems.push(item);
} else {
item.style.display = 'none'; // hide
}
});
// Re-append visible items to maintain sequence
visibleItems.forEach(item => list.appendChild(item));
}
// ====== HIDE COMPONENT IF EMPTY ======
function hideEnrolledCoursesComponent() {
const component = document.querySelector('.enrolled-courses-component');
if (!component) return;
const hasCourses = memberData.coursesData.length > 0;
component.style.display = hasCourses ? '' : 'none';
}
// ====== WAIT FOR CMS TO RENDER ======
function waitForCMS() {
return new Promise(resolve => {
const check = setInterval(() => {
if (document.querySelector('[ms-code-enrolled-list] [ms-code-item]')) {
clearInterval(check);
resolve();
}
}, 300);
});
}
// ====== INIT ======
document.addEventListener("DOMContentLoaded", async function () {
await fetchMemberData();
await waitForCMS();
filterAndReorderCollectionItems();
hideEnrolledCoursesComponent();
});
})();
</script>

#183 - Member Milestone Badges
Automatically display milestone badges to members based on how long they’ve been active.
<!-- 💙 MEMBERSCRIPT #183 v0.1 💙 MEMBER MILESTONE BADGE -->
<script>
(function() {
'use strict';
// ====== CONFIGURATION - Customize your milestones here ======
const MILESTONE_CONFIG = {
milestones: [
{ days: 0, badge: 'Stackling', icon: '', message: 'Welcome, Stackling! Your journey begins.' },
{ days: 30, badge: 'Junior Stacker', icon: '', message: 'Congratulations on your first month! You\'re stacking up nicely.' },
{ days: 90, badge: 'Stacksmith', icon: '', message: 'You\'re becoming a regular! Your stacks are getting sharper.' },
{ days: 180, badge: 'Stackwright', icon: '', message: 'Half a year of awesomeness! You\'re crafting real magic.' },
{ days: 365, badge: 'Stackmaster', icon: '', message: 'One year strong! You\'ve mastered your stack.' },
{ days: 730, badge: 'Stack Sage', icon: '', message: 'Two years of loyalty — you\'ve reached legendary builder status.' },
{ days: 1095, badge: 'Grand Stacker', icon: '', message: 'Three years of greatness — you\'re a pillar of the Memberverse.' }
]
};
// ====== HELPER FUNCTIONS ======
// Wait for Memberstack to be ready
function waitForMemberstack(callback) {
if (window.$memberstackDom && window.$memberstackReady) {
callback();
} else {
document.addEventListener('memberstack.ready', callback);
}
}
// Calculate days between two dates
function calculateDaysBetween(startDate, endDate) {
const oneDay = 24 * 60 * 60 * 1000; // milliseconds in a day
return Math.floor(Math.abs((endDate - startDate) / oneDay));
}
// Find the highest milestone achieved
function getHighestMilestone(daysActive, milestones) {
let achievedMilestone = null;
for (const milestone of milestones) {
if (daysActive >= milestone.days) {
achievedMilestone = milestone;
}
}
return achievedMilestone;
}
// Display the milestone badge in your HTML using data attributes
function displayBadge(milestone, daysActive) {
const badgeElements = document.querySelectorAll('[data-ms-code="milestone-badge"]');
if (badgeElements.length === 0) {
console.warn('MemberScript #183: No elements found with data-ms-code="milestone-badge"');
return;
}
badgeElements.forEach(element => {
const daysElement = element.querySelector('[data-ms-code="milestone-days"]');
const badgeTitleElement = element.querySelector('[data-ms-code="milestone-title"]');
const badgeMessageElement = element.querySelector('[data-ms-code="milestone-message"]');
if (daysElement) daysElement.textContent = daysActive;
if (badgeTitleElement) badgeTitleElement.textContent = milestone.badge;
if (badgeMessageElement) badgeMessageElement.textContent = milestone.message;
// Set data attributes for styling and identification
element.setAttribute('data-milestone-badge', milestone.badge);
element.setAttribute('data-milestone-message', milestone.message || '');
element.setAttribute('data-milestone-days', milestone.days);
element.setAttribute('data-milestone-icon', milestone.icon || '');
element.setAttribute('data-milestone-type', milestone.badge.toLowerCase().replace(/\s+/g, '-'));
element.setAttribute('data-milestone-active', 'true'); // Used for CSS visibility
});
}
// ====== MAIN FUNCTION ======
async function checkAndDisplayMilestone() {
try {
// Ensure milestone elements exist before proceeding
await waitForBadgeElements();
const { data: member } = await window.$memberstackDom.getCurrentMember();
if (!member) return;
const signupDateString = member.createdAt || member.created_at;
if (!signupDateString) {
console.error('MemberScript #183: No signup date found in member object');
return;
}
const signupDate = new Date(signupDateString);
const currentDate = new Date();
const daysActive = calculateDaysBetween(signupDate, currentDate);
const milestone = getHighestMilestone(daysActive, MILESTONE_CONFIG.milestones);
if (!milestone) return;
displayBadge(milestone, daysActive);
} catch (error) {
console.error('MemberScript #183: Error checking milestones:', error);
}
}
// Wait for milestone DOM elements to be present
function waitForBadgeElements(timeoutMs = 5000) {
return new Promise((resolve) => {
const selector = '[data-ms-code="milestone-badge"]';
if (document.querySelector(selector)) return resolve();
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
// Fallback timeout
setTimeout(() => {
observer.disconnect();
resolve();
}, timeoutMs);
});
}
// ====== INITIALIZE ======
waitForMemberstack(checkAndDisplayMilestone);
})();
</script>
<!-- 💙 MEMBERSCRIPT #183 v0.1 💙 MEMBER MILESTONE BADGE -->
<script>
(function() {
'use strict';
// ====== CONFIGURATION - Customize your milestones here ======
const MILESTONE_CONFIG = {
milestones: [
{ days: 0, badge: 'Stackling', icon: '', message: 'Welcome, Stackling! Your journey begins.' },
{ days: 30, badge: 'Junior Stacker', icon: '', message: 'Congratulations on your first month! You\'re stacking up nicely.' },
{ days: 90, badge: 'Stacksmith', icon: '', message: 'You\'re becoming a regular! Your stacks are getting sharper.' },
{ days: 180, badge: 'Stackwright', icon: '', message: 'Half a year of awesomeness! You\'re crafting real magic.' },
{ days: 365, badge: 'Stackmaster', icon: '', message: 'One year strong! You\'ve mastered your stack.' },
{ days: 730, badge: 'Stack Sage', icon: '', message: 'Two years of loyalty — you\'ve reached legendary builder status.' },
{ days: 1095, badge: 'Grand Stacker', icon: '', message: 'Three years of greatness — you\'re a pillar of the Memberverse.' }
]
};
// ====== HELPER FUNCTIONS ======
// Wait for Memberstack to be ready
function waitForMemberstack(callback) {
if (window.$memberstackDom && window.$memberstackReady) {
callback();
} else {
document.addEventListener('memberstack.ready', callback);
}
}
// Calculate days between two dates
function calculateDaysBetween(startDate, endDate) {
const oneDay = 24 * 60 * 60 * 1000; // milliseconds in a day
return Math.floor(Math.abs((endDate - startDate) / oneDay));
}
// Find the highest milestone achieved
function getHighestMilestone(daysActive, milestones) {
let achievedMilestone = null;
for (const milestone of milestones) {
if (daysActive >= milestone.days) {
achievedMilestone = milestone;
}
}
return achievedMilestone;
}
// Display the milestone badge in your HTML using data attributes
function displayBadge(milestone, daysActive) {
const badgeElements = document.querySelectorAll('[data-ms-code="milestone-badge"]');
if (badgeElements.length === 0) {
console.warn('MemberScript #183: No elements found with data-ms-code="milestone-badge"');
return;
}
badgeElements.forEach(element => {
const daysElement = element.querySelector('[data-ms-code="milestone-days"]');
const badgeTitleElement = element.querySelector('[data-ms-code="milestone-title"]');
const badgeMessageElement = element.querySelector('[data-ms-code="milestone-message"]');
if (daysElement) daysElement.textContent = daysActive;
if (badgeTitleElement) badgeTitleElement.textContent = milestone.badge;
if (badgeMessageElement) badgeMessageElement.textContent = milestone.message;
// Set data attributes for styling and identification
element.setAttribute('data-milestone-badge', milestone.badge);
element.setAttribute('data-milestone-message', milestone.message || '');
element.setAttribute('data-milestone-days', milestone.days);
element.setAttribute('data-milestone-icon', milestone.icon || '');
element.setAttribute('data-milestone-type', milestone.badge.toLowerCase().replace(/\s+/g, '-'));
element.setAttribute('data-milestone-active', 'true'); // Used for CSS visibility
});
}
// ====== MAIN FUNCTION ======
async function checkAndDisplayMilestone() {
try {
// Ensure milestone elements exist before proceeding
await waitForBadgeElements();
const { data: member } = await window.$memberstackDom.getCurrentMember();
if (!member) return;
const signupDateString = member.createdAt || member.created_at;
if (!signupDateString) {
console.error('MemberScript #183: No signup date found in member object');
return;
}
const signupDate = new Date(signupDateString);
const currentDate = new Date();
const daysActive = calculateDaysBetween(signupDate, currentDate);
const milestone = getHighestMilestone(daysActive, MILESTONE_CONFIG.milestones);
if (!milestone) return;
displayBadge(milestone, daysActive);
} catch (error) {
console.error('MemberScript #183: Error checking milestones:', error);
}
}
// Wait for milestone DOM elements to be present
function waitForBadgeElements(timeoutMs = 5000) {
return new Promise((resolve) => {
const selector = '[data-ms-code="milestone-badge"]';
if (document.querySelector(selector)) return resolve();
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
// Fallback timeout
setTimeout(() => {
observer.disconnect();
resolve();
}, timeoutMs);
});
}
// ====== INITIALIZE ======
waitForMemberstack(checkAndDisplayMilestone);
})();
</script>

#182 - Disable Animations Using cookies
Instantly disable or enable all Webflow animations with a toggle, cookies, and reduced-motion support.
<!-- 💙 MEMBERSCRIPT #182 v.01 - DISABLE ANIMATIONS USING COOKIES & PREFERS-REDUCED-MOTION 💙 -->
<script>
// Run immediately to catch animations before they start
(function() {
console.log('Animation Disable Script loaded!');
// Configuration - Customize these values as needed
const config = {
// Cookie settings
cookieName: 'animationsDisabled',
cookieExpiryDays: 365, // How long to remember the preference
// Universal animation attribute - use this on ANY animated element
animationAttribute: 'data-ms-animate',
// Toggle control settings
showToggle: true, // Set to false to hide the toggle button
togglePosition: 'bottom-right', // 'top-right', 'bottom-right', 'top-left', 'bottom-left'
toggleText: {
disable: 'Disable Animations',
enable: 'Enable Animations'
}
};
// Cookie management functions
function setCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/; SameSite=Lax";
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function deleteCookie(name) {
document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
// Check if user prefers reduced motion
function prefersReducedMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Check if animations should be disabled
function shouldDisableAnimations() {
const cookieDisabled = getCookie(config.cookieName) === 'true';
const systemPrefersReduced = prefersReducedMotion();
console.log('Animation check:', {
cookieDisabled,
systemPrefersReduced,
shouldDisable: cookieDisabled || systemPrefersReduced
});
return cookieDisabled || systemPrefersReduced;
}
// Disable animations on page
function disableAnimations() {
console.log('Disabling animations...');
// Find all elements with the animation attribute
const animatedElements = document.querySelectorAll(`[${config.animationAttribute}]`);
console.log(`Found ${animatedElements.length} animated elements`);
animatedElements.forEach(element => {
// Remove Webflow animation ID to prevent interactions
const webflowId = element.getAttribute('data-w-id');
if (webflowId) {
element.setAttribute('data-w-id-disabled', webflowId);
element.removeAttribute('data-w-id');
console.log('Disabled Webflow animation for:', element);
}
// Mark as animation disabled
element.setAttribute('data-animation-disabled', 'true');
});
// Disable Webflow interactions globally
if (window.Webflow && window.Webflow.require) {
try {
const ix2 = window.Webflow.require('ix2');
if (ix2 && ix2.store) {
ix2.store.dispatch({ type: 'ix2/STORE_DISABLE' });
console.log('Disabled Webflow interactions globally');
}
} catch (e) {
console.log('Could not disable Webflow interactions:', e);
}
}
// Override Webflow animation styles
const style = document.createElement('style');
style.id = 'webflow-animation-disable';
style.textContent = `
[data-animation-disabled="true"] {
animation: none !important;
transition: none !important;
transform: none !important;
opacity: 1 !important;
visibility: visible !important;
}
`;
if (!document.getElementById('webflow-animation-disable')) {
document.head.appendChild(style);
}
console.log('Animations disabled successfully');
}
// Enable animations on page
function enableAnimations() {
console.log('Enabling animations...');
// Find all elements with the animation attribute
const animatedElements = document.querySelectorAll(`[${config.animationAttribute}]`);
animatedElements.forEach(element => {
if (element.getAttribute('data-animation-disabled') === 'true') {
// Restore Webflow animation ID
const disabledId = element.getAttribute('data-w-id-disabled');
if (disabledId) {
element.setAttribute('data-w-id', disabledId);
element.removeAttribute('data-w-id-disabled');
console.log('Re-enabled Webflow animation for:', element);
}
// Remove disabled marker
element.removeAttribute('data-animation-disabled');
}
});
// Re-enable Webflow interactions globally
if (window.Webflow && window.Webflow.require) {
try {
const ix2 = window.Webflow.require('ix2');
if (ix2 && ix2.store) {
ix2.store.dispatch({ type: 'ix2/STORE_ENABLE' });
console.log('Re-enabled Webflow interactions globally');
}
} catch (e) {
console.log('Could not re-enable Webflow interactions:', e);
}
}
// Remove override styles
const style = document.getElementById('webflow-animation-disable');
if (style) {
style.remove();
}
console.log('Animations enabled successfully');
}
// Create toggle button
function createToggleButton() {
if (!config.showToggle) return;
// Double check that body exists
if (!document.body) {
console.log('Body not ready, retrying toggle creation...');
setTimeout(createToggleButton, 100);
return;
}
//CUSTOMIZE THE TOGGLE COLORS
const toggle = document.createElement('button');
toggle.id = 'animation-toggle';
toggle.type = 'button';
toggle.setAttribute('data-ms-code', 'animation-toggle');
toggle.style.cssText = `
position: fixed;
${config.togglePosition.includes('top') ? 'top: 20px;' : 'bottom: 20px;'}
${config.togglePosition.includes('right') ? 'right: 20px;' : 'left: 20px;'}
z-index: 10000;
background: #2d62ff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 25px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
opacity: 0.8;
`;
// Add hover effects
toggle.addEventListener('mouseenter', function() {
this.style.opacity = '1';
this.style.transform = 'scale(1.05)';
});
toggle.addEventListener('mouseleave', function() {
this.style.opacity = '0.8';
this.style.transform = 'scale(1)';
});
// Add click handler
toggle.addEventListener('click', function() {
console.log('Toggle clicked!');
const currentlyDisabled = shouldDisableAnimations();
console.log('Currently disabled:', currentlyDisabled);
if (currentlyDisabled) {
// Enable animations
console.log('Enabling animations...');
deleteCookie(config.cookieName);
enableAnimations();
updateToggleButton(false);
showMessage('Animations enabled', 'success');
} else {
// Disable animations
console.log('Disabling animations...');
setCookie(config.cookieName, 'true', config.cookieExpiryDays);
disableAnimations();
updateToggleButton(true);
showMessage('Animations disabled', 'info');
}
});
document.body.appendChild(toggle);
updateToggleButton(shouldDisableAnimations());
console.log('Toggle button created');
}
// Update toggle button text and state
function updateToggleButton(disabled) {
const toggle = document.getElementById('animation-toggle');
if (!toggle) return;
toggle.textContent = disabled ? config.toggleText.enable : config.toggleText.disable;
toggle.style.background = disabled ? '#28a745' : '#2d62ff';
}
// Show message to user, CUSTOMIZE THIS
function showMessage(message, type = 'info') {
const messageDiv = document.createElement('div');
messageDiv.className = 'animation-message';
messageDiv.textContent = message;
const colors = {
success: '#28a745',
error: '#dc3545',
info: '#2d62ff',
warning: '#ffc107'
};
messageDiv.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: ${colors[type] || colors.info};
color: white;
padding: 12px 20px;
border-radius: 25px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 10001;
font-size: 14px;
font-weight: 500;
opacity: 0;
transition: opacity 0.3s ease;
`;
document.body.appendChild(messageDiv);
// Fade in
setTimeout(() => {
messageDiv.style.opacity = '1';
}, 100);
// Fade out and remove
setTimeout(() => {
messageDiv.style.opacity = '0';
setTimeout(() => {
if (document.body.contains(messageDiv)) {
document.body.removeChild(messageDiv);
}
}, 300);
}, 2000);
}
// Listen for system preference changes
function setupPreferenceListener() {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
function handlePreferenceChange(e) {
console.log('System preference changed:', e.matches);
if (e.matches && !getCookie(config.cookieName)) {
// User now prefers reduced motion and hasn't manually set a preference
disableAnimations();
updateToggleButton(true);
} else if (!e.matches && !getCookie(config.cookieName)) {
// User no longer prefers reduced motion and hasn't manually set a preference
enableAnimations();
updateToggleButton(false);
}
}
// Modern browsers
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handlePreferenceChange);
} else {
// Fallback for older browsers
mediaQuery.addListener(handlePreferenceChange);
}
}
// Initialize the script
function initialize() {
console.log('Initializing animation disable script...');
// Check if animations should be disabled
if (shouldDisableAnimations()) {
disableAnimations();
}
// Setup preference listener
setupPreferenceListener();
console.log('Animation disable script initialized successfully');
}
// Initialize animations immediately
initialize();
// Create toggle button when body is ready
function createToggleWhenReady() {
if (document.body) {
createToggleButton();
} else {
setTimeout(createToggleWhenReady, 10);
}
}
// Run when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
initialize();
createToggleWhenReady();
});
// Debug function
window.debugAnimationDisable = function() {
console.log('=== Animation Disable Debug Info ===');
console.log('Cookie value:', getCookie(config.cookieName));
console.log('Prefers reduced motion:', prefersReducedMotion());
console.log('Should disable animations:', shouldDisableAnimations());
console.log('Animation elements found:', document.querySelectorAll(`[${config.animationAttribute}]`).length);
console.log('Toggle button:', document.getElementById('animation-toggle'));
console.log('Config:', config);
};
})();
</script>
<!-- 💙 MEMBERSCRIPT #182 v.01 - DISABLE ANIMATIONS USING COOKIES & PREFERS-REDUCED-MOTION 💙 -->
<script>
// Run immediately to catch animations before they start
(function() {
console.log('Animation Disable Script loaded!');
// Configuration - Customize these values as needed
const config = {
// Cookie settings
cookieName: 'animationsDisabled',
cookieExpiryDays: 365, // How long to remember the preference
// Universal animation attribute - use this on ANY animated element
animationAttribute: 'data-ms-animate',
// Toggle control settings
showToggle: true, // Set to false to hide the toggle button
togglePosition: 'bottom-right', // 'top-right', 'bottom-right', 'top-left', 'bottom-left'
toggleText: {
disable: 'Disable Animations',
enable: 'Enable Animations'
}
};
// Cookie management functions
function setCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/; SameSite=Lax";
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function deleteCookie(name) {
document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
// Check if user prefers reduced motion
function prefersReducedMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Check if animations should be disabled
function shouldDisableAnimations() {
const cookieDisabled = getCookie(config.cookieName) === 'true';
const systemPrefersReduced = prefersReducedMotion();
console.log('Animation check:', {
cookieDisabled,
systemPrefersReduced,
shouldDisable: cookieDisabled || systemPrefersReduced
});
return cookieDisabled || systemPrefersReduced;
}
// Disable animations on page
function disableAnimations() {
console.log('Disabling animations...');
// Find all elements with the animation attribute
const animatedElements = document.querySelectorAll(`[${config.animationAttribute}]`);
console.log(`Found ${animatedElements.length} animated elements`);
animatedElements.forEach(element => {
// Remove Webflow animation ID to prevent interactions
const webflowId = element.getAttribute('data-w-id');
if (webflowId) {
element.setAttribute('data-w-id-disabled', webflowId);
element.removeAttribute('data-w-id');
console.log('Disabled Webflow animation for:', element);
}
// Mark as animation disabled
element.setAttribute('data-animation-disabled', 'true');
});
// Disable Webflow interactions globally
if (window.Webflow && window.Webflow.require) {
try {
const ix2 = window.Webflow.require('ix2');
if (ix2 && ix2.store) {
ix2.store.dispatch({ type: 'ix2/STORE_DISABLE' });
console.log('Disabled Webflow interactions globally');
}
} catch (e) {
console.log('Could not disable Webflow interactions:', e);
}
}
// Override Webflow animation styles
const style = document.createElement('style');
style.id = 'webflow-animation-disable';
style.textContent = `
[data-animation-disabled="true"] {
animation: none !important;
transition: none !important;
transform: none !important;
opacity: 1 !important;
visibility: visible !important;
}
`;
if (!document.getElementById('webflow-animation-disable')) {
document.head.appendChild(style);
}
console.log('Animations disabled successfully');
}
// Enable animations on page
function enableAnimations() {
console.log('Enabling animations...');
// Find all elements with the animation attribute
const animatedElements = document.querySelectorAll(`[${config.animationAttribute}]`);
animatedElements.forEach(element => {
if (element.getAttribute('data-animation-disabled') === 'true') {
// Restore Webflow animation ID
const disabledId = element.getAttribute('data-w-id-disabled');
if (disabledId) {
element.setAttribute('data-w-id', disabledId);
element.removeAttribute('data-w-id-disabled');
console.log('Re-enabled Webflow animation for:', element);
}
// Remove disabled marker
element.removeAttribute('data-animation-disabled');
}
});
// Re-enable Webflow interactions globally
if (window.Webflow && window.Webflow.require) {
try {
const ix2 = window.Webflow.require('ix2');
if (ix2 && ix2.store) {
ix2.store.dispatch({ type: 'ix2/STORE_ENABLE' });
console.log('Re-enabled Webflow interactions globally');
}
} catch (e) {
console.log('Could not re-enable Webflow interactions:', e);
}
}
// Remove override styles
const style = document.getElementById('webflow-animation-disable');
if (style) {
style.remove();
}
console.log('Animations enabled successfully');
}
// Create toggle button
function createToggleButton() {
if (!config.showToggle) return;
// Double check that body exists
if (!document.body) {
console.log('Body not ready, retrying toggle creation...');
setTimeout(createToggleButton, 100);
return;
}
//CUSTOMIZE THE TOGGLE COLORS
const toggle = document.createElement('button');
toggle.id = 'animation-toggle';
toggle.type = 'button';
toggle.setAttribute('data-ms-code', 'animation-toggle');
toggle.style.cssText = `
position: fixed;
${config.togglePosition.includes('top') ? 'top: 20px;' : 'bottom: 20px;'}
${config.togglePosition.includes('right') ? 'right: 20px;' : 'left: 20px;'}
z-index: 10000;
background: #2d62ff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 25px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
opacity: 0.8;
`;
// Add hover effects
toggle.addEventListener('mouseenter', function() {
this.style.opacity = '1';
this.style.transform = 'scale(1.05)';
});
toggle.addEventListener('mouseleave', function() {
this.style.opacity = '0.8';
this.style.transform = 'scale(1)';
});
// Add click handler
toggle.addEventListener('click', function() {
console.log('Toggle clicked!');
const currentlyDisabled = shouldDisableAnimations();
console.log('Currently disabled:', currentlyDisabled);
if (currentlyDisabled) {
// Enable animations
console.log('Enabling animations...');
deleteCookie(config.cookieName);
enableAnimations();
updateToggleButton(false);
showMessage('Animations enabled', 'success');
} else {
// Disable animations
console.log('Disabling animations...');
setCookie(config.cookieName, 'true', config.cookieExpiryDays);
disableAnimations();
updateToggleButton(true);
showMessage('Animations disabled', 'info');
}
});
document.body.appendChild(toggle);
updateToggleButton(shouldDisableAnimations());
console.log('Toggle button created');
}
// Update toggle button text and state
function updateToggleButton(disabled) {
const toggle = document.getElementById('animation-toggle');
if (!toggle) return;
toggle.textContent = disabled ? config.toggleText.enable : config.toggleText.disable;
toggle.style.background = disabled ? '#28a745' : '#2d62ff';
}
// Show message to user, CUSTOMIZE THIS
function showMessage(message, type = 'info') {
const messageDiv = document.createElement('div');
messageDiv.className = 'animation-message';
messageDiv.textContent = message;
const colors = {
success: '#28a745',
error: '#dc3545',
info: '#2d62ff',
warning: '#ffc107'
};
messageDiv.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: ${colors[type] || colors.info};
color: white;
padding: 12px 20px;
border-radius: 25px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 10001;
font-size: 14px;
font-weight: 500;
opacity: 0;
transition: opacity 0.3s ease;
`;
document.body.appendChild(messageDiv);
// Fade in
setTimeout(() => {
messageDiv.style.opacity = '1';
}, 100);
// Fade out and remove
setTimeout(() => {
messageDiv.style.opacity = '0';
setTimeout(() => {
if (document.body.contains(messageDiv)) {
document.body.removeChild(messageDiv);
}
}, 300);
}, 2000);
}
// Listen for system preference changes
function setupPreferenceListener() {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
function handlePreferenceChange(e) {
console.log('System preference changed:', e.matches);
if (e.matches && !getCookie(config.cookieName)) {
// User now prefers reduced motion and hasn't manually set a preference
disableAnimations();
updateToggleButton(true);
} else if (!e.matches && !getCookie(config.cookieName)) {
// User no longer prefers reduced motion and hasn't manually set a preference
enableAnimations();
updateToggleButton(false);
}
}
// Modern browsers
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handlePreferenceChange);
} else {
// Fallback for older browsers
mediaQuery.addListener(handlePreferenceChange);
}
}
// Initialize the script
function initialize() {
console.log('Initializing animation disable script...');
// Check if animations should be disabled
if (shouldDisableAnimations()) {
disableAnimations();
}
// Setup preference listener
setupPreferenceListener();
console.log('Animation disable script initialized successfully');
}
// Initialize animations immediately
initialize();
// Create toggle button when body is ready
function createToggleWhenReady() {
if (document.body) {
createToggleButton();
} else {
setTimeout(createToggleWhenReady, 10);
}
}
// Run when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
initialize();
createToggleWhenReady();
});
// Debug function
window.debugAnimationDisable = function() {
console.log('=== Animation Disable Debug Info ===');
console.log('Cookie value:', getCookie(config.cookieName));
console.log('Prefers reduced motion:', prefersReducedMotion());
console.log('Should disable animations:', shouldDisableAnimations());
console.log('Animation elements found:', document.querySelectorAll(`[${config.animationAttribute}]`).length);
console.log('Toggle button:', document.getElementById('animation-toggle'));
console.log('Config:', config);
};
})();
</script>

#181 - Dynamic Google Maps With CMS Location Pins
Display CMS locations on a dynamic Google Map with interactive pins and info windows.
<!-- 💙 MEMBERSCRIPT #181 v.01 - DYNAMIC GOOGLE MAPS WITH CMS LOCATION PINS 💙 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('Dynamic Google Maps with CMS data loaded!');
// Configuration - Customize these values as needed
const config = {
// Map settings
defaultZoom: 10,
defaultCenter: { lat: 40.7128, lng: -74.0060 }, // New York City as default
mapTypeId: 'roadmap', // roadmap, satellite, hybrid, terrain
mapId: "df5b64a914f0e2d26021bc7d", // Set to your Map ID for Advanced Markers (optional but recommended)
// Marker settings
markerIcon: null, // Set to custom icon URL if desired
markerAnimation: 'DROP', // DROP, BOUNCE, or null
// Info window settings
infoWindowMaxWidth: 300,
infoWindowPixelOffset: { width: 0, height: -30 },
// Data attributes
mapContainerAttr: 'map-container',
locationItemAttr: 'location-item',
locationNameAttr: 'location-name',
locationAddressAttr: 'location-address',
locationLatAttr: 'location-lat',
locationLngAttr: 'location-lng',
locationDescriptionAttr: 'location-description'
};
// Initialize the dynamic map
function initializeDynamicMap() {
const mapContainer = document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`);
if (!mapContainer) {
console.log('No map container found with data-ms-code="map-container"');
return;
}
console.log('Initializing dynamic map...');
// Set map container dimensions
mapContainer.style.width = config.mapWidth;
mapContainer.style.height = config.mapHeight;
mapContainer.style.borderRadius = '8px';
mapContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
// Load Google Maps API if not already loaded
if (typeof google === 'undefined' || !google.maps) {
loadGoogleMapsAPI();
} else {
createMap();
}
}
// Load Google Maps API
function loadGoogleMapsAPI() {
// Check if script is already being loaded
if (document.querySelector('script[src*="maps.googleapis.com"]')) {
console.log('Google Maps API already loading...');
return;
}
console.log('Loading Google Maps API...');
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${getGoogleMapsAPIKey()}&libraries=marker&loading=async&callback=initDynamicMap`;
script.async = true;
script.defer = true;
script.onerror = function() {
console.error('Failed to load Google Maps API');
showMapError('Failed to load Google Maps. Please check your API key.');
};
document.head.appendChild(script);
// Make initDynamicMap globally available
window.initDynamicMap = createMap;
}
// Get Google Maps API key from data attribute or environment
function getGoogleMapsAPIKey() {
const mapContainer = document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`);
const apiKey = mapContainer?.getAttribute('data-api-key') ||
document.querySelector('[data-ms-code="google-maps-api-key"]')?.value ||
'YOUR_GOOGLE_MAPS_API_KEY'; // Replace with your actual API key
if (apiKey === 'YOUR_GOOGLE_MAPS_API_KEY') {
console.warn('Please set your Google Maps API key in the map container or form field');
showMapError('Google Maps API key not configured. Please contact the administrator.');
}
return apiKey;
}
// Create the map with CMS data
function createMap() {
const mapContainer = document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`);
if (!mapContainer) {
console.error('Map container not found');
return;
}
console.log('Creating map with CMS data...');
// Get location data from CMS
const locations = getLocationDataFromCMS();
if (locations.length === 0) {
console.log('No location data found in CMS');
showMapError('No locations found. Please add location data to your CMS.');
return;
}
console.log(`Found ${locations.length} locations:`, locations);
// Calculate map center based on locations
const mapCenter = calculateMapCenter(locations);
// Initialize the map
const mapOptions = {
center: mapCenter,
zoom: config.defaultZoom,
mapTypeId: config.mapTypeId,
styles: getMapStyles(), // Custom map styling
gestureHandling: 'cooperative', // Require Ctrl+scroll to zoom
zoomControl: true,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
rotateControl: true,
fullscreenControl: true
};
// Add Map ID if provided (recommended for Advanced Markers)
if (config.mapId) {
mapOptions.mapId = config.mapId;
}
const map = new google.maps.Map(mapContainer, mapOptions);
// Create markers for each location
const markers = [];
const bounds = new google.maps.LatLngBounds();
locations.forEach((location, index) => {
const marker = createLocationMarker(map, location, index);
markers.push(marker);
// AdvancedMarkerElement uses position property instead of getPosition() method
bounds.extend(marker.position);
});
// Fit map to show all markers
if (locations.length > 1) {
map.fitBounds(bounds);
// Ensure minimum zoom level
google.maps.event.addListenerOnce(map, 'bounds_changed', function() {
if (map.getZoom() > 15) {
map.setZoom(15);
}
});
}
console.log('Map created successfully with', markers.length, 'markers');
}
// Get location data from CMS elements
function getLocationDataFromCMS() {
const locationItems = document.querySelectorAll(`[data-ms-code="${config.locationItemAttr}"]`);
const locations = [];
locationItems.forEach((item, index) => {
const name = getElementText(item, config.locationNameAttr);
const address = getElementText(item, config.locationAddressAttr);
const lat = parseFloat(getElementText(item, config.locationLatAttr));
const lng = parseFloat(getElementText(item, config.locationLngAttr));
const description = getElementText(item, config.locationDescriptionAttr);
// Validate coordinates
if (isNaN(lat) || isNaN(lng)) {
console.warn(`Invalid coordinates for location ${index + 1}:`, { lat, lng });
return;
}
// Validate coordinate ranges
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
console.warn(`Coordinates out of range for location ${index + 1}:`, { lat, lng });
return;
}
locations.push({
name: name || `Location ${index + 1}`,
address: address || '',
lat: lat,
lng: lng,
description: description || '',
index: index
});
});
return locations;
}
// Helper function to get text content from nested elements
function getElementText(parent, attribute) {
const element = parent.querySelector(`[data-ms-code="${attribute}"]`);
return element ? element.textContent.trim() : '';
}
// Calculate map center based on locations
function calculateMapCenter(locations) {
if (locations.length === 0) {
return config.defaultCenter;
}
if (locations.length === 1) {
return { lat: locations[0].lat, lng: locations[0].lng };
}
// Calculate average coordinates
const totalLat = locations.reduce((sum, loc) => sum + loc.lat, 0);
const totalLng = locations.reduce((sum, loc) => sum + loc.lng, 0);
return {
lat: totalLat / locations.length,
lng: totalLng / locations.length
};
}
// Create a marker for a location
function createLocationMarker(map, location, index) {
const position = { lat: location.lat, lng: location.lng };
// CHANGE MARKER CONTENT ELEMENT
const markerContent = document.createElement('div');
markerContent.style.cssText = `
width: 32px;
height: 32px;
background-color: #4285f4;
border: 2px solid #ffffff;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.2s ease;
`;
// Add custom icon if specified
if (config.markerIcon) {
markerContent.style.backgroundImage = `url(${config.markerIcon})`;
markerContent.style.backgroundSize = 'cover';
markerContent.style.backgroundPosition = 'center';
markerContent.style.backgroundColor = 'transparent';
markerContent.style.border = 'none';
} else {
// Add default pin icon
const pinIcon = document.createElement('div');
pinIcon.style.cssText = `
width: 8px;
height: 8px;
background-color: #ffffff;
border-radius: 50%;
`;
markerContent.appendChild(pinIcon);
}
// Add hover effect
markerContent.addEventListener('mouseenter', function() {
this.style.transform = 'scale(1.1)';
});
markerContent.addEventListener('mouseleave', function() {
this.style.transform = 'scale(1)';
});
// Create AdvancedMarkerElement
const marker = new google.maps.marker.AdvancedMarkerElement({
position: position,
map: map,
title: location.name,
content: markerContent
});
// Create info window content
const infoWindowContent = createInfoWindowContent(location);
// Add click event to marker
marker.addListener('click', function() {
// Close any existing info windows
closeAllInfoWindows();
// Create and open info window
const infoWindow = new google.maps.InfoWindow({
content: infoWindowContent,
maxWidth: config.infoWindowMaxWidth,
pixelOffset: config.infoWindowPixelOffset
});
infoWindow.open(map, marker);
// Store reference for closing
window.currentInfoWindow = infoWindow;
});
return marker;
}
// Create info window content
function createInfoWindowContent(location) {
let content = `<div style="padding: 10px; font-family: Arial, sans-serif;">`;
if (location.name) {
content += `<h3 style="margin: 0 0 8px 0; color: #333; font-size: 16px;">${escapeHtml(location.name)}</h3>`;
}
if (location.address) {
content += `<p style="margin: 0 0 8px 0; color: #666; font-size: 14px;">${escapeHtml(location.address)}</p>`;
}
if (location.description) {
content += `<p style="margin: 0; color: #555; font-size: 13px; line-height: 1.4;">${escapeHtml(location.description)}</p>`;
}
content += `</div>`;
return content;
}
// Close all info windows
function closeAllInfoWindows() {
if (window.currentInfoWindow) {
window.currentInfoWindow.close();
window.currentInfoWindow = null;
}
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Custom map styles (optional)
function getMapStyles() {
return [
{
featureType: 'poi',
elementType: 'labels',
stylers: [{ visibility: 'off' }]
},
{
featureType: 'transit',
elementType: 'labels',
stylers: [{ visibility: 'off' }]
}
];
}
// Show error message
function showMapError(message) {
const mapContainer = document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`);
if (mapContainer) {
mapContainer.innerHTML = `
<div style="
display: flex;
align-items: center;
justify-content: center;
height: ${config.mapHeight};
background: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 8px;
color: #6c757d;
font-family: Arial, sans-serif;
text-align: center;
padding: 20px;
">
<div>
<div style="font-size: 24px; margin-bottom: 10px;">🗺️</div>
<div style="font-size: 14px;">${message}</div>
</div>
</div>
`;
}
}
// Initialize the map
initializeDynamicMap();
// Debug function
window.debugDynamicMap = function() {
console.log('=== Dynamic Map Debug Info ===');
console.log('Map container:', document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`));
console.log('Location items:', document.querySelectorAll(`[data-ms-code="${config.locationItemAttr}"]`));
console.log('CMS locations:', getLocationDataFromCMS());
console.log('Google Maps loaded:', typeof google !== 'undefined' && !!google.maps);
console.log('Config:', config);
};
});
</script>
<!-- 💙 MEMBERSCRIPT #181 v.01 - DYNAMIC GOOGLE MAPS WITH CMS LOCATION PINS 💙 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('Dynamic Google Maps with CMS data loaded!');
// Configuration - Customize these values as needed
const config = {
// Map settings
defaultZoom: 10,
defaultCenter: { lat: 40.7128, lng: -74.0060 }, // New York City as default
mapTypeId: 'roadmap', // roadmap, satellite, hybrid, terrain
mapId: "df5b64a914f0e2d26021bc7d", // Set to your Map ID for Advanced Markers (optional but recommended)
// Marker settings
markerIcon: null, // Set to custom icon URL if desired
markerAnimation: 'DROP', // DROP, BOUNCE, or null
// Info window settings
infoWindowMaxWidth: 300,
infoWindowPixelOffset: { width: 0, height: -30 },
// Data attributes
mapContainerAttr: 'map-container',
locationItemAttr: 'location-item',
locationNameAttr: 'location-name',
locationAddressAttr: 'location-address',
locationLatAttr: 'location-lat',
locationLngAttr: 'location-lng',
locationDescriptionAttr: 'location-description'
};
// Initialize the dynamic map
function initializeDynamicMap() {
const mapContainer = document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`);
if (!mapContainer) {
console.log('No map container found with data-ms-code="map-container"');
return;
}
console.log('Initializing dynamic map...');
// Set map container dimensions
mapContainer.style.width = config.mapWidth;
mapContainer.style.height = config.mapHeight;
mapContainer.style.borderRadius = '8px';
mapContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
// Load Google Maps API if not already loaded
if (typeof google === 'undefined' || !google.maps) {
loadGoogleMapsAPI();
} else {
createMap();
}
}
// Load Google Maps API
function loadGoogleMapsAPI() {
// Check if script is already being loaded
if (document.querySelector('script[src*="maps.googleapis.com"]')) {
console.log('Google Maps API already loading...');
return;
}
console.log('Loading Google Maps API...');
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${getGoogleMapsAPIKey()}&libraries=marker&loading=async&callback=initDynamicMap`;
script.async = true;
script.defer = true;
script.onerror = function() {
console.error('Failed to load Google Maps API');
showMapError('Failed to load Google Maps. Please check your API key.');
};
document.head.appendChild(script);
// Make initDynamicMap globally available
window.initDynamicMap = createMap;
}
// Get Google Maps API key from data attribute or environment
function getGoogleMapsAPIKey() {
const mapContainer = document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`);
const apiKey = mapContainer?.getAttribute('data-api-key') ||
document.querySelector('[data-ms-code="google-maps-api-key"]')?.value ||
'YOUR_GOOGLE_MAPS_API_KEY'; // Replace with your actual API key
if (apiKey === 'YOUR_GOOGLE_MAPS_API_KEY') {
console.warn('Please set your Google Maps API key in the map container or form field');
showMapError('Google Maps API key not configured. Please contact the administrator.');
}
return apiKey;
}
// Create the map with CMS data
function createMap() {
const mapContainer = document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`);
if (!mapContainer) {
console.error('Map container not found');
return;
}
console.log('Creating map with CMS data...');
// Get location data from CMS
const locations = getLocationDataFromCMS();
if (locations.length === 0) {
console.log('No location data found in CMS');
showMapError('No locations found. Please add location data to your CMS.');
return;
}
console.log(`Found ${locations.length} locations:`, locations);
// Calculate map center based on locations
const mapCenter = calculateMapCenter(locations);
// Initialize the map
const mapOptions = {
center: mapCenter,
zoom: config.defaultZoom,
mapTypeId: config.mapTypeId,
styles: getMapStyles(), // Custom map styling
gestureHandling: 'cooperative', // Require Ctrl+scroll to zoom
zoomControl: true,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
rotateControl: true,
fullscreenControl: true
};
// Add Map ID if provided (recommended for Advanced Markers)
if (config.mapId) {
mapOptions.mapId = config.mapId;
}
const map = new google.maps.Map(mapContainer, mapOptions);
// Create markers for each location
const markers = [];
const bounds = new google.maps.LatLngBounds();
locations.forEach((location, index) => {
const marker = createLocationMarker(map, location, index);
markers.push(marker);
// AdvancedMarkerElement uses position property instead of getPosition() method
bounds.extend(marker.position);
});
// Fit map to show all markers
if (locations.length > 1) {
map.fitBounds(bounds);
// Ensure minimum zoom level
google.maps.event.addListenerOnce(map, 'bounds_changed', function() {
if (map.getZoom() > 15) {
map.setZoom(15);
}
});
}
console.log('Map created successfully with', markers.length, 'markers');
}
// Get location data from CMS elements
function getLocationDataFromCMS() {
const locationItems = document.querySelectorAll(`[data-ms-code="${config.locationItemAttr}"]`);
const locations = [];
locationItems.forEach((item, index) => {
const name = getElementText(item, config.locationNameAttr);
const address = getElementText(item, config.locationAddressAttr);
const lat = parseFloat(getElementText(item, config.locationLatAttr));
const lng = parseFloat(getElementText(item, config.locationLngAttr));
const description = getElementText(item, config.locationDescriptionAttr);
// Validate coordinates
if (isNaN(lat) || isNaN(lng)) {
console.warn(`Invalid coordinates for location ${index + 1}:`, { lat, lng });
return;
}
// Validate coordinate ranges
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
console.warn(`Coordinates out of range for location ${index + 1}:`, { lat, lng });
return;
}
locations.push({
name: name || `Location ${index + 1}`,
address: address || '',
lat: lat,
lng: lng,
description: description || '',
index: index
});
});
return locations;
}
// Helper function to get text content from nested elements
function getElementText(parent, attribute) {
const element = parent.querySelector(`[data-ms-code="${attribute}"]`);
return element ? element.textContent.trim() : '';
}
// Calculate map center based on locations
function calculateMapCenter(locations) {
if (locations.length === 0) {
return config.defaultCenter;
}
if (locations.length === 1) {
return { lat: locations[0].lat, lng: locations[0].lng };
}
// Calculate average coordinates
const totalLat = locations.reduce((sum, loc) => sum + loc.lat, 0);
const totalLng = locations.reduce((sum, loc) => sum + loc.lng, 0);
return {
lat: totalLat / locations.length,
lng: totalLng / locations.length
};
}
// Create a marker for a location
function createLocationMarker(map, location, index) {
const position = { lat: location.lat, lng: location.lng };
// CHANGE MARKER CONTENT ELEMENT
const markerContent = document.createElement('div');
markerContent.style.cssText = `
width: 32px;
height: 32px;
background-color: #4285f4;
border: 2px solid #ffffff;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.2s ease;
`;
// Add custom icon if specified
if (config.markerIcon) {
markerContent.style.backgroundImage = `url(${config.markerIcon})`;
markerContent.style.backgroundSize = 'cover';
markerContent.style.backgroundPosition = 'center';
markerContent.style.backgroundColor = 'transparent';
markerContent.style.border = 'none';
} else {
// Add default pin icon
const pinIcon = document.createElement('div');
pinIcon.style.cssText = `
width: 8px;
height: 8px;
background-color: #ffffff;
border-radius: 50%;
`;
markerContent.appendChild(pinIcon);
}
// Add hover effect
markerContent.addEventListener('mouseenter', function() {
this.style.transform = 'scale(1.1)';
});
markerContent.addEventListener('mouseleave', function() {
this.style.transform = 'scale(1)';
});
// Create AdvancedMarkerElement
const marker = new google.maps.marker.AdvancedMarkerElement({
position: position,
map: map,
title: location.name,
content: markerContent
});
// Create info window content
const infoWindowContent = createInfoWindowContent(location);
// Add click event to marker
marker.addListener('click', function() {
// Close any existing info windows
closeAllInfoWindows();
// Create and open info window
const infoWindow = new google.maps.InfoWindow({
content: infoWindowContent,
maxWidth: config.infoWindowMaxWidth,
pixelOffset: config.infoWindowPixelOffset
});
infoWindow.open(map, marker);
// Store reference for closing
window.currentInfoWindow = infoWindow;
});
return marker;
}
// Create info window content
function createInfoWindowContent(location) {
let content = `<div style="padding: 10px; font-family: Arial, sans-serif;">`;
if (location.name) {
content += `<h3 style="margin: 0 0 8px 0; color: #333; font-size: 16px;">${escapeHtml(location.name)}</h3>`;
}
if (location.address) {
content += `<p style="margin: 0 0 8px 0; color: #666; font-size: 14px;">${escapeHtml(location.address)}</p>`;
}
if (location.description) {
content += `<p style="margin: 0; color: #555; font-size: 13px; line-height: 1.4;">${escapeHtml(location.description)}</p>`;
}
content += `</div>`;
return content;
}
// Close all info windows
function closeAllInfoWindows() {
if (window.currentInfoWindow) {
window.currentInfoWindow.close();
window.currentInfoWindow = null;
}
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Custom map styles (optional)
function getMapStyles() {
return [
{
featureType: 'poi',
elementType: 'labels',
stylers: [{ visibility: 'off' }]
},
{
featureType: 'transit',
elementType: 'labels',
stylers: [{ visibility: 'off' }]
}
];
}
// Show error message
function showMapError(message) {
const mapContainer = document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`);
if (mapContainer) {
mapContainer.innerHTML = `
<div style="
display: flex;
align-items: center;
justify-content: center;
height: ${config.mapHeight};
background: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 8px;
color: #6c757d;
font-family: Arial, sans-serif;
text-align: center;
padding: 20px;
">
<div>
<div style="font-size: 24px; margin-bottom: 10px;">🗺️</div>
<div style="font-size: 14px;">${message}</div>
</div>
</div>
`;
}
}
// Initialize the map
initializeDynamicMap();
// Debug function
window.debugDynamicMap = function() {
console.log('=== Dynamic Map Debug Info ===');
console.log('Map container:', document.querySelector(`[data-ms-code="${config.mapContainerAttr}"]`));
console.log('Location items:', document.querySelectorAll(`[data-ms-code="${config.locationItemAttr}"]`));
console.log('CMS locations:', getLocationDataFromCMS());
console.log('Google Maps loaded:', typeof google !== 'undefined' && !!google.maps);
console.log('Config:', config);
};
});
</script>

#180 - Allow Members to Edit CMS Blog Content
Let members edit live CMS blog posts directly on your Webflow site.
Código de la cabeza
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #180 v.01 CSS FOR THE RICH TEXT FIELD 💙 -->
<style>
/* =========================================== */
/* MAIN EDITOR CONTAINER - Customize borders, colors, etc. */
/* =========================================== */
.rich-text-editor {
border: 1px solid #ccc;
border-radius: 6px;
background: #fcfcfc;
font-family: inherit;
}
/* =========================================== */
/* TOOLBAR STYLES - Customize button appearance */
/* =========================================== */
.toolbar {
display: flex;
gap: 5px;
padding: 10px;
background: #f9fafb;
border-bottom: 1px solid #d1d5db;
border-radius: 6px 6px 0 0;
flex-wrap: wrap;
}
/* =========================================== */
/* TOOLBAR BUTTONS - Customize button styling */
/* =========================================== */
.toolbar button {
padding: 6px 10px;
border: 1px solid #d1d5db;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.toolbar button:hover {
background: #f3f4f6;
}
.toolbar button.active {
background: #3b82f6;
color: white;
}
/* =========================================== */
/* EDITOR CONTENT AREA - Customize typing area */
/* =========================================== */
.editor-content {
padding: 15px;
min-height: 120px;
outline: none;
line-height: 1.6;
}
/* =========================================== */
/* CONTENT STYLING - Customize text formatting */
/* =========================================== */
.editor-content p {
margin: 0 0 10px 0;
}
.editor-content h1,
.editor-content h2,
.editor-content h3,
.editor-content h4,
.editor-content h5,
.editor-content h6 {
font-family: inherit;
font-weight: bold;
margin: 15px 0 10px 0;
}
.editor-content h1 { font-size: 1.8em; }
.editor-content h2 { font-size: 1.5em; }
.editor-content h3 { font-size: 1.3em; }
.editor-content ul,
.editor-content ol {
margin: 10px 0;
padding-left: 30px;
}
.editor-content a {
color: #3b82f6;
text-decoration: underline;
}
/* Link input overlay styles */
.link-input-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.link-input-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 300px;
}
.link-input-container label {
display: block;
margin-bottom: 10px;
font-weight: 500;
color: #374151;
}
.link-url-input {
width: 100%;
padding: 10px;
border: 2px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
margin-bottom: 15px;
box-sizing: border-box;
}
.link-url-input:focus {
outline: none;
border-color: #3b82f6;
}
.link-input-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.link-cancel-btn, .link-create-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.link-cancel-btn {
background: #f3f4f6;
color: #374151;
}
.link-cancel-btn:hover {
background: #e5e7eb;
}
.link-create-btn {
background: #3b82f6;
color: white;
}
.link-create-btn:hover {
background: #2563eb;
}
</style>
Código del cuerpo
Place this in your page <body>
<!-- 💙 MEMBERSCRIPT #180 v.01 ALLOW MEMBERS TO EDIT CMS BLOG CONTENT 💙 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('Blog editor with rich text loaded!');
// REPLACE THIS WEBHOOK LINK WITH YOUR OWN
const WEBHOOK_URL = 'https://hook.eu2.make.com/ycjsr8mhbqhim3ic85o6hxcj9t0kp999';
// Initialize Rich Text Editor
function initializeRichTextEditor() {
const contentTextarea = document.querySelector('[data-ms-code="blog-content"]');
if (!contentTextarea) return console.log('No content textarea found for rich text editor');
contentTextarea.style.display = 'none';
const editorContainer = document.createElement('div');
editorContainer.className = 'rich-text-editor';
const toolbar = document.createElement('div');
toolbar.className = 'toolbar';
toolbar.innerHTML = `
`;
const editorContent = document.createElement('div');
editorContent.className = 'editor-content';
editorContent.contentEditable = true;
editorContent.innerHTML = contentTextarea.value || '';
editorContainer.appendChild(toolbar);
editorContainer.appendChild(editorContent);
contentTextarea.parentNode.insertBefore(editorContainer, contentTextarea);
toolbar.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON') {
e.preventDefault();
const command = e.target.dataset.command;
const value = e.target.dataset.value;
if (command === 'createLink') handleLinkCreation();
else if (command === 'formatBlock') document.execCommand('formatBlock', false, value);
else document.execCommand(command, false, null);
updateToolbarStates();
updateHiddenField();
}
});
function handleLinkCreation() {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText) return showMessage('Please select text first, then click the link button', 'warning');
const range = selection.getRangeAt(0);
const linkInput = document.createElement('div');
linkInput.className = 'link-input-overlay';
linkInput.innerHTML = `
`;
document.body.appendChild(linkInput);
const urlInput = linkInput.querySelector('.link-url-input');
urlInput.focus();
linkInput.querySelector('.link-cancel-btn').addEventListener('click', () => document.body.removeChild(linkInput));
linkInput.querySelector('.link-create-btn').addEventListener('click', () => {
const url = urlInput.value.trim();
if (url) {
const fullUrl = url.startsWith('http') ? url : 'https://' + url;
const newSelection = window.getSelection();
newSelection.removeAllRanges();
newSelection.addRange(range);
editorContent.focus();
document.execCommand('createLink', false, fullUrl);
updateHiddenField();
}
document.body.removeChild(linkInput);
});
urlInput.addEventListener('keypress', e => { if (e.key === 'Enter') linkInput.querySelector('.link-create-btn').click(); });
urlInput.addEventListener('keydown', e => { if (e.key === 'Escape') linkInput.querySelector('.link-cancel-btn').click(); });
}
function updateToolbarStates() {
toolbar.querySelectorAll('button').forEach(button => {
const command = button.dataset.command;
const value = button.dataset.value;
if (command === 'formatBlock') {
button.classList.toggle('active', document.queryCommandValue('formatBlock') === value);
} else if (command !== 'createLink' && command !== 'removeFormat') {
button.classList.toggle('active', document.queryCommandState(command));
}
});
}
function updateHiddenField() { contentTextarea.value = editorContent.innerHTML; }
editorContent.addEventListener('input', updateHiddenField);
editorContent.addEventListener('mouseup', updateToolbarStates);
editorContent.addEventListener('keyup', updateToolbarStates);
updateHiddenField();
}
function initializeBlogEditor() {
let editForm = document.querySelector('[data-ms-code="edit-blog-form"]') || document.querySelector('form');
if (!editForm) return console.error('No form found on page');
const titleInput = editForm.querySelector('[data-ms-code="blog-title"]') || document.querySelector('[data-ms-code="blog-title"]');
const contentInput = editForm.querySelector('[data-ms-code="blog-content"]') || document.querySelector('[data-ms-code="blog-content"]');
const submitButton = editForm.querySelector('[data-ms-code="submit-edit"]') || editForm.querySelector('[type="submit"]') || editForm.querySelector('button');
if (!titleInput || !contentInput || !submitButton) return console.error('Required form elements not found');
editForm.setAttribute('data-wf-ignore', 'true');
editForm.removeAttribute('action');
const handleSubmit = async function(event) {
event.preventDefault();
const formData = {
title: titleInput.value.trim(),
content: contentInput.value.trim()
};
if (!validateFormData(titleInput, contentInput)) return false;
setLoadingState(submitButton, true);
try {
const response = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) showMessage('Blog post updated successfully!', 'success');
else throw new Error(`HTTP error! status: ${response.status}`);
} catch (error) {
console.error('Error updating blog post:', error);
showMessage('Failed to update blog post. Please try again.', 'error');
} finally {
setLoadingState(submitButton, false);
}
};
editForm.addEventListener('submit', handleSubmit);
submitButton.addEventListener('click', handleSubmit);
}
function validateFormData(titleInput, contentInput) {
if (!titleInput.value.trim()) { showMessage('Please enter a blog title.', 'error'); titleInput.focus(); return false; }
if (!contentInput.value.trim()) { showMessage('Please enter blog content.', 'error'); return false; }
return true;
}
function setLoadingState(button, isLoading) {
if (isLoading) { button.disabled = true; button.setAttribute('data-original-text', button.textContent); button.textContent = 'Updating...'; button.style.opacity = '0.7'; }
else { button.disabled = false; button.textContent = button.getAttribute('data-original-text') || 'Update Post'; button.style.opacity = '1'; }
}
function showMessage(message, type = 'info') {
const existingMessage = document.querySelector('.blog-message'); if (existingMessage) existingMessage.remove();
const messageDiv = document.createElement('div');
messageDiv.className = `blog-message blog-message-${type}`; messageDiv.textContent = message;
const colors = { success: '#10b981', error: '#ef4444', info: '#3b82f6', warning: '#f59e0b' };
messageDiv.style.cssText = `position: fixed; top: 20px; right: 20px; background: ${colors[type] || colors.info}; color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10000; font-size: 14px; font-weight: 500; max-width: 300px;`;
document.body.appendChild(messageDiv);
setTimeout(() => { if (document.body.contains(messageDiv)) messageDiv.remove(); }, 5000);
}
initializeRichTextEditor();
initializeBlogEditor();
});
</script>
Código de la cabeza
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #180 v.01 CSS FOR THE RICH TEXT FIELD 💙 -->
<style>
/* =========================================== */
/* MAIN EDITOR CONTAINER - Customize borders, colors, etc. */
/* =========================================== */
.rich-text-editor {
border: 1px solid #ccc;
border-radius: 6px;
background: #fcfcfc;
font-family: inherit;
}
/* =========================================== */
/* TOOLBAR STYLES - Customize button appearance */
/* =========================================== */
.toolbar {
display: flex;
gap: 5px;
padding: 10px;
background: #f9fafb;
border-bottom: 1px solid #d1d5db;
border-radius: 6px 6px 0 0;
flex-wrap: wrap;
}
/* =========================================== */
/* TOOLBAR BUTTONS - Customize button styling */
/* =========================================== */
.toolbar button {
padding: 6px 10px;
border: 1px solid #d1d5db;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.toolbar button:hover {
background: #f3f4f6;
}
.toolbar button.active {
background: #3b82f6;
color: white;
}
/* =========================================== */
/* EDITOR CONTENT AREA - Customize typing area */
/* =========================================== */
.editor-content {
padding: 15px;
min-height: 120px;
outline: none;
line-height: 1.6;
}
/* =========================================== */
/* CONTENT STYLING - Customize text formatting */
/* =========================================== */
.editor-content p {
margin: 0 0 10px 0;
}
.editor-content h1,
.editor-content h2,
.editor-content h3,
.editor-content h4,
.editor-content h5,
.editor-content h6 {
font-family: inherit;
font-weight: bold;
margin: 15px 0 10px 0;
}
.editor-content h1 { font-size: 1.8em; }
.editor-content h2 { font-size: 1.5em; }
.editor-content h3 { font-size: 1.3em; }
.editor-content ul,
.editor-content ol {
margin: 10px 0;
padding-left: 30px;
}
.editor-content a {
color: #3b82f6;
text-decoration: underline;
}
/* Link input overlay styles */
.link-input-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.link-input-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 300px;
}
.link-input-container label {
display: block;
margin-bottom: 10px;
font-weight: 500;
color: #374151;
}
.link-url-input {
width: 100%;
padding: 10px;
border: 2px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
margin-bottom: 15px;
box-sizing: border-box;
}
.link-url-input:focus {
outline: none;
border-color: #3b82f6;
}
.link-input-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.link-cancel-btn, .link-create-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.link-cancel-btn {
background: #f3f4f6;
color: #374151;
}
.link-cancel-btn:hover {
background: #e5e7eb;
}
.link-create-btn {
background: #3b82f6;
color: white;
}
.link-create-btn:hover {
background: #2563eb;
}
</style>
Código del cuerpo
Place this in your page <body>
<!-- 💙 MEMBERSCRIPT #180 v.01 ALLOW MEMBERS TO EDIT CMS BLOG CONTENT 💙 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('Blog editor with rich text loaded!');
// REPLACE THIS WEBHOOK LINK WITH YOUR OWN
const WEBHOOK_URL = 'https://hook.eu2.make.com/ycjsr8mhbqhim3ic85o6hxcj9t0kp999';
// Initialize Rich Text Editor
function initializeRichTextEditor() {
const contentTextarea = document.querySelector('[data-ms-code="blog-content"]');
if (!contentTextarea) return console.log('No content textarea found for rich text editor');
contentTextarea.style.display = 'none';
const editorContainer = document.createElement('div');
editorContainer.className = 'rich-text-editor';
const toolbar = document.createElement('div');
toolbar.className = 'toolbar';
toolbar.innerHTML = `
`;
const editorContent = document.createElement('div');
editorContent.className = 'editor-content';
editorContent.contentEditable = true;
editorContent.innerHTML = contentTextarea.value || '';
editorContainer.appendChild(toolbar);
editorContainer.appendChild(editorContent);
contentTextarea.parentNode.insertBefore(editorContainer, contentTextarea);
toolbar.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON') {
e.preventDefault();
const command = e.target.dataset.command;
const value = e.target.dataset.value;
if (command === 'createLink') handleLinkCreation();
else if (command === 'formatBlock') document.execCommand('formatBlock', false, value);
else document.execCommand(command, false, null);
updateToolbarStates();
updateHiddenField();
}
});
function handleLinkCreation() {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText) return showMessage('Please select text first, then click the link button', 'warning');
const range = selection.getRangeAt(0);
const linkInput = document.createElement('div');
linkInput.className = 'link-input-overlay';
linkInput.innerHTML = `
`;
document.body.appendChild(linkInput);
const urlInput = linkInput.querySelector('.link-url-input');
urlInput.focus();
linkInput.querySelector('.link-cancel-btn').addEventListener('click', () => document.body.removeChild(linkInput));
linkInput.querySelector('.link-create-btn').addEventListener('click', () => {
const url = urlInput.value.trim();
if (url) {
const fullUrl = url.startsWith('http') ? url : 'https://' + url;
const newSelection = window.getSelection();
newSelection.removeAllRanges();
newSelection.addRange(range);
editorContent.focus();
document.execCommand('createLink', false, fullUrl);
updateHiddenField();
}
document.body.removeChild(linkInput);
});
urlInput.addEventListener('keypress', e => { if (e.key === 'Enter') linkInput.querySelector('.link-create-btn').click(); });
urlInput.addEventListener('keydown', e => { if (e.key === 'Escape') linkInput.querySelector('.link-cancel-btn').click(); });
}
function updateToolbarStates() {
toolbar.querySelectorAll('button').forEach(button => {
const command = button.dataset.command;
const value = button.dataset.value;
if (command === 'formatBlock') {
button.classList.toggle('active', document.queryCommandValue('formatBlock') === value);
} else if (command !== 'createLink' && command !== 'removeFormat') {
button.classList.toggle('active', document.queryCommandState(command));
}
});
}
function updateHiddenField() { contentTextarea.value = editorContent.innerHTML; }
editorContent.addEventListener('input', updateHiddenField);
editorContent.addEventListener('mouseup', updateToolbarStates);
editorContent.addEventListener('keyup', updateToolbarStates);
updateHiddenField();
}
function initializeBlogEditor() {
let editForm = document.querySelector('[data-ms-code="edit-blog-form"]') || document.querySelector('form');
if (!editForm) return console.error('No form found on page');
const titleInput = editForm.querySelector('[data-ms-code="blog-title"]') || document.querySelector('[data-ms-code="blog-title"]');
const contentInput = editForm.querySelector('[data-ms-code="blog-content"]') || document.querySelector('[data-ms-code="blog-content"]');
const submitButton = editForm.querySelector('[data-ms-code="submit-edit"]') || editForm.querySelector('[type="submit"]') || editForm.querySelector('button');
if (!titleInput || !contentInput || !submitButton) return console.error('Required form elements not found');
editForm.setAttribute('data-wf-ignore', 'true');
editForm.removeAttribute('action');
const handleSubmit = async function(event) {
event.preventDefault();
const formData = {
title: titleInput.value.trim(),
content: contentInput.value.trim()
};
if (!validateFormData(titleInput, contentInput)) return false;
setLoadingState(submitButton, true);
try {
const response = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) showMessage('Blog post updated successfully!', 'success');
else throw new Error(`HTTP error! status: ${response.status}`);
} catch (error) {
console.error('Error updating blog post:', error);
showMessage('Failed to update blog post. Please try again.', 'error');
} finally {
setLoadingState(submitButton, false);
}
};
editForm.addEventListener('submit', handleSubmit);
submitButton.addEventListener('click', handleSubmit);
}
function validateFormData(titleInput, contentInput) {
if (!titleInput.value.trim()) { showMessage('Please enter a blog title.', 'error'); titleInput.focus(); return false; }
if (!contentInput.value.trim()) { showMessage('Please enter blog content.', 'error'); return false; }
return true;
}
function setLoadingState(button, isLoading) {
if (isLoading) { button.disabled = true; button.setAttribute('data-original-text', button.textContent); button.textContent = 'Updating...'; button.style.opacity = '0.7'; }
else { button.disabled = false; button.textContent = button.getAttribute('data-original-text') || 'Update Post'; button.style.opacity = '1'; }
}
function showMessage(message, type = 'info') {
const existingMessage = document.querySelector('.blog-message'); if (existingMessage) existingMessage.remove();
const messageDiv = document.createElement('div');
messageDiv.className = `blog-message blog-message-${type}`; messageDiv.textContent = message;
const colors = { success: '#10b981', error: '#ef4444', info: '#3b82f6', warning: '#f59e0b' };
messageDiv.style.cssText = `position: fixed; top: 20px; right: 20px; background: ${colors[type] || colors.info}; color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10000; font-size: 14px; font-weight: 500; max-width: 300px;`;
document.body.appendChild(messageDiv);
setTimeout(() => { if (document.body.contains(messageDiv)) messageDiv.remove(); }, 5000);
}
initializeRichTextEditor();
initializeBlogEditor();
});
</script>

#179 - Rich Text Fields For Webflow Forms
Add a simple rich text editor to Webflow forms so members can format text with headings, styles, and links.
Código de la cabeza
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #179 v0.1 RICH TEXT FIELDS FOR WEBFLOW FORMS 💙 -->
<style>
/* =========================================== */
/* MAIN EDITOR CONTAINER - Customize borders, colors, etc. */
/* =========================================== */
.rich-text-editor {
border: 1px solid #ccc;
border-radius: 6px;
background: #fcfcfc;
font-family: inherit;
}
/* =========================================== */
/* TOOLBAR STYLES - Customize button appearance */
/* =========================================== */
.toolbar {
display: flex;
gap: 5px;
padding: 10px;
background: #f9fafb;
border-bottom: 1px solid #d1d5db;
border-radius: 6px 6px 0 0;
flex-wrap: wrap;
}
/* =========================================== */
/* TOOLBAR BUTTONS - Customize button styling */
/* =========================================== */
.toolbar button {
padding: 6px 10px;
border: 1px solid #d1d5db;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.toolbar button:hover {
background: #f3f4f6;
}
.toolbar button.active {
background: #3b82f6;
color: white;
}
/* =========================================== */
/* EDITOR CONTENT AREA - Customize typing area */
/* =========================================== */
.editor-content {
padding: 15px;
min-height: 120px;
outline: none;
line-height: 1.6;
}
/* =========================================== */
/* CONTENT STYLING - Customize text formatting */
/* =========================================== */
.editor-content p {
margin: 0 0 10px 0;
}
.editor-content h1,
.editor-content h2,
.editor-content h3,
.editor-content h4,
.editor-content h5,
.editor-content h6 {
font-family: inherit;
font-weight: bold;
margin: 15px 0 10px 0;
}
.editor-content h1 { font-size: 1.8em; }
.editor-content h2 { font-size: 1.5em; }
.editor-content h3 { font-size: 1.3em; }
.editor-content ul,
.editor-content ol {
margin: 10px 0;
padding-left: 30px;
}
.editor-content a {
color: #3b82f6;
text-decoration: underline;
}
/* Link input overlay styles */
.link-input-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.link-input-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 300px;
}
.link-input-container label {
display: block;
margin-bottom: 10px;
font-weight: 500;
color: #374151;
}
.link-url-input {
width: 100%;
padding: 10px;
border: 2px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
margin-bottom: 15px;
box-sizing: border-box;
}
.link-url-input:focus {
outline: none;
border-color: #3b82f6;
}
.link-input-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.link-cancel-btn, .link-create-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.link-cancel-btn {
background: #f3f4f6;
color: #374151;
}
.link-cancel-btn:hover {
background: #e5e7eb;
}
.link-create-btn {
background: #3b82f6;
color: white;
}
.link-create-btn:hover {
background: #2563eb;
}
</style>
Código del cuerpo
Place this in your page <body>
<!-- 💙 MEMBERSCRIPT #179 v0.1 RICH TEXT FIELDS FOR WEBFLOW FORMS 💙 -->
<!--
===========================================
JAVASCRIPT FUNCTIONALITY - DO NOT MODIFY
===========================================
The script below handles all the rich text editor functionality.
Only modify if you know what you're doing.
-->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Find all rich text editors
const editors = document.querySelectorAll('[data-ms-code="rich-text-editor"]');
editors.forEach(function(textarea) {
// Hide original textarea
textarea.style.display = 'none';
// Create editor container
const editorContainer = document.createElement('div');
editorContainer.className = 'rich-text-editor';
// Create toolbar
const toolbar = document.createElement('div');
toolbar.className = 'toolbar';
toolbar.innerHTML = `
<button type="button" data-command="formatBlock" data-value="h1">H1</button>
<button type="button" data-command="formatBlock" data-value="h2">H2</button>
<button type="button" data-command="formatBlock" data-value="h3">H3</button>
<button type="button" data-command="formatBlock" data-value="p">P</button>
<button type="button" data-command="bold"><b>B</b></button>
<button type="button" data-command="italic"><i>I</i></button>
<button type="button" data-command="underline"><u>U</u></button>
<button type="button" data-command="insertUnorderedList">• List</button>
<button type="button" data-command="insertOrderedList">1. List</button>
<button type="button" data-command="createLink">Link</button>
<button type="button" data-command="removeFormat">Clear</button>
`;
// Create editable content area
const editorContent = document.createElement('div');
editorContent.className = 'editor-content';
editorContent.contentEditable = true;
editorContent.innerHTML = textarea.value || '';
// Assemble editor
editorContainer.appendChild(toolbar);
editorContainer.appendChild(editorContent);
textarea.parentNode.insertBefore(editorContainer, textarea);
// Handle toolbar buttons
toolbar.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON') {
e.preventDefault();
const command = e.target.dataset.command;
const value = e.target.dataset.value;
if (command === 'createLink') {
handleLinkCreation();
} else if (command === 'formatBlock') {
document.execCommand('formatBlock', false, value);
} else {
document.execCommand(command, false, null);
}
// Update button states
updateToolbarStates();
// Update hidden field
updateHiddenField();
}
});
// Handle link creation without popup
function handleLinkCreation() {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText) {
// Store the selection range to restore it later
const range = selection.getRangeAt(0);
// Create a temporary input field for URL entry
const linkInput = document.createElement('div');
linkInput.className = 'link-input-overlay';
linkInput.innerHTML = `
<div class="link-input-container">
<label>Enter URL for "${selectedText}":</label>
<input type="url" placeholder="https://example.com" class="link-url-input">
<div class="link-input-buttons">
<button type="button" class="link-cancel-btn">Cancel</button>
<button type="button" class="link-create-btn">Create Link</button>
</div>
</div>
`;
// Add styles for the input overlay
const overlayStyles = `...`; // (Styles unchanged, truncated here for brevity)
// Add styles if not already added
if (!document.querySelector('#link-input-styles')) {
const styleSheet = document.createElement('style');
styleSheet.id = 'link-input-styles';
styleSheet.textContent = overlayStyles;
document.head.appendChild(styleSheet);
}
// (Remaining logic unchanged — handles link creation, cancel, enter/escape keys, etc.)
}
}
// Update toolbar button states
function updateToolbarStates() { ... }
// Update hidden field with HTML content
function updateHiddenField() { ... }
// Handle content changes
editorContent.addEventListener('input', function() {
updateHiddenField();
});
// Handle selection changes for toolbar states
editorContent.addEventListener('mouseup', updateToolbarStates);
editorContent.addEventListener('keyup', updateToolbarStates);
// Initial update
updateHiddenField();
// Handle form submission
const form = document.querySelector('[data-ms-code="rich-text-form"]');
if (form) {
form.addEventListener('submit', function() {
updateHiddenField();
});
}
});
});
</script>
Código de la cabeza
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #179 v0.1 RICH TEXT FIELDS FOR WEBFLOW FORMS 💙 -->
<style>
/* =========================================== */
/* MAIN EDITOR CONTAINER - Customize borders, colors, etc. */
/* =========================================== */
.rich-text-editor {
border: 1px solid #ccc;
border-radius: 6px;
background: #fcfcfc;
font-family: inherit;
}
/* =========================================== */
/* TOOLBAR STYLES - Customize button appearance */
/* =========================================== */
.toolbar {
display: flex;
gap: 5px;
padding: 10px;
background: #f9fafb;
border-bottom: 1px solid #d1d5db;
border-radius: 6px 6px 0 0;
flex-wrap: wrap;
}
/* =========================================== */
/* TOOLBAR BUTTONS - Customize button styling */
/* =========================================== */
.toolbar button {
padding: 6px 10px;
border: 1px solid #d1d5db;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.toolbar button:hover {
background: #f3f4f6;
}
.toolbar button.active {
background: #3b82f6;
color: white;
}
/* =========================================== */
/* EDITOR CONTENT AREA - Customize typing area */
/* =========================================== */
.editor-content {
padding: 15px;
min-height: 120px;
outline: none;
line-height: 1.6;
}
/* =========================================== */
/* CONTENT STYLING - Customize text formatting */
/* =========================================== */
.editor-content p {
margin: 0 0 10px 0;
}
.editor-content h1,
.editor-content h2,
.editor-content h3,
.editor-content h4,
.editor-content h5,
.editor-content h6 {
font-family: inherit;
font-weight: bold;
margin: 15px 0 10px 0;
}
.editor-content h1 { font-size: 1.8em; }
.editor-content h2 { font-size: 1.5em; }
.editor-content h3 { font-size: 1.3em; }
.editor-content ul,
.editor-content ol {
margin: 10px 0;
padding-left: 30px;
}
.editor-content a {
color: #3b82f6;
text-decoration: underline;
}
/* Link input overlay styles */
.link-input-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.link-input-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 300px;
}
.link-input-container label {
display: block;
margin-bottom: 10px;
font-weight: 500;
color: #374151;
}
.link-url-input {
width: 100%;
padding: 10px;
border: 2px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
margin-bottom: 15px;
box-sizing: border-box;
}
.link-url-input:focus {
outline: none;
border-color: #3b82f6;
}
.link-input-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.link-cancel-btn, .link-create-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.link-cancel-btn {
background: #f3f4f6;
color: #374151;
}
.link-cancel-btn:hover {
background: #e5e7eb;
}
.link-create-btn {
background: #3b82f6;
color: white;
}
.link-create-btn:hover {
background: #2563eb;
}
</style>
Código del cuerpo
Place this in your page <body>
<!-- 💙 MEMBERSCRIPT #179 v0.1 RICH TEXT FIELDS FOR WEBFLOW FORMS 💙 -->
<!--
===========================================
JAVASCRIPT FUNCTIONALITY - DO NOT MODIFY
===========================================
The script below handles all the rich text editor functionality.
Only modify if you know what you're doing.
-->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Find all rich text editors
const editors = document.querySelectorAll('[data-ms-code="rich-text-editor"]');
editors.forEach(function(textarea) {
// Hide original textarea
textarea.style.display = 'none';
// Create editor container
const editorContainer = document.createElement('div');
editorContainer.className = 'rich-text-editor';
// Create toolbar
const toolbar = document.createElement('div');
toolbar.className = 'toolbar';
toolbar.innerHTML = `
<button type="button" data-command="formatBlock" data-value="h1">H1</button>
<button type="button" data-command="formatBlock" data-value="h2">H2</button>
<button type="button" data-command="formatBlock" data-value="h3">H3</button>
<button type="button" data-command="formatBlock" data-value="p">P</button>
<button type="button" data-command="bold"><b>B</b></button>
<button type="button" data-command="italic"><i>I</i></button>
<button type="button" data-command="underline"><u>U</u></button>
<button type="button" data-command="insertUnorderedList">• List</button>
<button type="button" data-command="insertOrderedList">1. List</button>
<button type="button" data-command="createLink">Link</button>
<button type="button" data-command="removeFormat">Clear</button>
`;
// Create editable content area
const editorContent = document.createElement('div');
editorContent.className = 'editor-content';
editorContent.contentEditable = true;
editorContent.innerHTML = textarea.value || '';
// Assemble editor
editorContainer.appendChild(toolbar);
editorContainer.appendChild(editorContent);
textarea.parentNode.insertBefore(editorContainer, textarea);
// Handle toolbar buttons
toolbar.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON') {
e.preventDefault();
const command = e.target.dataset.command;
const value = e.target.dataset.value;
if (command === 'createLink') {
handleLinkCreation();
} else if (command === 'formatBlock') {
document.execCommand('formatBlock', false, value);
} else {
document.execCommand(command, false, null);
}
// Update button states
updateToolbarStates();
// Update hidden field
updateHiddenField();
}
});
// Handle link creation without popup
function handleLinkCreation() {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText) {
// Store the selection range to restore it later
const range = selection.getRangeAt(0);
// Create a temporary input field for URL entry
const linkInput = document.createElement('div');
linkInput.className = 'link-input-overlay';
linkInput.innerHTML = `
<div class="link-input-container">
<label>Enter URL for "${selectedText}":</label>
<input type="url" placeholder="https://example.com" class="link-url-input">
<div class="link-input-buttons">
<button type="button" class="link-cancel-btn">Cancel</button>
<button type="button" class="link-create-btn">Create Link</button>
</div>
</div>
`;
// Add styles for the input overlay
const overlayStyles = `...`; // (Styles unchanged, truncated here for brevity)
// Add styles if not already added
if (!document.querySelector('#link-input-styles')) {
const styleSheet = document.createElement('style');
styleSheet.id = 'link-input-styles';
styleSheet.textContent = overlayStyles;
document.head.appendChild(styleSheet);
}
// (Remaining logic unchanged — handles link creation, cancel, enter/escape keys, etc.)
}
}
// Update toolbar button states
function updateToolbarStates() { ... }
// Update hidden field with HTML content
function updateHiddenField() { ... }
// Handle content changes
editorContent.addEventListener('input', function() {
updateHiddenField();
});
// Handle selection changes for toolbar states
editorContent.addEventListener('mouseup', updateToolbarStates);
editorContent.addEventListener('keyup', updateToolbarStates);
// Initial update
updateHiddenField();
// Handle form submission
const form = document.querySelector('[data-ms-code="rich-text-form"]');
if (form) {
form.addEventListener('submit', function() {
updateHiddenField();
});
}
});
});
</script>

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

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

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

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

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

#173 - CMS Chatbot Assistant
Build a Webflow chatbot that dynamically loads help articles from collection lists and updates in real time.
<!-- 💙 MEMBERSCRIPT #173 v0.1 💙 - CMS BASED CHATBOT ASSISTANT -->
<script>
(function() {
'use strict';
const CONFIG = {
primary: '#2d62ff', // CHANGE THIS
maxResults: 3, // CHANGE THIS
helpPath: '/post/' // CHANGE THIS
};
let kb = [], member = null, open = false, history = [];
let conversationContext = { lastQuery: '', topics: [] };
document.readyState === 'loading' ?
document.addEventListener('DOMContentLoaded', init) : init();
async function init() {
await loadMember();
createUI();
setupEvents();
await loadKB();
}
async function loadMember() {
try {
if (window.$memberstackDom) {
const { data } = await window.$memberstackDom.getCurrentMember();
member = data;
}
} catch {}
}
async function loadKB() {
const articles = document.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) return loadArticlesFromElements(articles);
if (!window.location.pathname.includes('/help')) {
return loadKnowledgeBaseFromHelpPage();
}
const wrappers = Array.from(document.querySelectorAll('[data-ms-code="kb-article"]'))
.map(el => el.parentElement)
.filter((v,i,a) => v && a.indexOf(v) === i);
if (wrappers.length === 0) return [];
const kbSet = new Set();
wrappers.forEach(wrapper => {
const observer = new MutationObserver(() => {
const articles = wrapper.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) {
Array.from(articles).forEach(el => {
const titleEl = el.querySelector('[data-ms-code="kb-title"]');
const contentEl = el.querySelector('[data-ms-code="kb-content"]');
const categoriesEl = el.querySelector('[data-ms-code="kb-categories"]');
const slugEl = el.querySelector('[data-ms-code="kb-slug"]');
const title = titleEl?.textContent?.trim() || '';
const content = contentEl?.textContent?.trim() || '';
const categoriesText = categoriesEl?.textContent?.trim() || '';
const slug = slugEl?.textContent?.trim() || `article-${kb.length}`;
if (!title || kbSet.has(title)) return;
const categories = categoriesText ? categoriesText.split(',').map(c => c.trim().toLowerCase()).filter(c => c) : [];
kb.push({ id: kb.length, title, content, slug, categories });
kbSet.add(title);
});
conversationContext.topics = kb.map(a => a.title);
updateWelcomeMessage(kb.length);
}
});
observer.observe(wrapper, { childList: true, subtree: true });
});
return kb;
}
async function loadKnowledgeBaseFromHelpPage() {
try {
const response = await fetch('/help');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const articles = doc.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) {
const kbData = loadArticlesFromElements(articles);
return kbData;
} else return [];
} catch {
return [];
}
}
function loadArticlesFromElements(articles) {
const uniqueArticles = new Map();
Array.from(articles).forEach((el, i) => {
const titleEl = el.querySelector('[data-ms-code="kb-title"]');
const contentEl = el.querySelector('[data-ms-code="kb-content"]');
const categoriesEl = el.querySelector('[data-ms-code="kb-categories"]');
const slugEl = el.querySelector('[data-ms-code="kb-slug"]');
const title = titleEl?.textContent?.trim() || '';
const content = contentEl?.textContent?.trim() || '';
const categoriesText = categoriesEl?.textContent?.trim() || '';
const slug = slugEl?.textContent?.trim() || `article-${i}`;
const categories = categoriesText ? categoriesText.split(',').map(c => c.trim().toLowerCase()).filter(c => c) : [];
if (uniqueArticles.has(title)) return;
uniqueArticles.set(title, {
id: uniqueArticles.size,
title,
content,
slug,
categories
});
});
kb = Array.from(uniqueArticles.values()).filter(a => a.title && a.content);
conversationContext.topics = kb.map(a => a.title);
updateWelcomeMessage(kb.length);
return kb;
}
function updateWelcomeMessage(articleCount) {
setTimeout(() => {
const messages = document.getElementById('ms-messages');
if (messages) {
const firstBubble = messages.querySelector('div');
if (firstBubble) {
firstBubble.innerHTML = `👋 Ask me anything! I can help with ${articleCount} topics.`;
}
}
}, 100);
}
function createUI() {
const trigger = document.createElement('div');
trigger.id = 'ms-chatbot';
trigger.innerHTML = `<div id="ms-chat-button" onclick="MemberscriptChat.toggle()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg></div>`;
const widget = document.createElement('div');
widget.id = 'ms-chat-window';
widget.innerHTML = `... (truncated for brevity) ...`; // Keep full inner HTML here as in original
document.body.appendChild(trigger);
document.body.appendChild(widget);
}
function setupEvents() {}
function toggle() { ... }
function close() { ... }
function send() { ... }
function search(query) { ... }
function generateFollowUpSuggestions(results) { ... }
function generateIntelligentFallback(query) { ... }
function addMsg(sender,text){ ... }
window.MemberscriptChat={
toggle,
close,
send,
ask: q => { document.getElementById('ms-input').value = q; send(); },
history: () => history,
reloadFromHelp: async () => {
const kbData = await loadKnowledgeBaseFromHelpPage();
if (kbData.length > 0) updateWelcomeMessage(kbData.length);
return kbData;
}
};
})();
</script>
<!-- 💙 MEMBERSCRIPT #173 v0.1 💙 - CMS BASED CHATBOT ASSISTANT -->
<script>
(function() {
'use strict';
const CONFIG = {
primary: '#2d62ff', // CHANGE THIS
maxResults: 3, // CHANGE THIS
helpPath: '/post/' // CHANGE THIS
};
let kb = [], member = null, open = false, history = [];
let conversationContext = { lastQuery: '', topics: [] };
document.readyState === 'loading' ?
document.addEventListener('DOMContentLoaded', init) : init();
async function init() {
await loadMember();
createUI();
setupEvents();
await loadKB();
}
async function loadMember() {
try {
if (window.$memberstackDom) {
const { data } = await window.$memberstackDom.getCurrentMember();
member = data;
}
} catch {}
}
async function loadKB() {
const articles = document.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) return loadArticlesFromElements(articles);
if (!window.location.pathname.includes('/help')) {
return loadKnowledgeBaseFromHelpPage();
}
const wrappers = Array.from(document.querySelectorAll('[data-ms-code="kb-article"]'))
.map(el => el.parentElement)
.filter((v,i,a) => v && a.indexOf(v) === i);
if (wrappers.length === 0) return [];
const kbSet = new Set();
wrappers.forEach(wrapper => {
const observer = new MutationObserver(() => {
const articles = wrapper.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) {
Array.from(articles).forEach(el => {
const titleEl = el.querySelector('[data-ms-code="kb-title"]');
const contentEl = el.querySelector('[data-ms-code="kb-content"]');
const categoriesEl = el.querySelector('[data-ms-code="kb-categories"]');
const slugEl = el.querySelector('[data-ms-code="kb-slug"]');
const title = titleEl?.textContent?.trim() || '';
const content = contentEl?.textContent?.trim() || '';
const categoriesText = categoriesEl?.textContent?.trim() || '';
const slug = slugEl?.textContent?.trim() || `article-${kb.length}`;
if (!title || kbSet.has(title)) return;
const categories = categoriesText ? categoriesText.split(',').map(c => c.trim().toLowerCase()).filter(c => c) : [];
kb.push({ id: kb.length, title, content, slug, categories });
kbSet.add(title);
});
conversationContext.topics = kb.map(a => a.title);
updateWelcomeMessage(kb.length);
}
});
observer.observe(wrapper, { childList: true, subtree: true });
});
return kb;
}
async function loadKnowledgeBaseFromHelpPage() {
try {
const response = await fetch('/help');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const articles = doc.querySelectorAll('[data-ms-code="kb-article"]');
if (articles.length > 0) {
const kbData = loadArticlesFromElements(articles);
return kbData;
} else return [];
} catch {
return [];
}
}
function loadArticlesFromElements(articles) {
const uniqueArticles = new Map();
Array.from(articles).forEach((el, i) => {
const titleEl = el.querySelector('[data-ms-code="kb-title"]');
const contentEl = el.querySelector('[data-ms-code="kb-content"]');
const categoriesEl = el.querySelector('[data-ms-code="kb-categories"]');
const slugEl = el.querySelector('[data-ms-code="kb-slug"]');
const title = titleEl?.textContent?.trim() || '';
const content = contentEl?.textContent?.trim() || '';
const categoriesText = categoriesEl?.textContent?.trim() || '';
const slug = slugEl?.textContent?.trim() || `article-${i}`;
const categories = categoriesText ? categoriesText.split(',').map(c => c.trim().toLowerCase()).filter(c => c) : [];
if (uniqueArticles.has(title)) return;
uniqueArticles.set(title, {
id: uniqueArticles.size,
title,
content,
slug,
categories
});
});
kb = Array.from(uniqueArticles.values()).filter(a => a.title && a.content);
conversationContext.topics = kb.map(a => a.title);
updateWelcomeMessage(kb.length);
return kb;
}
function updateWelcomeMessage(articleCount) {
setTimeout(() => {
const messages = document.getElementById('ms-messages');
if (messages) {
const firstBubble = messages.querySelector('div');
if (firstBubble) {
firstBubble.innerHTML = `👋 Ask me anything! I can help with ${articleCount} topics.`;
}
}
}, 100);
}
function createUI() {
const trigger = document.createElement('div');
trigger.id = 'ms-chatbot';
trigger.innerHTML = `<div id="ms-chat-button" onclick="MemberscriptChat.toggle()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg></div>`;
const widget = document.createElement('div');
widget.id = 'ms-chat-window';
widget.innerHTML = `... (truncated for brevity) ...`; // Keep full inner HTML here as in original
document.body.appendChild(trigger);
document.body.appendChild(widget);
}
function setupEvents() {}
function toggle() { ... }
function close() { ... }
function send() { ... }
function search(query) { ... }
function generateFollowUpSuggestions(results) { ... }
function generateIntelligentFallback(query) { ... }
function addMsg(sender,text){ ... }
window.MemberscriptChat={
toggle,
close,
send,
ask: q => { document.getElementById('ms-input').value = q; send(); },
history: () => history,
reloadFromHelp: async () => {
const kbData = await loadKnowledgeBaseFromHelpPage();
if (kbData.length > 0) updateWelcomeMessage(kbData.length);
return kbData;
}
};
})();
</script>
¿Necesitas ayuda con MemberScripts? ¡Únete a nuestra comunidad Slack de más de 5.500 miembros! 🙌
Los MemberScripts son un recurso comunitario de Memberstack - si necesitas ayuda para que funcionen con tu proyecto, ¡únete al Slack de Memberstack 2.0 y pide ayuda!
Únete a nuestro SlackExplore empresas reales que han tenido éxito con Memberstack
No se fíe sólo de nuestra palabra: eche un vistazo a las empresas de todos los tamaños que confían en Memberstack para su autenticación y sus pagos.
Empieza a construir tus sueños
Memberstack es 100% gratis hasta que estés listo para lanzarla - así que, ¿a qué estás esperando? Crea tu primera aplicación y empieza a construir hoy mismo.




.png)