Komponenten
Schablonen
Attribute
Integrationen
Standort-Tester
Benutzerdefinierter Code

Mitglieder-Skripte

Eine attributbasierte Lösung zum Hinzufügen von Funktionen zu Ihrer Webflow-Site.
Kopieren Sie einfach etwas Code, fügen Sie einige Attribute hinzu, und schon sind Sie fertig.

Vielen Dank! Ihr Beitrag ist eingegangen!
Huch! Beim Absenden des Formulars ist etwas schief gelaufen.
Benötigen Sie Hilfe mit MemberScripts?

Alle Memberstack-Kunden können im 2.0 Slack um Unterstützung bitten. Bitte beachten Sie, dass dies keine offiziellen Funktionen sind und der Support nicht garantiert werden kann.

UX
Forms

#195 - Custom Input Validation

Add real-time form validation with custom error messages.


<!-- 💙 MEMBERSCRIPT #195 v0.1 💙 - CUSTOM INPUT VALIDATION WITH ERROR MESSAGES -->
<!--
  A flexible validation system for form inputs that supports international formats
  for postal codes, phone numbers, email addresses, and custom regex patterns.
  
  CUSTOMIZATION GUIDE:
  ====================
  All customizable sections are marked with "MODIFY:" comments below.
  Search for "MODIFY:" to find all areas you can customize.
  
  Key Customization Areas:
  1. VALIDATION PATTERNS (lines ~20-60) - Change regex patterns for different countries
  2. ERROR MESSAGES (lines ~65-75) - Customize error messages in any language
  3. OPTIONS (lines ~80-85) - Toggle validation features on/off
-->
<script>
(function() {
  'use strict';
  
  // ============================================================================
  // MODIFY: SELECTORS - Change these if you use different data-ms-code attributes
  // ============================================================================
  const CONFIG = {
    SELECTORS: {
      form: '[data-ms-code="validation-form"]',
      inputPrefix: '[data-ms-code^="validation-input-"]',
      errorPrefix: '[data-ms-code^="validation-error-"]'
    },
    
    // ============================================================================
    // MODIFY: VALIDATION PATTERNS - Customize for your country/region
    // ============================================================================
    // Replace these patterns with formats for your country or use 'regex' type
    // for custom patterns. Examples for different countries are provided below.
    PATTERNS: {
      // POSTAL CODE PATTERNS (choose one or use 'regex' for custom)
      // US ZIP: 12345 or 12345-6789
      zip: /^\d{5}(-\d{4})?$/,
      
      // UK Postcode: SW1A 1AA, M1 1AA, B33 8TH
      // Uncomment to use UK format instead:
      // zip: /^[A-Z]{1,2}\d{1,2}[A-Z]?\s?\d[A-Z]{2}$/i,
      
      // Canadian Postal Code: A1A 1A1
      // Uncomment to use Canadian format instead:
      // zip: /^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/i,
      
      // Australian Postcode: 2000-9999
      // Uncomment to use Australian format instead:
      // zip: /^\d{4}$/,
      
      // German Postcode: 12345
      // Uncomment to use German format instead:
      // zip: /^\d{5}$/,
      
      // Flexible international postal code (allows letters, numbers, spaces, hyphens)
      // Uncomment to use flexible format:
      // zip: /^[A-Z0-9\s\-]{3,10}$/i,
      
      // PHONE NUMBER PATTERNS
      // Flexible international phone (allows digits, spaces, hyphens, parentheses, plus, dots)
      phone: /^[\d\s\-\(\)\+\.]+$/,
      
      // US Phone: (123) 456-7890 or 123-456-7890
      // Uncomment to use US format instead:
      // phone: /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/,
      
      // International with country code: +1 234 567 8900, +44 20 1234 5678
      // Uncomment to use international format:
      // phone: /^[\+]?[1-9][\d]{0,15}$/,
      
      // EMAIL PATTERN (works internationally)
      email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    },
    
    // ============================================================================
    // MODIFY: ERROR MESSAGES - Customize messages in your language
    // ============================================================================
    // Change these messages to match your language and validation rules
    MESSAGES: {
      // Postal code messages (update example to match your pattern)
      zip: 'Please enter a valid postal code',
      // Examples for different countries:
      // zip: 'Please enter a valid UK postcode (e.g., SW1A 1AA)',
      // zip: 'Please enter a valid Canadian postal code (e.g., A1A 1A1)',
      // zip: 'Bitte geben Sie eine gültige Postleitzahl ein', // German
      // zip: 'Por favor ingrese un código postal válido', // Spanish
      
      // Phone number messages
      phone: 'Please enter a valid phone number',
      // Examples:
      // phone: 'Please enter a valid phone number (e.g., +1 234 567 8900)',
      // phone: 'Bitte geben Sie eine gültige Telefonnummer ein', // German
      
      // Email messages
      email: 'Please enter a valid email address',
      
      // Generic messages
      regex: 'Please enter a valid value',
      required: 'This field is required',
      default: 'Please enter a valid value'
    },
    
    // ============================================================================
    // MODIFY: OPTIONS - Toggle validation features on/off
    // ============================================================================
    OPTIONS: {
      realTimeValidation: true,  // Show errors as user types
      validateOnBlur: true,  // Validate when user leaves the field
      preventSubmit: true  // Prevent form submission if validation fails
    }
  };
  
  let form = null;
  let validationRules = [];
  
  // TIMING - Adjust timeout values if needed (in milliseconds)
  function waitFor(condition, timeout = 5000) {
    return new Promise((resolve) => {
      if (condition()) return resolve();
      const interval = setInterval(() => {
        if (condition()) {
          clearInterval(interval);
          resolve();
        }
      }, 100);
      setTimeout(() => {
        clearInterval(interval);
        resolve();
      }, timeout);
    });
  }
  
  async function init() {
    await waitFor(() => document.querySelector(CONFIG.SELECTORS.form));
    
    form = document.querySelector(CONFIG.SELECTORS.form);
    if (!form) {
      console.warn('MemberScript #195: Form not found. Add data-ms-code="validation-form" to your form element.');
      return;
    }
    
    // Find all inputs with validation attributes
    const inputs = form.querySelectorAll(CONFIG.SELECTORS.inputPrefix);
    
    if (inputs.length === 0) {
      console.warn('MemberScript #195: No validation inputs found. Add data-ms-code="validation-input-[name]" to your inputs.');
      return;
    }
    
    // Build validation rules for each input
    inputs.forEach(input => {
      const inputCode = input.getAttribute('data-ms-code');
      const fieldName = inputCode.replace('validation-input-', '');
      const validationType = input.getAttribute('data-validation-type');
      const errorElement = form.querySelector(`[data-ms-code="validation-error-${fieldName}"]`);
      
      if (!validationType) {
        console.warn(`MemberScript #195: Input "${fieldName}" missing data-validation-type attribute.`);
        return;
      }
      
      if (!errorElement) {
        console.warn(`MemberScript #195: Error element not found for "${fieldName}". Add data-ms-code="validation-error-${fieldName}".`);
        return;
      }
      
      // Hide error message initially
      errorElement.style.display = 'none';
      
      // Build validation rule
      const rule = {
        input: input,
        fieldName: fieldName,
        type: validationType,
        errorElement: errorElement,
        pattern: null,
        message: null,
        required: input.hasAttribute('required') || input.getAttribute('data-validation-required') === 'true'
      };
      
      // Set up validation based on type
      if (validationType === 'regex') {
        // MODIFY: Custom regex validation - use data-validation-pattern attribute
        // Example: data-validation-pattern="^[A-Z0-9]{5}$" for 5 alphanumeric characters
        const customPattern = input.getAttribute('data-validation-pattern');
        const customMessage = input.getAttribute('data-validation-message');
        
        if (!customPattern) {
          console.warn(`MemberScript #195: Regex validation requires data-validation-pattern attribute for "${fieldName}".`);
          return;
        }
        
        try {
          rule.pattern = new RegExp(customPattern);
          rule.message = customMessage || CONFIG.MESSAGES.regex;
        } catch (e) {
          console.error(`MemberScript #195: Invalid regex pattern for "${fieldName}":`, e);
          return;
        }
      } else if (CONFIG.PATTERNS[validationType]) {
        // Use predefined pattern from CONFIG.PATTERNS
        rule.pattern = CONFIG.PATTERNS[validationType];
        rule.message = CONFIG.MESSAGES[validationType];
        
        // MODIFY: Override message per field using data-validation-message attribute
        // Example: <input data-validation-message="Custom error message" ...>
        const fieldMessage = input.getAttribute('data-validation-message');
        if (fieldMessage) {
          rule.message = fieldMessage;
        }
      } else {
        console.warn(`MemberScript #195: Unknown validation type "${validationType}" for "${fieldName}". Use 'zip', 'phone', 'email', or 'regex'.`);
        return;
      }
      
      validationRules.push(rule);
      
      // Add event listeners
      if (CONFIG.OPTIONS.realTimeValidation) {
        input.addEventListener('input', () => validateField(rule));
      }
      
      if (CONFIG.OPTIONS.validateOnBlur) {
        input.addEventListener('blur', () => validateField(rule));
      }
    });
    
    // Add form submit handler
    if (CONFIG.OPTIONS.preventSubmit) {
      form.addEventListener('submit', handleFormSubmit);
      
      // Also handle click on submit button (works with both <button> and <a> tags)
      const submitButton = form.querySelector('button[type="submit"], input[type="submit"], a[type="submit"], [type="submit"]');
      if (submitButton) {
        submitButton.addEventListener('click', function(e) {
          const isValid = validateAllFields();
          
          if (!isValid) {
            e.preventDefault();
            e.stopPropagation();
            
            // Focus first invalid field
            const firstInvalid = validationRules.find(rule => {
              const value = rule.input.value.trim();
              if (rule.required && !value) return true;
              if (value && rule.pattern && !rule.pattern.test(value)) return true;
              return false;
            });
            
            if (firstInvalid) {
              firstInvalid.input.focus();
              firstInvalid.input.scrollIntoView({ behavior: 'smooth', block: 'center' });
            }
          } else {
            // If validation passes and it's a link, prevent default navigation and submit form
            if (submitButton.tagName === 'A' && (submitButton.href === '#' || submitButton.getAttribute('href') === '#')) {
              e.preventDefault();
              e.stopPropagation();
              form.submit();
            }
          }
        });
      }
    }
  }
  
  function validateField(rule) {
    const value = rule.input.value.trim();
    let isValid = true;
    let errorMessage = '';
    
    // Check required field
    if (rule.required && !value) {
      isValid = false;
      errorMessage = CONFIG.MESSAGES.required;
    }
    // Check pattern if value exists
    else if (value && rule.pattern && !rule.pattern.test(value)) {
      isValid = false;
      errorMessage = rule.message || CONFIG.MESSAGES.default;
    }
    
    // Show or hide error message
    if (isValid) {
      rule.errorElement.style.display = 'none';
      rule.input.classList.remove('validation-error');
      rule.input.classList.add('validation-valid');
    } else {
      rule.errorElement.style.display = 'block';
      rule.errorElement.textContent = errorMessage;
      rule.input.classList.add('validation-error');
      rule.input.classList.remove('validation-valid');
    }
    
    return isValid;
  }
  
  function validateAllFields() {
    let allValid = true;
    
    validationRules.forEach(rule => {
      if (!validateField(rule)) {
        allValid = false;
      }
    });
    
    return allValid;
  }
  
  function handleFormSubmit(event) {
    if (!validateAllFields()) {
      event.preventDefault();
      event.stopPropagation();
      
      // Focus first invalid field
      const firstInvalid = validationRules.find(rule => {
        const value = rule.input.value.trim();
        if (rule.required && !value) return true;
        if (value && rule.pattern && !rule.pattern.test(value)) return true;
        return false;
      });
      
      if (firstInvalid) {
        firstInvalid.input.focus();
        firstInvalid.input.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  }
  
  // ============================================================================
  // MODIFY: INITIALIZATION DELAY - Adjust if scripts load slowly (in milliseconds)
  // ============================================================================
  const INIT_DELAY = 100; // Increase to 200-500 if form loads slowly
  
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => setTimeout(init, INIT_DELAY));
  } else {
    setTimeout(init, INIT_DELAY);
  }
  
})();
</script>



v0.1

<!-- 💙 MEMBERSCRIPT #195 v0.1 💙 - CUSTOM INPUT VALIDATION WITH ERROR MESSAGES -->
<!--
  A flexible validation system for form inputs that supports international formats
  for postal codes, phone numbers, email addresses, and custom regex patterns.
  
  CUSTOMIZATION GUIDE:
  ====================
  All customizable sections are marked with "MODIFY:" comments below.
  Search for "MODIFY:" to find all areas you can customize.
  
  Key Customization Areas:
  1. VALIDATION PATTERNS (lines ~20-60) - Change regex patterns for different countries
  2. ERROR MESSAGES (lines ~65-75) - Customize error messages in any language
  3. OPTIONS (lines ~80-85) - Toggle validation features on/off
-->
<script>
(function() {
  'use strict';
  
  // ============================================================================
  // MODIFY: SELECTORS - Change these if you use different data-ms-code attributes
  // ============================================================================
  const CONFIG = {
    SELECTORS: {
      form: '[data-ms-code="validation-form"]',
      inputPrefix: '[data-ms-code^="validation-input-"]',
      errorPrefix: '[data-ms-code^="validation-error-"]'
    },
    
    // ============================================================================
    // MODIFY: VALIDATION PATTERNS - Customize for your country/region
    // ============================================================================
    // Replace these patterns with formats for your country or use 'regex' type
    // for custom patterns. Examples for different countries are provided below.
    PATTERNS: {
      // POSTAL CODE PATTERNS (choose one or use 'regex' for custom)
      // US ZIP: 12345 or 12345-6789
      zip: /^\d{5}(-\d{4})?$/,
      
      // UK Postcode: SW1A 1AA, M1 1AA, B33 8TH
      // Uncomment to use UK format instead:
      // zip: /^[A-Z]{1,2}\d{1,2}[A-Z]?\s?\d[A-Z]{2}$/i,
      
      // Canadian Postal Code: A1A 1A1
      // Uncomment to use Canadian format instead:
      // zip: /^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/i,
      
      // Australian Postcode: 2000-9999
      // Uncomment to use Australian format instead:
      // zip: /^\d{4}$/,
      
      // German Postcode: 12345
      // Uncomment to use German format instead:
      // zip: /^\d{5}$/,
      
      // Flexible international postal code (allows letters, numbers, spaces, hyphens)
      // Uncomment to use flexible format:
      // zip: /^[A-Z0-9\s\-]{3,10}$/i,
      
      // PHONE NUMBER PATTERNS
      // Flexible international phone (allows digits, spaces, hyphens, parentheses, plus, dots)
      phone: /^[\d\s\-\(\)\+\.]+$/,
      
      // US Phone: (123) 456-7890 or 123-456-7890
      // Uncomment to use US format instead:
      // phone: /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/,
      
      // International with country code: +1 234 567 8900, +44 20 1234 5678
      // Uncomment to use international format:
      // phone: /^[\+]?[1-9][\d]{0,15}$/,
      
      // EMAIL PATTERN (works internationally)
      email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    },
    
    // ============================================================================
    // MODIFY: ERROR MESSAGES - Customize messages in your language
    // ============================================================================
    // Change these messages to match your language and validation rules
    MESSAGES: {
      // Postal code messages (update example to match your pattern)
      zip: 'Please enter a valid postal code',
      // Examples for different countries:
      // zip: 'Please enter a valid UK postcode (e.g., SW1A 1AA)',
      // zip: 'Please enter a valid Canadian postal code (e.g., A1A 1A1)',
      // zip: 'Bitte geben Sie eine gültige Postleitzahl ein', // German
      // zip: 'Por favor ingrese un código postal válido', // Spanish
      
      // Phone number messages
      phone: 'Please enter a valid phone number',
      // Examples:
      // phone: 'Please enter a valid phone number (e.g., +1 234 567 8900)',
      // phone: 'Bitte geben Sie eine gültige Telefonnummer ein', // German
      
      // Email messages
      email: 'Please enter a valid email address',
      
      // Generic messages
      regex: 'Please enter a valid value',
      required: 'This field is required',
      default: 'Please enter a valid value'
    },
    
    // ============================================================================
    // MODIFY: OPTIONS - Toggle validation features on/off
    // ============================================================================
    OPTIONS: {
      realTimeValidation: true,  // Show errors as user types
      validateOnBlur: true,  // Validate when user leaves the field
      preventSubmit: true  // Prevent form submission if validation fails
    }
  };
  
  let form = null;
  let validationRules = [];
  
  // TIMING - Adjust timeout values if needed (in milliseconds)
  function waitFor(condition, timeout = 5000) {
    return new Promise((resolve) => {
      if (condition()) return resolve();
      const interval = setInterval(() => {
        if (condition()) {
          clearInterval(interval);
          resolve();
        }
      }, 100);
      setTimeout(() => {
        clearInterval(interval);
        resolve();
      }, timeout);
    });
  }
  
  async function init() {
    await waitFor(() => document.querySelector(CONFIG.SELECTORS.form));
    
    form = document.querySelector(CONFIG.SELECTORS.form);
    if (!form) {
      console.warn('MemberScript #195: Form not found. Add data-ms-code="validation-form" to your form element.');
      return;
    }
    
    // Find all inputs with validation attributes
    const inputs = form.querySelectorAll(CONFIG.SELECTORS.inputPrefix);
    
    if (inputs.length === 0) {
      console.warn('MemberScript #195: No validation inputs found. Add data-ms-code="validation-input-[name]" to your inputs.');
      return;
    }
    
    // Build validation rules for each input
    inputs.forEach(input => {
      const inputCode = input.getAttribute('data-ms-code');
      const fieldName = inputCode.replace('validation-input-', '');
      const validationType = input.getAttribute('data-validation-type');
      const errorElement = form.querySelector(`[data-ms-code="validation-error-${fieldName}"]`);
      
      if (!validationType) {
        console.warn(`MemberScript #195: Input "${fieldName}" missing data-validation-type attribute.`);
        return;
      }
      
      if (!errorElement) {
        console.warn(`MemberScript #195: Error element not found for "${fieldName}". Add data-ms-code="validation-error-${fieldName}".`);
        return;
      }
      
      // Hide error message initially
      errorElement.style.display = 'none';
      
      // Build validation rule
      const rule = {
        input: input,
        fieldName: fieldName,
        type: validationType,
        errorElement: errorElement,
        pattern: null,
        message: null,
        required: input.hasAttribute('required') || input.getAttribute('data-validation-required') === 'true'
      };
      
      // Set up validation based on type
      if (validationType === 'regex') {
        // MODIFY: Custom regex validation - use data-validation-pattern attribute
        // Example: data-validation-pattern="^[A-Z0-9]{5}$" for 5 alphanumeric characters
        const customPattern = input.getAttribute('data-validation-pattern');
        const customMessage = input.getAttribute('data-validation-message');
        
        if (!customPattern) {
          console.warn(`MemberScript #195: Regex validation requires data-validation-pattern attribute for "${fieldName}".`);
          return;
        }
        
        try {
          rule.pattern = new RegExp(customPattern);
          rule.message = customMessage || CONFIG.MESSAGES.regex;
        } catch (e) {
          console.error(`MemberScript #195: Invalid regex pattern for "${fieldName}":`, e);
          return;
        }
      } else if (CONFIG.PATTERNS[validationType]) {
        // Use predefined pattern from CONFIG.PATTERNS
        rule.pattern = CONFIG.PATTERNS[validationType];
        rule.message = CONFIG.MESSAGES[validationType];
        
        // MODIFY: Override message per field using data-validation-message attribute
        // Example: <input data-validation-message="Custom error message" ...>
        const fieldMessage = input.getAttribute('data-validation-message');
        if (fieldMessage) {
          rule.message = fieldMessage;
        }
      } else {
        console.warn(`MemberScript #195: Unknown validation type "${validationType}" for "${fieldName}". Use 'zip', 'phone', 'email', or 'regex'.`);
        return;
      }
      
      validationRules.push(rule);
      
      // Add event listeners
      if (CONFIG.OPTIONS.realTimeValidation) {
        input.addEventListener('input', () => validateField(rule));
      }
      
      if (CONFIG.OPTIONS.validateOnBlur) {
        input.addEventListener('blur', () => validateField(rule));
      }
    });
    
    // Add form submit handler
    if (CONFIG.OPTIONS.preventSubmit) {
      form.addEventListener('submit', handleFormSubmit);
      
      // Also handle click on submit button (works with both <button> and <a> tags)
      const submitButton = form.querySelector('button[type="submit"], input[type="submit"], a[type="submit"], [type="submit"]');
      if (submitButton) {
        submitButton.addEventListener('click', function(e) {
          const isValid = validateAllFields();
          
          if (!isValid) {
            e.preventDefault();
            e.stopPropagation();
            
            // Focus first invalid field
            const firstInvalid = validationRules.find(rule => {
              const value = rule.input.value.trim();
              if (rule.required && !value) return true;
              if (value && rule.pattern && !rule.pattern.test(value)) return true;
              return false;
            });
            
            if (firstInvalid) {
              firstInvalid.input.focus();
              firstInvalid.input.scrollIntoView({ behavior: 'smooth', block: 'center' });
            }
          } else {
            // If validation passes and it's a link, prevent default navigation and submit form
            if (submitButton.tagName === 'A' && (submitButton.href === '#' || submitButton.getAttribute('href') === '#')) {
              e.preventDefault();
              e.stopPropagation();
              form.submit();
            }
          }
        });
      }
    }
  }
  
  function validateField(rule) {
    const value = rule.input.value.trim();
    let isValid = true;
    let errorMessage = '';
    
    // Check required field
    if (rule.required && !value) {
      isValid = false;
      errorMessage = CONFIG.MESSAGES.required;
    }
    // Check pattern if value exists
    else if (value && rule.pattern && !rule.pattern.test(value)) {
      isValid = false;
      errorMessage = rule.message || CONFIG.MESSAGES.default;
    }
    
    // Show or hide error message
    if (isValid) {
      rule.errorElement.style.display = 'none';
      rule.input.classList.remove('validation-error');
      rule.input.classList.add('validation-valid');
    } else {
      rule.errorElement.style.display = 'block';
      rule.errorElement.textContent = errorMessage;
      rule.input.classList.add('validation-error');
      rule.input.classList.remove('validation-valid');
    }
    
    return isValid;
  }
  
  function validateAllFields() {
    let allValid = true;
    
    validationRules.forEach(rule => {
      if (!validateField(rule)) {
        allValid = false;
      }
    });
    
    return allValid;
  }
  
  function handleFormSubmit(event) {
    if (!validateAllFields()) {
      event.preventDefault();
      event.stopPropagation();
      
      // Focus first invalid field
      const firstInvalid = validationRules.find(rule => {
        const value = rule.input.value.trim();
        if (rule.required && !value) return true;
        if (value && rule.pattern && !rule.pattern.test(value)) return true;
        return false;
      });
      
      if (firstInvalid) {
        firstInvalid.input.focus();
        firstInvalid.input.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  }
  
  // ============================================================================
  // MODIFY: INITIALIZATION DELAY - Adjust if scripts load slowly (in milliseconds)
  // ============================================================================
  const INIT_DELAY = 100; // Increase to 200-500 if form loads slowly
  
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => setTimeout(init, INIT_DELAY));
  } else {
    setTimeout(init, INIT_DELAY);
  }
  
})();
</script>



Ansicht Memberscript
Data Tables

#194 - To-do List

Create a fully functional to-do list that saves to Memberstack, with add, complete, and delete functionality.



<!-- 💙 MEMBERSCRIPT #194 v0.1 💙 - TO-DO LIST -->
<script>
(function() {
  'use strict';
  
  // CONFIGURATION - Change these values to customize
  const CONFIG = {
    // Change this to match your Data Table name in Memberstack
    TABLE_NAME: 'todos',
    
    // Change these selectors if you use different data-ms-code attributes
    SELECTORS: {
      container: '[data-ms-code="todo-container"]',
      form: '[data-ms-code="todo-form"]',
      input: '[data-ms-code="todo-input"]',
      addButton: '[data-ms-code="todo-add-button"]',
      list: '[data-ms-code="todo-list"]',
      empty: '[data-ms-code="todo-empty"]',
      template: '[data-ms-code="todo-item-template"]',
      deleteModal: '[data-ms-code="todo-delete-modal"]',
      deleteConfirm: '[data-ms-code="todo-delete-confirm"]',
      deleteCancel: '[data-ms-code="todo-delete-cancel"]'
    }
  };
  
  let memberstack = null;
  let currentMember = null;
  let pendingDeleteTaskId = null;
  
  // TIMING - Adjust timeout values if needed (in milliseconds)
  function waitFor(condition, timeout = 5000) {
    return new Promise((resolve) => {
      if (condition()) return resolve();
      const interval = setInterval(() => {
        if (condition()) {
          clearInterval(interval);
          resolve();
        }
      }, 100); // Check every 100ms
      setTimeout(() => {
        clearInterval(interval);
        resolve();
      }, timeout);
    });
  }
  
  async function init() {
    await Promise.all([
      waitFor(() => document.querySelector(CONFIG.SELECTORS.form) && window.$memberstackDom),
      waitFor(() => window.$memberstackDom, 10000)
    ]);
    
    memberstack = window.$memberstackDom;
    if (!memberstack) return;
    
    const memberResult = await memberstack.getCurrentMember();
    currentMember = memberResult?.data || memberResult;
    
    if (!currentMember?.id) {
      // CUSTOMIZE - Change the "not logged in" message here
      const container = document.querySelector(CONFIG.SELECTORS.container);
      if (container) container.innerHTML = '<div data-ms-code="todo-empty"><p>Please log in to use the to-do list.</p></div>';
      return;
    }
    
    const form = document.querySelector(CONFIG.SELECTORS.form);
    if (form) {
      const formClone = form.cloneNode(true);
      form.parentNode.replaceChild(formClone, form);
      formClone.addEventListener('submit', handleAddTask);
      
      // Also handle click on add button (works even if button is not type="submit")
      const addButton = formClone.querySelector(CONFIG.SELECTORS.addButton) || document.querySelector(CONFIG.SELECTORS.addButton);
      if (addButton) {
        addButton.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopPropagation();
          // Trigger form submit event so handleAddTask is called
          const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
          formClone.dispatchEvent(submitEvent);
        });
      }
    }
    
    document.querySelector(CONFIG.SELECTORS.deleteConfirm)?.addEventListener('click', (e) => {
      e.preventDefault();
      handleConfirmDelete();
    });
    
    document.querySelector(CONFIG.SELECTORS.deleteCancel)?.addEventListener('click', (e) => {
      e.preventDefault();
      const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
      if (modal) {
        modal.style.display = 'none';
        pendingDeleteTaskId = null;
      }
    });
    
    const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
    if (modal) modal.style.display = 'none';
    
    await loadTasks();
  }
  
  async function loadTasks() {
    if (!memberstack || !currentMember?.id) return;
    
    try {
      const result = await memberstack.queryDataRecords({
        table: CONFIG.TABLE_NAME,
        query: {
          where: { member: { equals: currentMember.id } },
          orderBy: { created_at: 'desc' }, // SORTING - Change 'desc' to 'asc' for oldest first
          take: 100 // LIMIT - Change max number of tasks to load
        }
      });
      
      const list = document.querySelector(CONFIG.SELECTORS.list);
      const empty = document.querySelector(CONFIG.SELECTORS.empty);
      const template = document.querySelector(CONFIG.SELECTORS.template);
      
      if (!list) return;
      
      const templateClone = template && template.parentElement === list ? template.cloneNode(true) : null;
      list.innerHTML = '';
      if (templateClone) list.appendChild(templateClone);
      
      const tasks = result?.data?.records || result?.data || result || [];
      
      if (tasks.length === 0) {
        if (empty) empty.style.display = 'block';
        return;
      }
      
      if (empty) empty.style.display = 'none';
      tasks.forEach(task => renderTask(task));
      
    } catch (error) {
      console.error('MemberScript #194: Error loading tasks:', error);
      const empty = document.querySelector(CONFIG.SELECTORS.empty);
      if (empty) empty.style.display = 'block';
    }
  }
  
  function renderTask(task) {
    const list = document.querySelector(CONFIG.SELECTORS.list);
    const template = document.querySelector(CONFIG.SELECTORS.template);
    if (!list || !template) return;
    
    const taskItem = template.cloneNode(true);
    const taskData = task.data || {};
    const isCompleted = taskData.completed === true;
    
    taskItem.removeAttribute('data-ms-code');
    taskItem.setAttribute('data-ms-code', 'todo-item');
    taskItem.setAttribute('data-task-id', task.id);
    taskItem.classList.remove('todo-template-hidden');
    taskItem.style.display = '';
    
    const checkbox = taskItem.querySelector('[data-ms-code="todo-checkbox"]');
    const taskTextEl = taskItem.querySelector('[data-ms-code="todo-text"]');
    const deleteBtn = taskItem.querySelector('[data-ms-code="todo-delete"]');
    
    if (checkbox) {
      checkbox.checked = isCompleted;
      checkbox.addEventListener('change', (e) => {
        handleToggleTask(task.id, e.target.checked);
      });
    }
    
    if (taskTextEl) {
      taskTextEl.textContent = taskData.task || '';
      if (isCompleted) {
        taskTextEl.classList.add('completed');
      }
    }
    
    if (deleteBtn) {
      deleteBtn.addEventListener('click', (e) => {
        e.preventDefault();
        handleDeleteTask(task.id);
      });
    }
    
    list.insertBefore(taskItem, list.firstChild);
  }
  
  async function handleAddTask(event) {
    event.preventDefault();
    const input = event.target.querySelector(CONFIG.SELECTORS.input) || document.querySelector(CONFIG.SELECTORS.input);
    if (!input) return;
    
    const taskText = input.value.trim();
    if (!taskText) return;
    
    const addButton = document.querySelector(CONFIG.SELECTORS.addButton);
    if (addButton) {
      addButton.disabled = true;
      addButton.textContent = 'Adding...'; // BUTTON TEXT - Change loading state text
    }
    
    try {
      const now = new Date().toISOString();
      // TASK DATA - Add or modify fields here to match your Data Table schema
      const taskData = {
        task: taskText,
        completed: false,
        member: currentMember.id,
        created_at: now,
        updated_at: now
      };
      
      try {
        await memberstack.createDataRecord({ table: CONFIG.TABLE_NAME, data: taskData });
      } catch (e) {
        await memberstack.createDataRecord({
          table: CONFIG.TABLE_NAME,
          data: { ...taskData, member: { id: currentMember.id } }
        });
      }
      
      input.value = '';
      await loadTasks();
    } catch (error) {
      console.error('MemberScript #194: Error adding task:', error);
      // ERROR MESSAGE - Customize the error message shown to users
      alert('Failed to add task. Please try again.');
    } finally {
      if (addButton) {
        addButton.disabled = false;
        addButton.textContent = 'Add'; // BUTTON TEXT - Change button text
      }
    }
  }
  
  async function handleToggleTask(taskId, newCompletedState) {
    try {
      await memberstack.updateDataRecord({
        recordId: taskId,
        data: { completed: newCompletedState, updated_at: new Date().toISOString() }
      });
      
      // Update UI immediately
      const taskItem = document.querySelector(`[data-task-id="${taskId}"]`);
      if (taskItem) {
        const taskTextEl = taskItem.querySelector('[data-ms-code="todo-text"]');
        const checkbox = taskItem.querySelector('[data-ms-code="todo-checkbox"]');
        if (taskTextEl) {
          if (newCompletedState) {
            taskTextEl.classList.add('completed');
          } else {
            taskTextEl.classList.remove('completed');
          }
        }
        if (checkbox) {
          checkbox.checked = newCompletedState;
        }
      }
    } catch (error) {
      console.error('MemberScript #194: Error toggling task:', error);
      await loadTasks();
    }
  }
  
  function handleDeleteTask(taskId) {
    pendingDeleteTaskId = taskId;
    const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
    if (modal) modal.style.display = 'flex';
  }
  
  async function handleConfirmDelete() {
    if (!pendingDeleteTaskId) return;
    
    const taskId = pendingDeleteTaskId;
    pendingDeleteTaskId = null;
    
    const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
    if (modal) modal.style.display = 'none';
    
    try {
      await memberstack.deleteDataRecord({ recordId: taskId });
      
      const taskItem = document.querySelector(`[data-task-id="${taskId}"]`);
      if (taskItem) taskItem.remove();
      
      const list = document.querySelector(CONFIG.SELECTORS.list);
      const taskItems = list ? Array.from(list.children).filter(c => c.getAttribute('data-ms-code') !== 'todo-item-template') : [];
      
      if (taskItems.length === 0) {
        const empty = document.querySelector(CONFIG.SELECTORS.empty);
        if (empty) empty.style.display = 'block';
      }
    } catch (error) {
      console.error('MemberScript #194: Error deleting task:', error);
      // ERROR MESSAGE - Customize the error message shown to users
      alert('Failed to delete task. Please try again.');
      await loadTasks();
    }
  }
  
  // INITIALIZATION DELAY - Adjust the 100ms delay if scripts load slowly
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
  } else {
    setTimeout(init, 100);
  }
  
})();
</script>


v0.1


<!-- 💙 MEMBERSCRIPT #194 v0.1 💙 - TO-DO LIST -->
<script>
(function() {
  'use strict';
  
  // CONFIGURATION - Change these values to customize
  const CONFIG = {
    // Change this to match your Data Table name in Memberstack
    TABLE_NAME: 'todos',
    
    // Change these selectors if you use different data-ms-code attributes
    SELECTORS: {
      container: '[data-ms-code="todo-container"]',
      form: '[data-ms-code="todo-form"]',
      input: '[data-ms-code="todo-input"]',
      addButton: '[data-ms-code="todo-add-button"]',
      list: '[data-ms-code="todo-list"]',
      empty: '[data-ms-code="todo-empty"]',
      template: '[data-ms-code="todo-item-template"]',
      deleteModal: '[data-ms-code="todo-delete-modal"]',
      deleteConfirm: '[data-ms-code="todo-delete-confirm"]',
      deleteCancel: '[data-ms-code="todo-delete-cancel"]'
    }
  };
  
  let memberstack = null;
  let currentMember = null;
  let pendingDeleteTaskId = null;
  
  // TIMING - Adjust timeout values if needed (in milliseconds)
  function waitFor(condition, timeout = 5000) {
    return new Promise((resolve) => {
      if (condition()) return resolve();
      const interval = setInterval(() => {
        if (condition()) {
          clearInterval(interval);
          resolve();
        }
      }, 100); // Check every 100ms
      setTimeout(() => {
        clearInterval(interval);
        resolve();
      }, timeout);
    });
  }
  
  async function init() {
    await Promise.all([
      waitFor(() => document.querySelector(CONFIG.SELECTORS.form) && window.$memberstackDom),
      waitFor(() => window.$memberstackDom, 10000)
    ]);
    
    memberstack = window.$memberstackDom;
    if (!memberstack) return;
    
    const memberResult = await memberstack.getCurrentMember();
    currentMember = memberResult?.data || memberResult;
    
    if (!currentMember?.id) {
      // CUSTOMIZE - Change the "not logged in" message here
      const container = document.querySelector(CONFIG.SELECTORS.container);
      if (container) container.innerHTML = '<div data-ms-code="todo-empty"><p>Please log in to use the to-do list.</p></div>';
      return;
    }
    
    const form = document.querySelector(CONFIG.SELECTORS.form);
    if (form) {
      const formClone = form.cloneNode(true);
      form.parentNode.replaceChild(formClone, form);
      formClone.addEventListener('submit', handleAddTask);
      
      // Also handle click on add button (works even if button is not type="submit")
      const addButton = formClone.querySelector(CONFIG.SELECTORS.addButton) || document.querySelector(CONFIG.SELECTORS.addButton);
      if (addButton) {
        addButton.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopPropagation();
          // Trigger form submit event so handleAddTask is called
          const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
          formClone.dispatchEvent(submitEvent);
        });
      }
    }
    
    document.querySelector(CONFIG.SELECTORS.deleteConfirm)?.addEventListener('click', (e) => {
      e.preventDefault();
      handleConfirmDelete();
    });
    
    document.querySelector(CONFIG.SELECTORS.deleteCancel)?.addEventListener('click', (e) => {
      e.preventDefault();
      const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
      if (modal) {
        modal.style.display = 'none';
        pendingDeleteTaskId = null;
      }
    });
    
    const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
    if (modal) modal.style.display = 'none';
    
    await loadTasks();
  }
  
  async function loadTasks() {
    if (!memberstack || !currentMember?.id) return;
    
    try {
      const result = await memberstack.queryDataRecords({
        table: CONFIG.TABLE_NAME,
        query: {
          where: { member: { equals: currentMember.id } },
          orderBy: { created_at: 'desc' }, // SORTING - Change 'desc' to 'asc' for oldest first
          take: 100 // LIMIT - Change max number of tasks to load
        }
      });
      
      const list = document.querySelector(CONFIG.SELECTORS.list);
      const empty = document.querySelector(CONFIG.SELECTORS.empty);
      const template = document.querySelector(CONFIG.SELECTORS.template);
      
      if (!list) return;
      
      const templateClone = template && template.parentElement === list ? template.cloneNode(true) : null;
      list.innerHTML = '';
      if (templateClone) list.appendChild(templateClone);
      
      const tasks = result?.data?.records || result?.data || result || [];
      
      if (tasks.length === 0) {
        if (empty) empty.style.display = 'block';
        return;
      }
      
      if (empty) empty.style.display = 'none';
      tasks.forEach(task => renderTask(task));
      
    } catch (error) {
      console.error('MemberScript #194: Error loading tasks:', error);
      const empty = document.querySelector(CONFIG.SELECTORS.empty);
      if (empty) empty.style.display = 'block';
    }
  }
  
  function renderTask(task) {
    const list = document.querySelector(CONFIG.SELECTORS.list);
    const template = document.querySelector(CONFIG.SELECTORS.template);
    if (!list || !template) return;
    
    const taskItem = template.cloneNode(true);
    const taskData = task.data || {};
    const isCompleted = taskData.completed === true;
    
    taskItem.removeAttribute('data-ms-code');
    taskItem.setAttribute('data-ms-code', 'todo-item');
    taskItem.setAttribute('data-task-id', task.id);
    taskItem.classList.remove('todo-template-hidden');
    taskItem.style.display = '';
    
    const checkbox = taskItem.querySelector('[data-ms-code="todo-checkbox"]');
    const taskTextEl = taskItem.querySelector('[data-ms-code="todo-text"]');
    const deleteBtn = taskItem.querySelector('[data-ms-code="todo-delete"]');
    
    if (checkbox) {
      checkbox.checked = isCompleted;
      checkbox.addEventListener('change', (e) => {
        handleToggleTask(task.id, e.target.checked);
      });
    }
    
    if (taskTextEl) {
      taskTextEl.textContent = taskData.task || '';
      if (isCompleted) {
        taskTextEl.classList.add('completed');
      }
    }
    
    if (deleteBtn) {
      deleteBtn.addEventListener('click', (e) => {
        e.preventDefault();
        handleDeleteTask(task.id);
      });
    }
    
    list.insertBefore(taskItem, list.firstChild);
  }
  
  async function handleAddTask(event) {
    event.preventDefault();
    const input = event.target.querySelector(CONFIG.SELECTORS.input) || document.querySelector(CONFIG.SELECTORS.input);
    if (!input) return;
    
    const taskText = input.value.trim();
    if (!taskText) return;
    
    const addButton = document.querySelector(CONFIG.SELECTORS.addButton);
    if (addButton) {
      addButton.disabled = true;
      addButton.textContent = 'Adding...'; // BUTTON TEXT - Change loading state text
    }
    
    try {
      const now = new Date().toISOString();
      // TASK DATA - Add or modify fields here to match your Data Table schema
      const taskData = {
        task: taskText,
        completed: false,
        member: currentMember.id,
        created_at: now,
        updated_at: now
      };
      
      try {
        await memberstack.createDataRecord({ table: CONFIG.TABLE_NAME, data: taskData });
      } catch (e) {
        await memberstack.createDataRecord({
          table: CONFIG.TABLE_NAME,
          data: { ...taskData, member: { id: currentMember.id } }
        });
      }
      
      input.value = '';
      await loadTasks();
    } catch (error) {
      console.error('MemberScript #194: Error adding task:', error);
      // ERROR MESSAGE - Customize the error message shown to users
      alert('Failed to add task. Please try again.');
    } finally {
      if (addButton) {
        addButton.disabled = false;
        addButton.textContent = 'Add'; // BUTTON TEXT - Change button text
      }
    }
  }
  
  async function handleToggleTask(taskId, newCompletedState) {
    try {
      await memberstack.updateDataRecord({
        recordId: taskId,
        data: { completed: newCompletedState, updated_at: new Date().toISOString() }
      });
      
      // Update UI immediately
      const taskItem = document.querySelector(`[data-task-id="${taskId}"]`);
      if (taskItem) {
        const taskTextEl = taskItem.querySelector('[data-ms-code="todo-text"]');
        const checkbox = taskItem.querySelector('[data-ms-code="todo-checkbox"]');
        if (taskTextEl) {
          if (newCompletedState) {
            taskTextEl.classList.add('completed');
          } else {
            taskTextEl.classList.remove('completed');
          }
        }
        if (checkbox) {
          checkbox.checked = newCompletedState;
        }
      }
    } catch (error) {
      console.error('MemberScript #194: Error toggling task:', error);
      await loadTasks();
    }
  }
  
  function handleDeleteTask(taskId) {
    pendingDeleteTaskId = taskId;
    const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
    if (modal) modal.style.display = 'flex';
  }
  
  async function handleConfirmDelete() {
    if (!pendingDeleteTaskId) return;
    
    const taskId = pendingDeleteTaskId;
    pendingDeleteTaskId = null;
    
    const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
    if (modal) modal.style.display = 'none';
    
    try {
      await memberstack.deleteDataRecord({ recordId: taskId });
      
      const taskItem = document.querySelector(`[data-task-id="${taskId}"]`);
      if (taskItem) taskItem.remove();
      
      const list = document.querySelector(CONFIG.SELECTORS.list);
      const taskItems = list ? Array.from(list.children).filter(c => c.getAttribute('data-ms-code') !== 'todo-item-template') : [];
      
      if (taskItems.length === 0) {
        const empty = document.querySelector(CONFIG.SELECTORS.empty);
        if (empty) empty.style.display = 'block';
      }
    } catch (error) {
      console.error('MemberScript #194: Error deleting task:', error);
      // ERROR MESSAGE - Customize the error message shown to users
      alert('Failed to delete task. Please try again.');
      await loadTasks();
    }
  }
  
  // INITIALIZATION DELAY - Adjust the 100ms delay if scripts load slowly
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
  } else {
    setTimeout(init, 100);
  }
  
})();
</script>


Ansicht Memberscript
Data Tables

#193 - Member Activity Timeline Tracker

Track and display all member interactions in a timeline.



<!-- 💙 MEMBERSCRIPT #193 v0.1 💙 - MEMBER ACTIVITY TRACKER -->
<script>
(function() {
  'use strict';
  // CUSTOMIZE: Configuration options
  const CONFIG = {
    DATA_TABLE_NAME: 'memberactivities', // Change to match your Memberstack Data Table name
    DATE_FORMAT: { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }, // Customize date format
    LOG_DEBOUNCE: 500, // Delay in ms before saving activities (reduce API calls)
    MAX_ACTIVITIES: 100 // Maximum activities to fetch and display
  };
  let memberstack = null, currentMember = null, queue = [], timeout = null, isSubmitting = false;
  function log(msg) { console.log('MS#193:', msg); }
  function waitForMS() {
    return new Promise(r => {
      if (window.$memberstackDom) return r();
      const t = setTimeout(r, 3000);
      document.addEventListener('memberstack.ready', () => { clearTimeout(t); r(); }, { once: true });
    });
  }
  async function getAllActivities() {
    const acts = [];
    if (CONFIG.DATA_TABLE_NAME && memberstack?.queryDataRecords) {
      try {
        const result = await memberstack.queryDataRecords({
          table: CONFIG.DATA_TABLE_NAME,
          query: { orderBy: { activitytimestamp: 'desc' }, take: CONFIG.MAX_ACTIVITIES }
        });
        if ('records' in (result.data || {})) {
          (result.data.records || []).forEach(rec => {
            const d = rec?.data;
            if (!d) return;
            acts.push({
              type: d.activitytype || 'activity',
              title: d.activitytitle || 'Activity',
              timestamp: d.activitytimestamp || (rec.createdAt ? new Date(rec.createdAt).getTime() : Date.now()),
              memberName: d.membername || 'Guest',
              memberEmail: d.memberemail || '',
              metadata: d.activitymetadata ? (typeof d.activitymetadata === 'string' ? JSON.parse(d.activitymetadata) : d.activitymetadata) : {}
            });
          });
        }
      } catch (e) { log('Data Table fetch failed: ' + e.message); }
    }
    return [...acts, ...queue].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
  }
  // CUSTOMIZE: Change 'en-US' to your locale (e.g., 'en-GB', 'fr-FR', 'de-DE')
  function formatDate(ts) {
    const d = new Date(ts);
    return isNaN(d.getTime()) ? 'Unknown' : d.toLocaleDateString('en-US', CONFIG.DATE_FORMAT);
  }
  function getMemberEmail(m) { return m?.auth?.email || m?.email || ''; }
  // CUSTOMIZE: Modify if your member data structure stores names differently
  function getMemberName(m) {
    if (!m) return 'Guest';
    const fn = m?.customFields?.['first-name'] || m?.customFields?.firstName || m?.data?.customFields?.['first-name'] || m?.data?.customFields?.firstName || m?.firstName || m?.data?.firstName;
    const ln = m?.customFields?.['last-name'] || m?.customFields?.lastName || m?.data?.customFields?.['last-name'] || m?.data?.customFields?.lastName || m?.lastName || m?.data?.lastName;
    const full = m?.customFields?.name || m?.data?.customFields?.name || m?.name || m?.data?.name;
    if (fn && ln) return `${fn} ${ln}`.trim();
    return fn || full || 'Guest';
  }
  function getElementText(el, activityType) {
    if (activityType === 'form' || el.tagName === 'FORM') {
      // For forms, prioritize data-name and aria-label which usually have the full form name
      return el.getAttribute('data-journey-title') || el.getAttribute('data-name') || el.getAttribute('aria-label') || el.getAttribute('name') || el.getAttribute('id') || el.title || 'form';
    }
    if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') {
      return el.getAttribute('data-journey-title') || el.getAttribute('data-name') || el.getAttribute('aria-label') || el.getAttribute('name') || el.getAttribute('id') || el.getAttribute('placeholder') || el.getAttribute('title') || (el.id ? document.querySelector(`label[for="${el.id}"]`)?.textContent?.trim() : null) || el.tagName.toLowerCase();
    }
    if (activityType === 'video' || el.tagName === 'VIDEO' || el.tagName === 'AUDIO') {
      let txt = el.getAttribute('data-journey-title') || el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('alt');
      if (!txt) {
        const container = el.closest('[data-ms-code="journey-track"]') || el;
        txt = container.querySelector('h1, h2, h3, h4, h5, h6')?.textContent?.trim();
        if (!txt) {
          let p = container.parentElement;
          while (p && !txt) { txt = p.querySelector('h1, h2, h3, h4, h5, h6')?.textContent?.trim(); p = p.parentElement; }
        }
        if (!txt) txt = container.querySelector('video[title], audio[title]')?.getAttribute('title') || 'video';
      }
      return txt;
    }
    return el.getAttribute('data-journey-title') || el.textContent?.trim() || el.getAttribute('aria-label') || el.title || el.getAttribute('alt') || 'item';
  }
  async function logActivity(el, type) {
    if (!memberstack) return;
    if (!currentMember) {
      try { const m = await memberstack.getCurrentMember(); currentMember = m?.data || m; } catch (e) { return; }
    }
    if (!currentMember) return;
    const email = getMemberEmail(currentMember);
    const name = getMemberName(currentMember);
    const eventType = el.getAttribute('data-journey-event');
    const activityType = el.getAttribute('data-journey-type') || type || 'click';
    const elementText = getElementText(el, activityType);
    const truncatedText = elementText.length > 50 ? elementText.substring(0, 50) + '...' : elementText; // CUSTOMIZE: Change 50 to adjust max text length
    // CUSTOMIZE: Modify action verbs to change how activities are described
    const eventActions = { 'play': 'played', 'pause': 'paused', 'ended': 'finished watching', 'change': 'changed', 'view': 'viewed', 'visible': 'viewed', 'submit': 'submitted', 'click': 'clicked on' };
    const typeActions = { 'link': 'clicked on', 'click': 'clicked on', 'form': 'submitted', 'button': 'clicked', 'video': 'watched', 'download': 'downloaded', 'view': 'viewed', 'custom': 'interacted with' };
    const action = eventActions[eventType] || typeActions[activityType] || 'interacted with';
    let itemType = '';
    if (el.tagName === 'A' || activityType === 'link') itemType = 'link';
    else if (el.tagName === 'BUTTON' || activityType === 'button') itemType = 'button';
    else if (el.tagName === 'FORM' || activityType === 'form') itemType = 'form';
    else if (el.tagName === 'VIDEO' || el.tagName === 'AUDIO' || activityType === 'video') itemType = 'video';
    else if (activityType === 'download') itemType = 'file';
    // Don't add 'page' for view events - just show "viewed 'text'"
    // For form field changes, get the parent form name
    let formName = '';
    if (eventType === 'change' && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')) {
      const parentForm = el.closest('form');
      if (parentForm) {
        // Get form name, prioritizing data-name and aria-label which usually have the full name
        formName = parentForm.getAttribute('data-name') || 
                   parentForm.getAttribute('aria-label') || 
                   parentForm.getAttribute('data-journey-title') ||
                   parentForm.getAttribute('name') || 
                   parentForm.getAttribute('id') || 
                   parentForm.title || 
                   'form';
        // Clean up form name - format nicely but keep "Form" if it's part of the name
        const formNameLower = formName.toLowerCase();
        // Only remove "form" suffix if it's a technical identifier (like "email-form" or "email_form")
        // Don't remove it if it's a proper name like "Email Form"
        if (formNameLower.endsWith('-form') || formNameLower.endsWith('_form')) {
          formName = formName.substring(0, formName.length - 5).trim();
          // Capitalize first letter of each word after removing technical suffix
          if (formName && formName.length > 0) {
            formName = formName.split(/[\s-]+/).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ') + ' Form';
          }
        } else {
          // Capitalize first letter of each word for better readability, keeping original structure
          if (formName && formName.length > 0) {
            formName = formName.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
          }
        }
      }
    }
    // Don't add itemType if elementText already contains it (e.g., "Email Form" already has "form")
    const textLower = truncatedText.toLowerCase();
    const typeLower = itemType.toLowerCase();
    const alreadyHasType = itemType && (textLower.endsWith(' ' + typeLower) || textLower.endsWith(typeLower) || textLower.includes(' ' + typeLower + ' '));
    // Build descriptive title
    let descriptiveTitle = '';
    if (formName && eventType === 'change') {
      // For form field changes: "Name changed 'Field Name' in the Form Name"
      descriptiveTitle = `${name} ${action} "${truncatedText}" in the ${formName}`;
    } else if (itemType && !alreadyHasType) {
      descriptiveTitle = `${name} ${action} "${truncatedText}" ${itemType}`;
    } else {
      descriptiveTitle = `${name} ${action} "${truncatedText}"`;
    }
    // Use descriptive title format (data-journey-title is used as element text, not complete override)
    queue.push({
      type: activityType,
      title: descriptiveTitle,
      timestamp: Date.now(),
      metadata: { url: el.href || window.location.href, pageTitle: document.title },
      memberName: name,
      memberEmail: email
    });
    clearTimeout(timeout);
    timeout = setTimeout(saveActivities, CONFIG.LOG_DEBOUNCE);
    refreshTimeline();
  }
  async function saveActivities() {
    if (queue.length === 0 || !memberstack || !currentMember) return;
    if (CONFIG.DATA_TABLE_NAME && memberstack?.createDataRecord) {
      const email = getMemberEmail(currentMember);
      const name = getMemberName(currentMember);
      for (const a of queue) {
        try {
          await memberstack.createDataRecord({
            table: CONFIG.DATA_TABLE_NAME,
            data: {
              memberid: currentMember.id || currentMember._id || '',
              membername: name,
              memberemail: email,
              activitytype: a.type,
              activitytitle: a.title,
              activitytimestamp: a.timestamp,
              activitymetadata: JSON.stringify(a.metadata || {})
            }
          });
        } catch (e) { log('Data Table save error: ' + e.message); }
      }
    }
    queue = [];
  }
  async function refreshTimeline() {
    const acts = await getAllActivities();
    displayTimeline(acts);
  }
  // CUSTOMIZE: Modify display properties (change 'flex' to 'block', 'grid', etc. based on your CSS)
  function displayTimeline(activities) {
    const container = document.querySelector('[data-ms-code="journey-timeline-container"]');
    const template = document.querySelector('[data-ms-code="journey-item-template"]');
    const emptyState = document.querySelector('[data-ms-code="journey-empty-state"]');
    if (!container || !template) return;
    if (emptyState) emptyState.style.display = 'none';
    container.querySelectorAll('[data-ms-code="journey-item"]').forEach(el => el.remove());
    template.style.display = 'none';
    container.style.display = '';
    if (!activities || activities.length === 0) {
      if (emptyState) emptyState.style.display = 'block';
      return;
    }
    activities.forEach(activity => {
      const item = template.cloneNode(true);
      item.removeAttribute('data-ms-template');
      item.setAttribute('data-ms-code', 'journey-item');
      item.style.display = 'flex'; // CUSTOMIZE: Change display type (flex, block, grid, etc.)
      const update = (sel, val) => { const el = item.querySelector(sel); if (el) el.textContent = val; };
      update('[data-ms-code="journey-activity-type"]', activity.type.charAt(0).toUpperCase() + activity.type.slice(1));
      update('[data-ms-code="journey-activity-title"]', activity.title);
      update('[data-ms-code="journey-activity-date"]', formatDate(activity.timestamp));
      container.appendChild(item);
    });
  }
  function handleVideoEvent(e, eventName) {
    const videoEl = e.target.tagName === 'VIDEO' || e.target.tagName === 'AUDIO' ? e.target : null;
    const trackedEl = videoEl ? videoEl.closest('[data-ms-code="journey-track"]') : e.target.closest('[data-ms-code="journey-track"]');
    if (trackedEl) {
      const actualVideo = (trackedEl.tagName === 'VIDEO' || trackedEl.tagName === 'AUDIO') ? trackedEl : trackedEl.querySelector('video, audio');
      if (actualVideo || trackedEl.hasAttribute('data-journey-event')) {
        const eventType = trackedEl.getAttribute('data-journey-event');
        if (!eventType || eventType === eventName) logActivity(trackedEl, 'video');
      }
    }
  }
  document.addEventListener('click', (e) => {
    // Don't log click events if form is currently being submitted (prevents duplicate logging)
    if (isSubmitting) return;
    // Don't log click events on submit buttons - they're handled by the submit event
    const isSubmitButton = e.target.type === 'submit' || 
                           (e.target.tagName === 'BUTTON' && e.target.type === 'submit') ||
                           (e.target.tagName === 'INPUT' && e.target.type === 'submit');
    if (isSubmitButton) {
      // Set flag immediately to prevent any other events during submission
      isSubmitting = true;
      setTimeout(() => { isSubmitting = false; }, 1000);
      return;
    }
    const el = e.target.closest('[data-ms-code="journey-track"]');
    if (el) {
      // Don't log click on form element if it's a form (submit event handles that)
      if (el.tagName === 'FORM') {
        return;
      }
      const eventType = el.getAttribute('data-journey-event');
      if (!eventType || eventType === 'click') {
        logActivity(el, el.getAttribute('data-journey-type') || (el.tagName === 'A' ? 'link' : 'click'));
      }
    }
  }, true);
  document.addEventListener('submit', (e) => {
    // Set flag to prevent change and click events from logging during form submission
    isSubmitting = true;
    const form = e.target;
    let trackedEl = form.hasAttribute('data-ms-code') && form.getAttribute('data-ms-code') === 'journey-track' ? form : null;
    if (!trackedEl && e.submitter) {
      trackedEl = e.submitter.hasAttribute('data-ms-code') && e.submitter.getAttribute('data-ms-code') === 'journey-track' ? e.submitter : e.submitter.closest('[data-ms-code="journey-track"]');
    }
    if (!trackedEl) {
      const btn = form.querySelector('button[type="submit"], input[type="submit"]');
      if (btn) trackedEl = btn.hasAttribute('data-ms-code') && btn.getAttribute('data-ms-code') === 'journey-track' ? btn : btn.closest('[data-ms-code="journey-track"]');
    }
    if (trackedEl) {
      const eventType = trackedEl.getAttribute('data-journey-event');
      if (!eventType || eventType === 'submit') logActivity(trackedEl, 'form');
    }
    // Reset flag after a short delay to allow form submission to complete
    setTimeout(() => { isSubmitting = false; }, 1000);
  }, true);
  document.addEventListener('play', (e) => handleVideoEvent(e, 'play'), true);
  document.addEventListener('pause', (e) => { const el = e.target.closest('[data-ms-code="journey-track"]'); if (el && el.getAttribute('data-journey-event') === 'pause') logActivity(el, 'video'); }, true);
  document.addEventListener('ended', (e) => { const el = e.target.closest('[data-ms-code="journey-track"]'); if (el && el.getAttribute('data-journey-event') === 'ended') logActivity(el, 'video'); }, true);
  document.addEventListener('change', (e) => {
    // Don't log change events if form is currently being submitted
    if (isSubmitting) return;
    const el = e.target.closest('[data-ms-code="journey-track"]');
    if (el && el.getAttribute('data-journey-event') === 'change') logActivity(el, 'form');
  }, true);
  if ('IntersectionObserver' in window) {
    // CUSTOMIZE: Change threshold (0.0-1.0) - 0.5 = element must be 50% visible to trigger
    const viewObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const el = entry.target;
          const eventType = el.getAttribute('data-journey-event');
          if (eventType === 'view' || eventType === 'visible') {
            logActivity(el, 'view');
            viewObserver.unobserve(el);
          }
        }
      });
    }, { threshold: 0.5 });
    function observeViewElements() {
      document.querySelectorAll('[data-ms-code="journey-track"][data-journey-event="view"]:not([data-journey-observed]), [data-ms-code="journey-track"][data-journey-event="visible"]:not([data-journey-observed])').forEach(el => {
        el.setAttribute('data-journey-observed', 'true');
        viewObserver.observe(el);
      });
    }
    observeViewElements();
    if ('MutationObserver' in window) {
      new MutationObserver(observeViewElements).observe(document.body, { childList: true, subtree: true });
    }
  }
  document.addEventListener('journey-track', (e) => {
    const el = e.target;
    if (el && el.hasAttribute('data-ms-code') && el.getAttribute('data-ms-code') === 'journey-track') {
      logActivity(el, el.getAttribute('data-journey-type') || 'custom');
    }
  }, true);
  (async function() {
    await waitForMS();
    memberstack = window.$memberstackDom;
    if (!memberstack) { log('Memberstack not found'); return; }
    try {
      const m = await memberstack.getCurrentMember();
      currentMember = m?.data || m;
      if (!currentMember) { log('No member found'); return; }
      const acts = await getAllActivities();
      displayTimeline(acts);
    } catch (e) { log('Init error: ' + e.message); }
  })();
})();
</script>

v0.1


<!-- 💙 MEMBERSCRIPT #193 v0.1 💙 - MEMBER ACTIVITY TRACKER -->
<script>
(function() {
  'use strict';
  // CUSTOMIZE: Configuration options
  const CONFIG = {
    DATA_TABLE_NAME: 'memberactivities', // Change to match your Memberstack Data Table name
    DATE_FORMAT: { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }, // Customize date format
    LOG_DEBOUNCE: 500, // Delay in ms before saving activities (reduce API calls)
    MAX_ACTIVITIES: 100 // Maximum activities to fetch and display
  };
  let memberstack = null, currentMember = null, queue = [], timeout = null, isSubmitting = false;
  function log(msg) { console.log('MS#193:', msg); }
  function waitForMS() {
    return new Promise(r => {
      if (window.$memberstackDom) return r();
      const t = setTimeout(r, 3000);
      document.addEventListener('memberstack.ready', () => { clearTimeout(t); r(); }, { once: true });
    });
  }
  async function getAllActivities() {
    const acts = [];
    if (CONFIG.DATA_TABLE_NAME && memberstack?.queryDataRecords) {
      try {
        const result = await memberstack.queryDataRecords({
          table: CONFIG.DATA_TABLE_NAME,
          query: { orderBy: { activitytimestamp: 'desc' }, take: CONFIG.MAX_ACTIVITIES }
        });
        if ('records' in (result.data || {})) {
          (result.data.records || []).forEach(rec => {
            const d = rec?.data;
            if (!d) return;
            acts.push({
              type: d.activitytype || 'activity',
              title: d.activitytitle || 'Activity',
              timestamp: d.activitytimestamp || (rec.createdAt ? new Date(rec.createdAt).getTime() : Date.now()),
              memberName: d.membername || 'Guest',
              memberEmail: d.memberemail || '',
              metadata: d.activitymetadata ? (typeof d.activitymetadata === 'string' ? JSON.parse(d.activitymetadata) : d.activitymetadata) : {}
            });
          });
        }
      } catch (e) { log('Data Table fetch failed: ' + e.message); }
    }
    return [...acts, ...queue].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
  }
  // CUSTOMIZE: Change 'en-US' to your locale (e.g., 'en-GB', 'fr-FR', 'de-DE')
  function formatDate(ts) {
    const d = new Date(ts);
    return isNaN(d.getTime()) ? 'Unknown' : d.toLocaleDateString('en-US', CONFIG.DATE_FORMAT);
  }
  function getMemberEmail(m) { return m?.auth?.email || m?.email || ''; }
  // CUSTOMIZE: Modify if your member data structure stores names differently
  function getMemberName(m) {
    if (!m) return 'Guest';
    const fn = m?.customFields?.['first-name'] || m?.customFields?.firstName || m?.data?.customFields?.['first-name'] || m?.data?.customFields?.firstName || m?.firstName || m?.data?.firstName;
    const ln = m?.customFields?.['last-name'] || m?.customFields?.lastName || m?.data?.customFields?.['last-name'] || m?.data?.customFields?.lastName || m?.lastName || m?.data?.lastName;
    const full = m?.customFields?.name || m?.data?.customFields?.name || m?.name || m?.data?.name;
    if (fn && ln) return `${fn} ${ln}`.trim();
    return fn || full || 'Guest';
  }
  function getElementText(el, activityType) {
    if (activityType === 'form' || el.tagName === 'FORM') {
      // For forms, prioritize data-name and aria-label which usually have the full form name
      return el.getAttribute('data-journey-title') || el.getAttribute('data-name') || el.getAttribute('aria-label') || el.getAttribute('name') || el.getAttribute('id') || el.title || 'form';
    }
    if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') {
      return el.getAttribute('data-journey-title') || el.getAttribute('data-name') || el.getAttribute('aria-label') || el.getAttribute('name') || el.getAttribute('id') || el.getAttribute('placeholder') || el.getAttribute('title') || (el.id ? document.querySelector(`label[for="${el.id}"]`)?.textContent?.trim() : null) || el.tagName.toLowerCase();
    }
    if (activityType === 'video' || el.tagName === 'VIDEO' || el.tagName === 'AUDIO') {
      let txt = el.getAttribute('data-journey-title') || el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('alt');
      if (!txt) {
        const container = el.closest('[data-ms-code="journey-track"]') || el;
        txt = container.querySelector('h1, h2, h3, h4, h5, h6')?.textContent?.trim();
        if (!txt) {
          let p = container.parentElement;
          while (p && !txt) { txt = p.querySelector('h1, h2, h3, h4, h5, h6')?.textContent?.trim(); p = p.parentElement; }
        }
        if (!txt) txt = container.querySelector('video[title], audio[title]')?.getAttribute('title') || 'video';
      }
      return txt;
    }
    return el.getAttribute('data-journey-title') || el.textContent?.trim() || el.getAttribute('aria-label') || el.title || el.getAttribute('alt') || 'item';
  }
  async function logActivity(el, type) {
    if (!memberstack) return;
    if (!currentMember) {
      try { const m = await memberstack.getCurrentMember(); currentMember = m?.data || m; } catch (e) { return; }
    }
    if (!currentMember) return;
    const email = getMemberEmail(currentMember);
    const name = getMemberName(currentMember);
    const eventType = el.getAttribute('data-journey-event');
    const activityType = el.getAttribute('data-journey-type') || type || 'click';
    const elementText = getElementText(el, activityType);
    const truncatedText = elementText.length > 50 ? elementText.substring(0, 50) + '...' : elementText; // CUSTOMIZE: Change 50 to adjust max text length
    // CUSTOMIZE: Modify action verbs to change how activities are described
    const eventActions = { 'play': 'played', 'pause': 'paused', 'ended': 'finished watching', 'change': 'changed', 'view': 'viewed', 'visible': 'viewed', 'submit': 'submitted', 'click': 'clicked on' };
    const typeActions = { 'link': 'clicked on', 'click': 'clicked on', 'form': 'submitted', 'button': 'clicked', 'video': 'watched', 'download': 'downloaded', 'view': 'viewed', 'custom': 'interacted with' };
    const action = eventActions[eventType] || typeActions[activityType] || 'interacted with';
    let itemType = '';
    if (el.tagName === 'A' || activityType === 'link') itemType = 'link';
    else if (el.tagName === 'BUTTON' || activityType === 'button') itemType = 'button';
    else if (el.tagName === 'FORM' || activityType === 'form') itemType = 'form';
    else if (el.tagName === 'VIDEO' || el.tagName === 'AUDIO' || activityType === 'video') itemType = 'video';
    else if (activityType === 'download') itemType = 'file';
    // Don't add 'page' for view events - just show "viewed 'text'"
    // For form field changes, get the parent form name
    let formName = '';
    if (eventType === 'change' && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')) {
      const parentForm = el.closest('form');
      if (parentForm) {
        // Get form name, prioritizing data-name and aria-label which usually have the full name
        formName = parentForm.getAttribute('data-name') || 
                   parentForm.getAttribute('aria-label') || 
                   parentForm.getAttribute('data-journey-title') ||
                   parentForm.getAttribute('name') || 
                   parentForm.getAttribute('id') || 
                   parentForm.title || 
                   'form';
        // Clean up form name - format nicely but keep "Form" if it's part of the name
        const formNameLower = formName.toLowerCase();
        // Only remove "form" suffix if it's a technical identifier (like "email-form" or "email_form")
        // Don't remove it if it's a proper name like "Email Form"
        if (formNameLower.endsWith('-form') || formNameLower.endsWith('_form')) {
          formName = formName.substring(0, formName.length - 5).trim();
          // Capitalize first letter of each word after removing technical suffix
          if (formName && formName.length > 0) {
            formName = formName.split(/[\s-]+/).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ') + ' Form';
          }
        } else {
          // Capitalize first letter of each word for better readability, keeping original structure
          if (formName && formName.length > 0) {
            formName = formName.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
          }
        }
      }
    }
    // Don't add itemType if elementText already contains it (e.g., "Email Form" already has "form")
    const textLower = truncatedText.toLowerCase();
    const typeLower = itemType.toLowerCase();
    const alreadyHasType = itemType && (textLower.endsWith(' ' + typeLower) || textLower.endsWith(typeLower) || textLower.includes(' ' + typeLower + ' '));
    // Build descriptive title
    let descriptiveTitle = '';
    if (formName && eventType === 'change') {
      // For form field changes: "Name changed 'Field Name' in the Form Name"
      descriptiveTitle = `${name} ${action} "${truncatedText}" in the ${formName}`;
    } else if (itemType && !alreadyHasType) {
      descriptiveTitle = `${name} ${action} "${truncatedText}" ${itemType}`;
    } else {
      descriptiveTitle = `${name} ${action} "${truncatedText}"`;
    }
    // Use descriptive title format (data-journey-title is used as element text, not complete override)
    queue.push({
      type: activityType,
      title: descriptiveTitle,
      timestamp: Date.now(),
      metadata: { url: el.href || window.location.href, pageTitle: document.title },
      memberName: name,
      memberEmail: email
    });
    clearTimeout(timeout);
    timeout = setTimeout(saveActivities, CONFIG.LOG_DEBOUNCE);
    refreshTimeline();
  }
  async function saveActivities() {
    if (queue.length === 0 || !memberstack || !currentMember) return;
    if (CONFIG.DATA_TABLE_NAME && memberstack?.createDataRecord) {
      const email = getMemberEmail(currentMember);
      const name = getMemberName(currentMember);
      for (const a of queue) {
        try {
          await memberstack.createDataRecord({
            table: CONFIG.DATA_TABLE_NAME,
            data: {
              memberid: currentMember.id || currentMember._id || '',
              membername: name,
              memberemail: email,
              activitytype: a.type,
              activitytitle: a.title,
              activitytimestamp: a.timestamp,
              activitymetadata: JSON.stringify(a.metadata || {})
            }
          });
        } catch (e) { log('Data Table save error: ' + e.message); }
      }
    }
    queue = [];
  }
  async function refreshTimeline() {
    const acts = await getAllActivities();
    displayTimeline(acts);
  }
  // CUSTOMIZE: Modify display properties (change 'flex' to 'block', 'grid', etc. based on your CSS)
  function displayTimeline(activities) {
    const container = document.querySelector('[data-ms-code="journey-timeline-container"]');
    const template = document.querySelector('[data-ms-code="journey-item-template"]');
    const emptyState = document.querySelector('[data-ms-code="journey-empty-state"]');
    if (!container || !template) return;
    if (emptyState) emptyState.style.display = 'none';
    container.querySelectorAll('[data-ms-code="journey-item"]').forEach(el => el.remove());
    template.style.display = 'none';
    container.style.display = '';
    if (!activities || activities.length === 0) {
      if (emptyState) emptyState.style.display = 'block';
      return;
    }
    activities.forEach(activity => {
      const item = template.cloneNode(true);
      item.removeAttribute('data-ms-template');
      item.setAttribute('data-ms-code', 'journey-item');
      item.style.display = 'flex'; // CUSTOMIZE: Change display type (flex, block, grid, etc.)
      const update = (sel, val) => { const el = item.querySelector(sel); if (el) el.textContent = val; };
      update('[data-ms-code="journey-activity-type"]', activity.type.charAt(0).toUpperCase() + activity.type.slice(1));
      update('[data-ms-code="journey-activity-title"]', activity.title);
      update('[data-ms-code="journey-activity-date"]', formatDate(activity.timestamp));
      container.appendChild(item);
    });
  }
  function handleVideoEvent(e, eventName) {
    const videoEl = e.target.tagName === 'VIDEO' || e.target.tagName === 'AUDIO' ? e.target : null;
    const trackedEl = videoEl ? videoEl.closest('[data-ms-code="journey-track"]') : e.target.closest('[data-ms-code="journey-track"]');
    if (trackedEl) {
      const actualVideo = (trackedEl.tagName === 'VIDEO' || trackedEl.tagName === 'AUDIO') ? trackedEl : trackedEl.querySelector('video, audio');
      if (actualVideo || trackedEl.hasAttribute('data-journey-event')) {
        const eventType = trackedEl.getAttribute('data-journey-event');
        if (!eventType || eventType === eventName) logActivity(trackedEl, 'video');
      }
    }
  }
  document.addEventListener('click', (e) => {
    // Don't log click events if form is currently being submitted (prevents duplicate logging)
    if (isSubmitting) return;
    // Don't log click events on submit buttons - they're handled by the submit event
    const isSubmitButton = e.target.type === 'submit' || 
                           (e.target.tagName === 'BUTTON' && e.target.type === 'submit') ||
                           (e.target.tagName === 'INPUT' && e.target.type === 'submit');
    if (isSubmitButton) {
      // Set flag immediately to prevent any other events during submission
      isSubmitting = true;
      setTimeout(() => { isSubmitting = false; }, 1000);
      return;
    }
    const el = e.target.closest('[data-ms-code="journey-track"]');
    if (el) {
      // Don't log click on form element if it's a form (submit event handles that)
      if (el.tagName === 'FORM') {
        return;
      }
      const eventType = el.getAttribute('data-journey-event');
      if (!eventType || eventType === 'click') {
        logActivity(el, el.getAttribute('data-journey-type') || (el.tagName === 'A' ? 'link' : 'click'));
      }
    }
  }, true);
  document.addEventListener('submit', (e) => {
    // Set flag to prevent change and click events from logging during form submission
    isSubmitting = true;
    const form = e.target;
    let trackedEl = form.hasAttribute('data-ms-code') && form.getAttribute('data-ms-code') === 'journey-track' ? form : null;
    if (!trackedEl && e.submitter) {
      trackedEl = e.submitter.hasAttribute('data-ms-code') && e.submitter.getAttribute('data-ms-code') === 'journey-track' ? e.submitter : e.submitter.closest('[data-ms-code="journey-track"]');
    }
    if (!trackedEl) {
      const btn = form.querySelector('button[type="submit"], input[type="submit"]');
      if (btn) trackedEl = btn.hasAttribute('data-ms-code') && btn.getAttribute('data-ms-code') === 'journey-track' ? btn : btn.closest('[data-ms-code="journey-track"]');
    }
    if (trackedEl) {
      const eventType = trackedEl.getAttribute('data-journey-event');
      if (!eventType || eventType === 'submit') logActivity(trackedEl, 'form');
    }
    // Reset flag after a short delay to allow form submission to complete
    setTimeout(() => { isSubmitting = false; }, 1000);
  }, true);
  document.addEventListener('play', (e) => handleVideoEvent(e, 'play'), true);
  document.addEventListener('pause', (e) => { const el = e.target.closest('[data-ms-code="journey-track"]'); if (el && el.getAttribute('data-journey-event') === 'pause') logActivity(el, 'video'); }, true);
  document.addEventListener('ended', (e) => { const el = e.target.closest('[data-ms-code="journey-track"]'); if (el && el.getAttribute('data-journey-event') === 'ended') logActivity(el, 'video'); }, true);
  document.addEventListener('change', (e) => {
    // Don't log change events if form is currently being submitted
    if (isSubmitting) return;
    const el = e.target.closest('[data-ms-code="journey-track"]');
    if (el && el.getAttribute('data-journey-event') === 'change') logActivity(el, 'form');
  }, true);
  if ('IntersectionObserver' in window) {
    // CUSTOMIZE: Change threshold (0.0-1.0) - 0.5 = element must be 50% visible to trigger
    const viewObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const el = entry.target;
          const eventType = el.getAttribute('data-journey-event');
          if (eventType === 'view' || eventType === 'visible') {
            logActivity(el, 'view');
            viewObserver.unobserve(el);
          }
        }
      });
    }, { threshold: 0.5 });
    function observeViewElements() {
      document.querySelectorAll('[data-ms-code="journey-track"][data-journey-event="view"]:not([data-journey-observed]), [data-ms-code="journey-track"][data-journey-event="visible"]:not([data-journey-observed])').forEach(el => {
        el.setAttribute('data-journey-observed', 'true');
        viewObserver.observe(el);
      });
    }
    observeViewElements();
    if ('MutationObserver' in window) {
      new MutationObserver(observeViewElements).observe(document.body, { childList: true, subtree: true });
    }
  }
  document.addEventListener('journey-track', (e) => {
    const el = e.target;
    if (el && el.hasAttribute('data-ms-code') && el.getAttribute('data-ms-code') === 'journey-track') {
      logActivity(el, el.getAttribute('data-journey-type') || 'custom');
    }
  }, true);
  (async function() {
    await waitForMS();
    memberstack = window.$memberstackDom;
    if (!memberstack) { log('Memberstack not found'); return; }
    try {
      const m = await memberstack.getCurrentMember();
      currentMember = m?.data || m;
      if (!currentMember) { log('No member found'); return; }
      const acts = await getAllActivities();
      displayTimeline(acts);
    } catch (e) { log('Init error: ' + e.message); }
  })();
})();
</script>

Ansicht Memberscript
UX

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

v0.1


<!-- 💙 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>

Ansicht Memberscript
UX
Erreichbarkeit

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

v0.1


<!-- 💙 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>

Ansicht Memberscript
Integration

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

v0.1


<!-- 💙 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>

Ansicht Memberscript
UX
JSON

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

v0.1


<!-- 💙 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>

Ansicht Memberscript
Bedingte Sichtbarkeit
UX

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

v0.1


<!-- 💙 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>

Ansicht Memberscript
UX

#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 no member is logged in, do nothing
      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 no plan connections, do nothing
      if (!planConnections || planConnections.length === 0) {
        return;
      }
      
      // Get the current plan connection details
      const planConnection = planConnections[0];
      const currentPlanId = planConnection?.planId;
      
      // Get the price ID from the payment object
      const currentPriceId = planConnection?.payment?.priceId;
      
      if (!currentPlanId) {
        return;
      }
      
      // Find all buttons with Memberstack plan attributes
      // Look for data-ms-price:update, data-ms-price:add, data-ms-plan:update, or data-ms-plan:add attributes
      const buttons = document.querySelectorAll(
        '[data-ms-price\\:update], [data-ms-price\\:add], [data-ms-plan\\:update], [data-ms-plan\\:add]'
      );
      
      // Update matching button to say "Current Plan"
      buttons.forEach(button => {
        // Get the plan ID from data-ms-price:update, data-ms-price:add, data-ms-plan:update, or data-ms-plan:add
        const planIdFromPriceUpdate = button.getAttribute('data-ms-price:update');
        const planIdFromPriceAdd = button.getAttribute('data-ms-price:add');
        const planIdFromPlanUpdate = button.getAttribute('data-ms-plan:update');
        const planIdFromPlanAdd = button.getAttribute('data-ms-plan:add');
        const buttonPlanId = planIdFromPriceUpdate || planIdFromPriceAdd || planIdFromPlanUpdate || planIdFromPlanAdd;
        
        // Check if this button's price ID matches the current price ID
        // OR if it matches the plan ID (covering both scenarios)
        if (buttonPlanId === currentPriceId || buttonPlanId === currentPlanId) {
          
          // Update button text
          button.textContent = 'Current Plan';
          
          // Disable the button to prevent clicking
          if (button.disabled !== undefined) {
            button.disabled = true;
          }
          
          // Add a class for styling (optional)
          button.classList.add('current-plan-button');
          
          // Update href to prevent navigation
          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;');
          }
          
          // Add style attributes for immediate visual feedback
          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);
        // Fallback timeout
        setTimeout(resolve, 2000);
      }
    });
  }
})();
</script>


v0.1


<!-- 💙 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 no member is logged in, do nothing
      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 no plan connections, do nothing
      if (!planConnections || planConnections.length === 0) {
        return;
      }
      
      // Get the current plan connection details
      const planConnection = planConnections[0];
      const currentPlanId = planConnection?.planId;
      
      // Get the price ID from the payment object
      const currentPriceId = planConnection?.payment?.priceId;
      
      if (!currentPlanId) {
        return;
      }
      
      // Find all buttons with Memberstack plan attributes
      // Look for data-ms-price:update, data-ms-price:add, data-ms-plan:update, or data-ms-plan:add attributes
      const buttons = document.querySelectorAll(
        '[data-ms-price\\:update], [data-ms-price\\:add], [data-ms-plan\\:update], [data-ms-plan\\:add]'
      );
      
      // Update matching button to say "Current Plan"
      buttons.forEach(button => {
        // Get the plan ID from data-ms-price:update, data-ms-price:add, data-ms-plan:update, or data-ms-plan:add
        const planIdFromPriceUpdate = button.getAttribute('data-ms-price:update');
        const planIdFromPriceAdd = button.getAttribute('data-ms-price:add');
        const planIdFromPlanUpdate = button.getAttribute('data-ms-plan:update');
        const planIdFromPlanAdd = button.getAttribute('data-ms-plan:add');
        const buttonPlanId = planIdFromPriceUpdate || planIdFromPriceAdd || planIdFromPlanUpdate || planIdFromPlanAdd;
        
        // Check if this button's price ID matches the current price ID
        // OR if it matches the plan ID (covering both scenarios)
        if (buttonPlanId === currentPriceId || buttonPlanId === currentPlanId) {
          
          // Update button text
          button.textContent = 'Current Plan';
          
          // Disable the button to prevent clicking
          if (button.disabled !== undefined) {
            button.disabled = true;
          }
          
          // Add a class for styling (optional)
          button.classList.add('current-plan-button');
          
          // Update href to prevent navigation
          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;');
          }
          
          // Add style attributes for immediate visual feedback
          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);
        // Fallback timeout
        setTimeout(resolve, 2000);
      }
    });
  }
})();
</script>


Ansicht Memberscript
UX

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

v0.1


<!-- 💙 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>

Ansicht Memberscript
JSON
UX

#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>
v0.1

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>
Ansicht Memberscript
Bedingte Sichtbarkeit
JSON
UX

#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>
v0.1

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>
Ansicht Memberscript
UX

#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>
v0.1

<!-- 💙 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>
Ansicht Memberscript
UX
Bedingte Sichtbarkeit

#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>
v0.1

<!-- 💙 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>
Ansicht Memberscript
Integration
UX

#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>
v0.1

<!-- 💙 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>
Ansicht Memberscript
Benutzerdefinierte Abläufe

#180 - Allow Members to Edit CMS Blog Content

Let members edit live CMS blog posts directly on your Webflow site.

Kopf-Code

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>

Code der Einrichtung

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>
v0.1

Kopf-Code

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>

Code der Einrichtung

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>
Ansicht Memberscript
UX

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

Kopf-Code

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>

Code der Einrichtung

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>
v0.1

Kopf-Code

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>

Code der Einrichtung

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>
Ansicht Memberscript
UX
Bedingte Sichtbarkeit

#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>
v0.1

<!-- 💙 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>
Ansicht Memberscript
Benutzerdefinierte Felder
UX

#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>
v0.1

<!-- 💙 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>
Ansicht Memberscript
UX
Erreichbarkeit

#176 - Save & Display Last Used Auth Method

Displays a popup showing the last login method a member used to make logging in easier.

Kopf-Code

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>

Code der Einrichtung

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>
v0.1

Kopf-Code

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>

Code der Einrichtung

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>
Ansicht Memberscript
Wir konnten keine Skripte für diese Suche finden... bitte versuchen Sie es erneut.
Slack

Brauchen Sie Hilfe mit MemberScripts? Treten Sie unserer Slack-Community mit über 5.500 Mitgliedern bei! 🙌

MemberScripts sind eine Community-Ressource von Memberstack - wenn du Hilfe brauchst, damit sie mit deinem Projekt funktionieren, melde dich bitte im Memberstack 2.0 Slack an und bitte um Hilfe!

Unserem Slack beitreten
Schaukasten

Entdecken Sie echte Unternehmen, die mit Memberstack erfolgreich waren

Verlassen Sie sich nicht nur auf unser Wort - schauen Sie sich die Unternehmen aller Größen an, die sich auf Memberstack für ihre Authentifizierung und Zahlungen verlassen.

Alle Erfolgsgeschichten anzeigen
Auch Webflow verwendet Memberstack!
Mit dem Bau beginnen

Bauen Sie Ihre Träume

Memberstack ist 100% kostenlos, bis Sie bereit sind, zu starten - worauf warten Sie also noch? Erstellen Sie Ihre erste App und beginnen Sie noch heute mit der Entwicklung.