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.

JSON
Bedingte Sichtbarkeit

#168 - Save Trusted Devices

Save trusted devices to extend user sessions and reduce repeated logins on your sites.


<!-- 💙 MEMBERSCRIPT #168 v0.1 💙 - SAVE TRUSTED DEVICE -->
<script>
(function() {
    const TRUST_EXPIRY_DAYS = 90;
    const MAX_TRUSTED_DEVICES = 5;
    const EXTENDED_SESSION_DAYS = 30;
  
    function generateDeviceIdentifier() {
      let id = localStorage.getItem('ms_device_id');
      if (id) return id;
      const info = {
        ua: navigator.userAgent,
        w: screen.width,
        h: screen.height,
        tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
        plat: navigator.platform
      };
      id = btoa(JSON.stringify(info)).slice(0, 32);
      localStorage.setItem('ms_device_id', id);
      return id;
    }
  
    async function getTrustedDevices() {
      const ms = window.$memberstackDom;
      const memberJson = await ms.getMemberJSON();
      return Array.isArray(memberJson?.data?.trustedDevices) ? memberJson.data.trustedDevices : [];
    }
  
    async function saveTrustedDevices(devices) {
      const ms = window.$memberstackDom;
      const memberJson = await ms.getMemberJSON();
      memberJson.data = memberJson.data || {};
      memberJson.data.trustedDevices = devices;
      await ms.updateMemberJSON({ json: memberJson });
    }
  
    async function addTrustedDevice(id, name) {
      const now = new Date();
      const expires = new Date(now.getTime() + TRUST_EXPIRY_DAYS * 864e5).toISOString();
      const devices = await getTrustedDevices();
      const existing = devices.find(d => d.id === id);
  
      if (existing) {
        existing.trustedAt = now.toISOString();
        existing.expiresAt = expires;
      } else {
        if (devices.length >= MAX_TRUSTED_DEVICES) devices.shift();
        devices.push({
          id,
          trustedAt: now.toISOString(),
          expiresAt: expires,
          ua: navigator.userAgent.slice(0, 100),
          name: name
        });
      }
  
      await saveTrustedDevices(devices);
    }
  
    function getDeviceName() {
      const ua = navigator.userAgent;
      if (ua.includes('iPhone')) return 'iPhone';
      if (ua.includes('iPad')) return 'iPad';
      if (ua.includes('Android')) return 'Android';
      if (ua.includes('Mac')) return 'Mac';
      if (ua.includes('Windows')) return 'Windows';
      return 'Device';
    }
  
    function setExtendedSession() {
      const exp = new Date();
      exp.setDate(exp.getDate() + EXTENDED_SESSION_DAYS);
      document.cookie = `trustedDevice=true; expires=${exp.toUTCString()}; path=/; SameSite=Strict`;
    }
  
    function showNotice() {
      const el = document.querySelector('[data-ms-code="trust-device-notice"]');
      if (!el) return;
      el.style.display = 'block';
      sessionStorage.setItem('ms_new_device_detected', '1');
      sessionStorage.removeItem('ms_device_trusted');
    }
  
    function hideNotice() {
      const el = document.querySelector('[data-ms-code="trust-device-notice"]');
      if (el) el.style.display = 'none';
      sessionStorage.removeItem('ms_new_device_detected');
      sessionStorage.setItem('ms_device_trusted', '1');
    }
  
    function setupTrustBtn() {
      document.addEventListener('click', async e => {
        const btn = e.target.closest('[data-ms-code="trust-device-btn"]');
        if (!btn) return;
        e.preventDefault();
        btn.disabled = true;
        btn.innerText = 'Trusting Device...';
  
        const member = await window.$memberstackDom.getCurrentMember();
        if (!member?.data) {
          alert('Please log in first.');
          btn.disabled = false;
          btn.innerText = 'Trust This Device';
          return;
        }
  
        const id = generateDeviceIdentifier();
        const name = getDeviceName();
        await addTrustedDevice(id, name);
        setExtendedSession();
  
        btn.innerText = 'Device Trusted!';
        setTimeout(hideNotice, 1000);
      });
    }
  
    async function checkTrust() {
      const member = await window.$memberstackDom.getCurrentMember();
      if (!member) {
        hideNotice();
        return;
      }

      const id = generateDeviceIdentifier();
      const devices = await getTrustedDevices();
      
      // Check if current device is trusted
      const trusted = devices.some(d => {
        // Check if device ID matches and hasn't expired
        if (d.id === id && new Date(d.expiresAt) > new Date()) {
          return true;
        }
        // Also check by user agent for better matching
        if (d.ua && d.ua.includes(navigator.userAgent.slice(0, 50)) && new Date(d.expiresAt) > new Date()) {
          return true;
        }
        return false;
      });

      if (trusted) {
        hideNotice();
        setExtendedSession();
        // Also store in sessionStorage to prevent showing on refresh
        sessionStorage.setItem('ms_device_trusted', '1');
      } else {
        // Check if we already showed the notice in this session or have the cookie
        if (sessionStorage.getItem('ms_device_trusted') === '1' || 
            document.cookie.includes('trustedDevice=true')) {
          hideNotice();
        } else {
          showNotice();
        }
      }
    }
  
    function preventRedirect() {
      window.addEventListener('ms:member:will-redirect', e => {
        if (sessionStorage.getItem('ms_new_device_detected') === '1') {
          e.preventDefault();
        }
      });
    }
  
    function init() {
      // Immediately hide notice if device is already trusted in this session
      if (sessionStorage.getItem('ms_device_trusted') === '1' || 
          document.cookie.includes('trustedDevice=true')) {
        hideNotice();
      }
      
      if (window.$memberstackDom?.getCurrentMember) {
        setupTrustBtn();
        preventRedirect();
        window.addEventListener('ms:member:login', () => setTimeout(checkTrust, 1000));
        window.addEventListener('ms:member:info-changed', checkTrust);
        checkTrust();
      } else {
        setTimeout(init, 500);
      }
    }
  
    document.addEventListener('DOMContentLoaded', init);
  })();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #168 v0.1 💙 - SAVE TRUSTED DEVICE -->
<script>
(function() {
    const TRUST_EXPIRY_DAYS = 90;
    const MAX_TRUSTED_DEVICES = 5;
    const EXTENDED_SESSION_DAYS = 30;
  
    function generateDeviceIdentifier() {
      let id = localStorage.getItem('ms_device_id');
      if (id) return id;
      const info = {
        ua: navigator.userAgent,
        w: screen.width,
        h: screen.height,
        tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
        plat: navigator.platform
      };
      id = btoa(JSON.stringify(info)).slice(0, 32);
      localStorage.setItem('ms_device_id', id);
      return id;
    }
  
    async function getTrustedDevices() {
      const ms = window.$memberstackDom;
      const memberJson = await ms.getMemberJSON();
      return Array.isArray(memberJson?.data?.trustedDevices) ? memberJson.data.trustedDevices : [];
    }
  
    async function saveTrustedDevices(devices) {
      const ms = window.$memberstackDom;
      const memberJson = await ms.getMemberJSON();
      memberJson.data = memberJson.data || {};
      memberJson.data.trustedDevices = devices;
      await ms.updateMemberJSON({ json: memberJson });
    }
  
    async function addTrustedDevice(id, name) {
      const now = new Date();
      const expires = new Date(now.getTime() + TRUST_EXPIRY_DAYS * 864e5).toISOString();
      const devices = await getTrustedDevices();
      const existing = devices.find(d => d.id === id);
  
      if (existing) {
        existing.trustedAt = now.toISOString();
        existing.expiresAt = expires;
      } else {
        if (devices.length >= MAX_TRUSTED_DEVICES) devices.shift();
        devices.push({
          id,
          trustedAt: now.toISOString(),
          expiresAt: expires,
          ua: navigator.userAgent.slice(0, 100),
          name: name
        });
      }
  
      await saveTrustedDevices(devices);
    }
  
    function getDeviceName() {
      const ua = navigator.userAgent;
      if (ua.includes('iPhone')) return 'iPhone';
      if (ua.includes('iPad')) return 'iPad';
      if (ua.includes('Android')) return 'Android';
      if (ua.includes('Mac')) return 'Mac';
      if (ua.includes('Windows')) return 'Windows';
      return 'Device';
    }
  
    function setExtendedSession() {
      const exp = new Date();
      exp.setDate(exp.getDate() + EXTENDED_SESSION_DAYS);
      document.cookie = `trustedDevice=true; expires=${exp.toUTCString()}; path=/; SameSite=Strict`;
    }
  
    function showNotice() {
      const el = document.querySelector('[data-ms-code="trust-device-notice"]');
      if (!el) return;
      el.style.display = 'block';
      sessionStorage.setItem('ms_new_device_detected', '1');
      sessionStorage.removeItem('ms_device_trusted');
    }
  
    function hideNotice() {
      const el = document.querySelector('[data-ms-code="trust-device-notice"]');
      if (el) el.style.display = 'none';
      sessionStorage.removeItem('ms_new_device_detected');
      sessionStorage.setItem('ms_device_trusted', '1');
    }
  
    function setupTrustBtn() {
      document.addEventListener('click', async e => {
        const btn = e.target.closest('[data-ms-code="trust-device-btn"]');
        if (!btn) return;
        e.preventDefault();
        btn.disabled = true;
        btn.innerText = 'Trusting Device...';
  
        const member = await window.$memberstackDom.getCurrentMember();
        if (!member?.data) {
          alert('Please log in first.');
          btn.disabled = false;
          btn.innerText = 'Trust This Device';
          return;
        }
  
        const id = generateDeviceIdentifier();
        const name = getDeviceName();
        await addTrustedDevice(id, name);
        setExtendedSession();
  
        btn.innerText = 'Device Trusted!';
        setTimeout(hideNotice, 1000);
      });
    }
  
    async function checkTrust() {
      const member = await window.$memberstackDom.getCurrentMember();
      if (!member) {
        hideNotice();
        return;
      }

      const id = generateDeviceIdentifier();
      const devices = await getTrustedDevices();
      
      // Check if current device is trusted
      const trusted = devices.some(d => {
        // Check if device ID matches and hasn't expired
        if (d.id === id && new Date(d.expiresAt) > new Date()) {
          return true;
        }
        // Also check by user agent for better matching
        if (d.ua && d.ua.includes(navigator.userAgent.slice(0, 50)) && new Date(d.expiresAt) > new Date()) {
          return true;
        }
        return false;
      });

      if (trusted) {
        hideNotice();
        setExtendedSession();
        // Also store in sessionStorage to prevent showing on refresh
        sessionStorage.setItem('ms_device_trusted', '1');
      } else {
        // Check if we already showed the notice in this session or have the cookie
        if (sessionStorage.getItem('ms_device_trusted') === '1' || 
            document.cookie.includes('trustedDevice=true')) {
          hideNotice();
        } else {
          showNotice();
        }
      }
    }
  
    function preventRedirect() {
      window.addEventListener('ms:member:will-redirect', e => {
        if (sessionStorage.getItem('ms_new_device_detected') === '1') {
          e.preventDefault();
        }
      });
    }
  
    function init() {
      // Immediately hide notice if device is already trusted in this session
      if (sessionStorage.getItem('ms_device_trusted') === '1' || 
          document.cookie.includes('trustedDevice=true')) {
        hideNotice();
      }
      
      if (window.$memberstackDom?.getCurrentMember) {
        setupTrustBtn();
        preventRedirect();
        window.addEventListener('ms:member:login', () => setTimeout(checkTrust, 1000));
        window.addEventListener('ms:member:info-changed', checkTrust);
        checkTrust();
      } else {
        setTimeout(init, 500);
      }
    }
  
    document.addEventListener('DOMContentLoaded', init);
  })();
</script>
Ansicht Memberscript
Sicherheit

#167 - Login Form Throttle With Security Check

Limit failed login attempts and trigger a timed security check to prevent brute force attacks.


<!-- 💙 MEMBERSCRIPT #167 v0.1 💙 - LOGIN THROTTLE WITH SECURITY CHECK -->
<script>
(function() {
  const MAX_ATTEMPTS = 3;
  const STORAGE_KEY = 'ms_login_attempts';
  const SECURITY_DELAY = 30; // seconds
  
  const formWrapper = document.querySelector('[data-ms-code="login-throttle-form"]');
  const submitButton = document.querySelector('[data-ms-code="throttle-submit"]');
  const errorMessage = document.querySelector('[data-ms-code="throttle-error"]');
  const attemptCounter = document.querySelector('[data-ms-code="attempt-counter"]');
  const loginForm = formWrapper?.querySelector('[data-ms-form="login"]');
  
  if (!formWrapper || !submitButton || !loginForm) {
    console.warn('MemberScript #167: Required elements not found.');
    return;
  }

  function getAttemptData() {
    const stored = sessionStorage.getItem(STORAGE_KEY);
    if (!stored) return { count: 0, timestamp: 0 };
    
    try {
      return JSON.parse(stored);
    } catch {
      return { count: 0, timestamp: 0 };
    }
  }

  function setAttemptData(count, timestamp = Date.now()) {
    sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ count, timestamp }));
  }

  let attemptData = getAttemptData();
  let securityTimer = null;

  function updateUIState() {
    const remainingAttempts = MAX_ATTEMPTS - attemptData.count;
    
    // Don't update UI if security timer is running
    if (securityTimer) {
      return;
    }
    
    if (attemptData.count >= MAX_ATTEMPTS) {
      showSecurityCheck();
      showError('Too many failed attempts. Please wait for security verification.');
    } else {
      hideSecurityCheck();
      if (attemptData.count > 0) {
        showError(`Login failed. ${remainingAttempts} attempt${remainingAttempts === 1 ? '' : 's'} remaining.`);
      } else {
        hideError();
      }
    }
    
    if (attemptCounter) {
      if (attemptData.count >= MAX_ATTEMPTS) {
        attemptCounter.textContent = 'Security verification required';
        attemptCounter.style.color = '#e74c3c';
      } else if (attemptData.count > 0) {
        attemptCounter.textContent = `${remainingAttempts} attempt${remainingAttempts === 1 ? '' : 's'} remaining`;
        attemptCounter.style.color = attemptData.count >= 2 ? '#e67e22' : '#95a5a6';
      } else {
        attemptCounter.textContent = '';
      }
    }
    
    console.log(`UI State: ${attemptData.count}/${MAX_ATTEMPTS} attempts, security timer: ${securityTimer ? 'active' : 'inactive'}`);
  }

  function showSecurityCheck() {
    let securityBox = formWrapper.querySelector('[data-ms-security-check]');
    
    if (!securityBox) {
      securityBox = document.createElement('div');
      securityBox.setAttribute('data-ms-security-check', 'true');
      securityBox.style.cssText = `
        width: 100%;
        margin: 15px 0;
        padding: 20px;
        border: 2px solid #ff6b6b;
        border-radius: 8px;
        background: #fff5f5;
        text-align: center;
        box-sizing: border-box;
      `;
      
      submitButton.parentNode.insertBefore(securityBox, submitButton);
    }
    
    securityBox.innerHTML = `
      <strong>Security Check</strong><br>
      <small>Please wait ${SECURITY_DELAY} seconds before trying again</small><br>
      <div id="security-countdown" style="margin-top: 10px; font-size: 24px; font-weight: bold; color: #ff6b6b;">${SECURITY_DELAY}</div>
    `;
    
    // Disable submit button
    submitButton.disabled = true;
    submitButton.style.opacity = '0.5';
    
    // Clear any existing timer
    if (securityTimer) {
      clearInterval(securityTimer);
    }
    
    // Start countdown
    let timeLeft = SECURITY_DELAY;
    securityTimer = setInterval(() => {
      timeLeft--;
      const countdown = securityBox.querySelector('#security-countdown');
      if (countdown) {
        countdown.textContent = timeLeft;
      }
      
      if (timeLeft <= 0) {
        clearInterval(securityTimer);
        securityTimer = null;
        
        securityBox.innerHTML = `
          <strong style="color: #27ae60;">✓ Security Check Complete</strong><br>
          <small>You may now try logging in again</small>
        `;
        
        // Re-enable submit button
        submitButton.disabled = false;
        submitButton.style.opacity = '1';
        
        // Reset attempt count so user gets fresh attempts
        attemptData.count = 0;
        setAttemptData(0);
        
        // Update UI to reflect fresh state
        updateUIState();
        
        // Hide security box after 5 seconds (longer so user sees message)
        setTimeout(() => {
          if (securityBox) {
            securityBox.style.display = 'none';
          }
        }, 5000);
      }
    }, 1000);
  }

  function hideSecurityCheck() {
    const securityBox = formWrapper.querySelector('[data-ms-security-check]');
    if (securityBox && !securityTimer) {
      securityBox.remove();
    }
    
    if (securityTimer) {
      clearInterval(securityTimer);
      securityTimer = null;
    }
    
    // Only enable button if we're not in security check mode
    if (attemptData.count < MAX_ATTEMPTS) {
      submitButton.disabled = false;
      submitButton.style.opacity = '1';
    }
  }

  function showError(message) {
    if (errorMessage) {
      errorMessage.textContent = message;
      errorMessage.style.display = 'block';
    }
  }

  function hideError() {
    if (errorMessage) {
      errorMessage.style.display = 'none';
    }
  }

  function handleSubmit(event) {
    // Prevent submission if security check is active
    if (attemptData.count >= MAX_ATTEMPTS && securityTimer) {
      event.preventDefault();
      showError('Please wait for the security check to complete.');
      return false;
    }
    
    const currentAttemptCount = attemptData.count;
    
    setTimeout(() => {
      checkLoginResult(currentAttemptCount);
    }, 1500);
  }

  function checkLoginResult(previousAttemptCount) {
    const hasError = document.querySelector('[data-ms-error]') || 
                    document.querySelector('.w-form-fail:not([style*="display: none"])') ||
                    formWrapper.querySelector('.w-form-fail:not([style*="display: none"])') ||
                    loginForm.querySelector('[data-ms-error]');

    if (window.$memberstackDom) {
      window.$memberstackDom.getCurrentMember().then(member => {
        if (member && member.id) {
          // Success! Reset everything
          sessionStorage.removeItem(STORAGE_KEY);
          attemptData = { count: 0, timestamp: 0 };
          hideError();
          hideSecurityCheck();
          
          if (attemptCounter) {
            attemptCounter.textContent = 'Login successful!';
            attemptCounter.style.color = '#27ae60';
          }
          
        } else if (hasError) {
          handleFailedLogin(previousAttemptCount);
        }
      }).catch(() => {
        if (hasError) {
          handleFailedLogin(previousAttemptCount);
        }
      });
    } else {
      if (hasError) {
        handleFailedLogin(previousAttemptCount);
      } else {
        const successElement = document.querySelector('.w-form-done:not([style*="display: none"])');
        if (successElement) {
          sessionStorage.removeItem(STORAGE_KEY);
          attemptData = { count: 0, timestamp: 0 };
          hideError();
          hideSecurityCheck();
        }
      }
    }
  }

  function handleFailedLogin(previousAttemptCount) {
    attemptData.count = previousAttemptCount + 1;
    setAttemptData(attemptData.count);
    
    console.log(`Failed login attempt ${attemptData.count}/${MAX_ATTEMPTS}`);
    
    // Force UI update after a brief delay to ensure DOM is ready
    setTimeout(() => {
      updateUIState();
    }, 100);
  }

  function init() {
    loginForm.addEventListener('submit', handleSubmit);
    updateUIState();
    
    if (window.$memberstackDom) {
      window.$memberstackDom.getCurrentMember().then(member => {
        if (member && member.id) {
          sessionStorage.removeItem(STORAGE_KEY);
          attemptData = { count: 0, timestamp: 0 };
        }
      }).catch(() => {
        // No user logged in
      });
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

})();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #167 v0.1 💙 - LOGIN THROTTLE WITH SECURITY CHECK -->
<script>
(function() {
  const MAX_ATTEMPTS = 3;
  const STORAGE_KEY = 'ms_login_attempts';
  const SECURITY_DELAY = 30; // seconds
  
  const formWrapper = document.querySelector('[data-ms-code="login-throttle-form"]');
  const submitButton = document.querySelector('[data-ms-code="throttle-submit"]');
  const errorMessage = document.querySelector('[data-ms-code="throttle-error"]');
  const attemptCounter = document.querySelector('[data-ms-code="attempt-counter"]');
  const loginForm = formWrapper?.querySelector('[data-ms-form="login"]');
  
  if (!formWrapper || !submitButton || !loginForm) {
    console.warn('MemberScript #167: Required elements not found.');
    return;
  }

  function getAttemptData() {
    const stored = sessionStorage.getItem(STORAGE_KEY);
    if (!stored) return { count: 0, timestamp: 0 };
    
    try {
      return JSON.parse(stored);
    } catch {
      return { count: 0, timestamp: 0 };
    }
  }

  function setAttemptData(count, timestamp = Date.now()) {
    sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ count, timestamp }));
  }

  let attemptData = getAttemptData();
  let securityTimer = null;

  function updateUIState() {
    const remainingAttempts = MAX_ATTEMPTS - attemptData.count;
    
    // Don't update UI if security timer is running
    if (securityTimer) {
      return;
    }
    
    if (attemptData.count >= MAX_ATTEMPTS) {
      showSecurityCheck();
      showError('Too many failed attempts. Please wait for security verification.');
    } else {
      hideSecurityCheck();
      if (attemptData.count > 0) {
        showError(`Login failed. ${remainingAttempts} attempt${remainingAttempts === 1 ? '' : 's'} remaining.`);
      } else {
        hideError();
      }
    }
    
    if (attemptCounter) {
      if (attemptData.count >= MAX_ATTEMPTS) {
        attemptCounter.textContent = 'Security verification required';
        attemptCounter.style.color = '#e74c3c';
      } else if (attemptData.count > 0) {
        attemptCounter.textContent = `${remainingAttempts} attempt${remainingAttempts === 1 ? '' : 's'} remaining`;
        attemptCounter.style.color = attemptData.count >= 2 ? '#e67e22' : '#95a5a6';
      } else {
        attemptCounter.textContent = '';
      }
    }
    
    console.log(`UI State: ${attemptData.count}/${MAX_ATTEMPTS} attempts, security timer: ${securityTimer ? 'active' : 'inactive'}`);
  }

  function showSecurityCheck() {
    let securityBox = formWrapper.querySelector('[data-ms-security-check]');
    
    if (!securityBox) {
      securityBox = document.createElement('div');
      securityBox.setAttribute('data-ms-security-check', 'true');
      securityBox.style.cssText = `
        width: 100%;
        margin: 15px 0;
        padding: 20px;
        border: 2px solid #ff6b6b;
        border-radius: 8px;
        background: #fff5f5;
        text-align: center;
        box-sizing: border-box;
      `;
      
      submitButton.parentNode.insertBefore(securityBox, submitButton);
    }
    
    securityBox.innerHTML = `
      <strong>Security Check</strong><br>
      <small>Please wait ${SECURITY_DELAY} seconds before trying again</small><br>
      <div id="security-countdown" style="margin-top: 10px; font-size: 24px; font-weight: bold; color: #ff6b6b;">${SECURITY_DELAY}</div>
    `;
    
    // Disable submit button
    submitButton.disabled = true;
    submitButton.style.opacity = '0.5';
    
    // Clear any existing timer
    if (securityTimer) {
      clearInterval(securityTimer);
    }
    
    // Start countdown
    let timeLeft = SECURITY_DELAY;
    securityTimer = setInterval(() => {
      timeLeft--;
      const countdown = securityBox.querySelector('#security-countdown');
      if (countdown) {
        countdown.textContent = timeLeft;
      }
      
      if (timeLeft <= 0) {
        clearInterval(securityTimer);
        securityTimer = null;
        
        securityBox.innerHTML = `
          <strong style="color: #27ae60;">✓ Security Check Complete</strong><br>
          <small>You may now try logging in again</small>
        `;
        
        // Re-enable submit button
        submitButton.disabled = false;
        submitButton.style.opacity = '1';
        
        // Reset attempt count so user gets fresh attempts
        attemptData.count = 0;
        setAttemptData(0);
        
        // Update UI to reflect fresh state
        updateUIState();
        
        // Hide security box after 5 seconds (longer so user sees message)
        setTimeout(() => {
          if (securityBox) {
            securityBox.style.display = 'none';
          }
        }, 5000);
      }
    }, 1000);
  }

  function hideSecurityCheck() {
    const securityBox = formWrapper.querySelector('[data-ms-security-check]');
    if (securityBox && !securityTimer) {
      securityBox.remove();
    }
    
    if (securityTimer) {
      clearInterval(securityTimer);
      securityTimer = null;
    }
    
    // Only enable button if we're not in security check mode
    if (attemptData.count < MAX_ATTEMPTS) {
      submitButton.disabled = false;
      submitButton.style.opacity = '1';
    }
  }

  function showError(message) {
    if (errorMessage) {
      errorMessage.textContent = message;
      errorMessage.style.display = 'block';
    }
  }

  function hideError() {
    if (errorMessage) {
      errorMessage.style.display = 'none';
    }
  }

  function handleSubmit(event) {
    // Prevent submission if security check is active
    if (attemptData.count >= MAX_ATTEMPTS && securityTimer) {
      event.preventDefault();
      showError('Please wait for the security check to complete.');
      return false;
    }
    
    const currentAttemptCount = attemptData.count;
    
    setTimeout(() => {
      checkLoginResult(currentAttemptCount);
    }, 1500);
  }

  function checkLoginResult(previousAttemptCount) {
    const hasError = document.querySelector('[data-ms-error]') || 
                    document.querySelector('.w-form-fail:not([style*="display: none"])') ||
                    formWrapper.querySelector('.w-form-fail:not([style*="display: none"])') ||
                    loginForm.querySelector('[data-ms-error]');

    if (window.$memberstackDom) {
      window.$memberstackDom.getCurrentMember().then(member => {
        if (member && member.id) {
          // Success! Reset everything
          sessionStorage.removeItem(STORAGE_KEY);
          attemptData = { count: 0, timestamp: 0 };
          hideError();
          hideSecurityCheck();
          
          if (attemptCounter) {
            attemptCounter.textContent = 'Login successful!';
            attemptCounter.style.color = '#27ae60';
          }
          
        } else if (hasError) {
          handleFailedLogin(previousAttemptCount);
        }
      }).catch(() => {
        if (hasError) {
          handleFailedLogin(previousAttemptCount);
        }
      });
    } else {
      if (hasError) {
        handleFailedLogin(previousAttemptCount);
      } else {
        const successElement = document.querySelector('.w-form-done:not([style*="display: none"])');
        if (successElement) {
          sessionStorage.removeItem(STORAGE_KEY);
          attemptData = { count: 0, timestamp: 0 };
          hideError();
          hideSecurityCheck();
        }
      }
    }
  }

  function handleFailedLogin(previousAttemptCount) {
    attemptData.count = previousAttemptCount + 1;
    setAttemptData(attemptData.count);
    
    console.log(`Failed login attempt ${attemptData.count}/${MAX_ATTEMPTS}`);
    
    // Force UI update after a brief delay to ensure DOM is ready
    setTimeout(() => {
      updateUIState();
    }, 100);
  }

  function init() {
    loginForm.addEventListener('submit', handleSubmit);
    updateUIState();
    
    if (window.$memberstackDom) {
      window.$memberstackDom.getCurrentMember().then(member => {
        if (member && member.id) {
          sessionStorage.removeItem(STORAGE_KEY);
          attemptData = { count: 0, timestamp: 0 };
        }
      }).catch(() => {
        // No user logged in
      });
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

})();
</script>
Ansicht Memberscript
Bedingte Sichtbarkeit

#166 - Show or Hide Content Based on Regions

Show or hide Webflow content based on a visitor’s country using simple data attributes.


<!-- 💙 MEMBERSCRIPT #166 v0.1 💙 - GEO‑GATED REGION BLOCKER (Stable Version) -->

<script>
(function () {
  // STEP 1: Store and remove locale-show elements
  const localeShowElements = [];
  document.querySelectorAll('[data-ms-code="locale-show"]').forEach(el => {
    localeShowElements.push({
      el,
      parent: el.parentNode,
      next: el.nextSibling
    });
    el.remove();
  });

  // STEP 2: Store and remove locale-hide elements
  const localeHideElements = [];
  document.querySelectorAll('[data-ms-code="locale-hide"]').forEach(el => {
    localeHideElements.push({
      el,
      parent: el.parentNode,
      next: el.nextSibling
    });
    el.remove();
  });

  // STEP 3: Safe DOM reinsertion
  function safeInsert(parent, el, next) {
    if (next && parent.contains(next)) {
      parent.insertBefore(el, next);
    } else {
      parent.appendChild(el);
    }
  }

  // STEP 4: Get country using two fallback-safe APIs
  async function getUserCountry() {
    try {
      const res = await fetch('https://api.country.is/');
      const data = await res.json();
      if (data && data.country) return data.country.toUpperCase();
    } catch (err1) {
      try {
        const res = await fetch('https://ipwho.is/');
        const data = await res.json();
        if (data && data.success && data.country_code) {
          return data.country_code.toUpperCase();
        }
      } catch (err2) {
        console.error('Geolocation failed:', err2);
      }
    }
    return null;
  }

  // STEP 5: Run logic after detecting country
  getUserCountry().then(userCountry => {
    if (!userCountry) return;

    // Show if user's country is allowed
    localeShowElements.forEach(({ el, parent, next }) => {
      const allowed = (el.getAttribute('data-ms-countries') || '')
        .split(',')
        .map(c => c.trim().toUpperCase());
      if (allowed.includes(userCountry)) {
        safeInsert(parent, el, next);
      }
    });

    // Hide if user's country is blocked
    localeHideElements.forEach(({ el, parent, next }) => {
      const blocked = (el.getAttribute('data-ms-countries') || '')
        .split(',')
        .map(c => c.trim().toUpperCase());
      if (!blocked.includes(userCountry)) {
        safeInsert(parent, el, next);
      }
    });
  });
})();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #166 v0.1 💙 - GEO‑GATED REGION BLOCKER (Stable Version) -->

<script>
(function () {
  // STEP 1: Store and remove locale-show elements
  const localeShowElements = [];
  document.querySelectorAll('[data-ms-code="locale-show"]').forEach(el => {
    localeShowElements.push({
      el,
      parent: el.parentNode,
      next: el.nextSibling
    });
    el.remove();
  });

  // STEP 2: Store and remove locale-hide elements
  const localeHideElements = [];
  document.querySelectorAll('[data-ms-code="locale-hide"]').forEach(el => {
    localeHideElements.push({
      el,
      parent: el.parentNode,
      next: el.nextSibling
    });
    el.remove();
  });

  // STEP 3: Safe DOM reinsertion
  function safeInsert(parent, el, next) {
    if (next && parent.contains(next)) {
      parent.insertBefore(el, next);
    } else {
      parent.appendChild(el);
    }
  }

  // STEP 4: Get country using two fallback-safe APIs
  async function getUserCountry() {
    try {
      const res = await fetch('https://api.country.is/');
      const data = await res.json();
      if (data && data.country) return data.country.toUpperCase();
    } catch (err1) {
      try {
        const res = await fetch('https://ipwho.is/');
        const data = await res.json();
        if (data && data.success && data.country_code) {
          return data.country_code.toUpperCase();
        }
      } catch (err2) {
        console.error('Geolocation failed:', err2);
      }
    }
    return null;
  }

  // STEP 5: Run logic after detecting country
  getUserCountry().then(userCountry => {
    if (!userCountry) return;

    // Show if user's country is allowed
    localeShowElements.forEach(({ el, parent, next }) => {
      const allowed = (el.getAttribute('data-ms-countries') || '')
        .split(',')
        .map(c => c.trim().toUpperCase());
      if (allowed.includes(userCountry)) {
        safeInsert(parent, el, next);
      }
    });

    // Hide if user's country is blocked
    localeHideElements.forEach(({ el, parent, next }) => {
      const blocked = (el.getAttribute('data-ms-countries') || '')
        .split(',')
        .map(c => c.trim().toUpperCase());
      if (!blocked.includes(userCountry)) {
        safeInsert(parent, el, next);
      }
    });
  });
})();
</script>
Ansicht Memberscript
UX

#165 - Typing Animation in a Search Bar

Create an animated typing effect in search bar placeholders that cycles through custom suggestions.


<!-- 💙 MEMBERSCRIPT #165 v0.1 💙 - TYPING ANIMATION IN A FUNCTIONAL SEARCH BAR -->
<script>

(function() {
  const searchInput = document.querySelector('[data-ms-code="search-bar"]');
  if (!searchInput) return;

  const suggestions = [];
  for (let i = 1; i <= 5; i++) {
    const suggestion = searchInput.getAttribute(`data-ms-suggestion-${i}`);
    if (suggestion) suggestions.push(suggestion);
  }
  if (suggestions.length === 0) return;

  let suggestionIndex = 0;
  let charIndex = 0;
  let typing;
  let isAnimating = false;
  let originalPlaceholder = searchInput.placeholder || '';

  function typeSuggestion() {
    if (!isAnimating) return;
    
    const current = suggestions[suggestionIndex];
    searchInput.placeholder = current.slice(0, charIndex++);
    
    if (charIndex <= current.length) {
      typing = setTimeout(typeSuggestion, 80);
    } else {
      setTimeout(eraseSuggestion, 1200);
    }
  }

  function eraseSuggestion() {
    if (!isAnimating) return;
    
    const current = suggestions[suggestionIndex];
    searchInput.placeholder = current.slice(0, --charIndex);
    
    if (charIndex > 0) {
      typing = setTimeout(eraseSuggestion, 40);
    } else {
      suggestionIndex = (suggestionIndex + 1) % suggestions.length;
      setTimeout(typeSuggestion, 500);
    }
  }

  function stopAnimation() {
    isAnimating = false;
    clearTimeout(typing);
    searchInput.placeholder = originalPlaceholder;
  }

  function startAnimation() {
    if (searchInput.value.trim() !== '') return;
    
    isAnimating = true;
    charIndex = 0;
    typeSuggestion();
  }

  // Event listeners
  searchInput.addEventListener("focus", stopAnimation);
  searchInput.addEventListener("blur", () => {
    if (searchInput.value.trim() === '') {
      setTimeout(startAnimation, 500);
    }
  });
  searchInput.addEventListener("input", () => {
    if (isAnimating) stopAnimation();
  });

  // Start animation after delay
  setTimeout(() => {
    if (searchInput.value.trim() === '' && document.activeElement !== searchInput) {
      startAnimation();
    }
  }, 2000);
})();

</script>
v0.1

<!-- 💙 MEMBERSCRIPT #165 v0.1 💙 - TYPING ANIMATION IN A FUNCTIONAL SEARCH BAR -->
<script>

(function() {
  const searchInput = document.querySelector('[data-ms-code="search-bar"]');
  if (!searchInput) return;

  const suggestions = [];
  for (let i = 1; i <= 5; i++) {
    const suggestion = searchInput.getAttribute(`data-ms-suggestion-${i}`);
    if (suggestion) suggestions.push(suggestion);
  }
  if (suggestions.length === 0) return;

  let suggestionIndex = 0;
  let charIndex = 0;
  let typing;
  let isAnimating = false;
  let originalPlaceholder = searchInput.placeholder || '';

  function typeSuggestion() {
    if (!isAnimating) return;
    
    const current = suggestions[suggestionIndex];
    searchInput.placeholder = current.slice(0, charIndex++);
    
    if (charIndex <= current.length) {
      typing = setTimeout(typeSuggestion, 80);
    } else {
      setTimeout(eraseSuggestion, 1200);
    }
  }

  function eraseSuggestion() {
    if (!isAnimating) return;
    
    const current = suggestions[suggestionIndex];
    searchInput.placeholder = current.slice(0, --charIndex);
    
    if (charIndex > 0) {
      typing = setTimeout(eraseSuggestion, 40);
    } else {
      suggestionIndex = (suggestionIndex + 1) % suggestions.length;
      setTimeout(typeSuggestion, 500);
    }
  }

  function stopAnimation() {
    isAnimating = false;
    clearTimeout(typing);
    searchInput.placeholder = originalPlaceholder;
  }

  function startAnimation() {
    if (searchInput.value.trim() !== '') return;
    
    isAnimating = true;
    charIndex = 0;
    typeSuggestion();
  }

  // Event listeners
  searchInput.addEventListener("focus", stopAnimation);
  searchInput.addEventListener("blur", () => {
    if (searchInput.value.trim() === '') {
      setTimeout(startAnimation, 500);
    }
  });
  searchInput.addEventListener("input", () => {
    if (isAnimating) stopAnimation();
  });

  // Start animation after delay
  setTimeout(() => {
    if (searchInput.value.trim() === '' && document.activeElement !== searchInput) {
      startAnimation();
    }
  }, 2000);
})();

</script>
Ansicht Memberscript
Bedingte Sichtbarkeit
UX

#164 - Animated Chat Conversation Layout

This script adds an engaging animated chat layout in Webflow, sequentially displaying messages.


<!-- 💙 MEMBERSCRIPT #164 v0.1 💙 - ANIMATED CHAT CONVERSATION LAYOUT -->

<script>
(function() {
  // Main animation function
  function animateChat() {
    const container = document.querySelector('[data-ms-code="chat-container"]');
    if (!container) return;

    const messages = Array.from(container.querySelectorAll('[data-ms-code="chat-message"]'));
    const button = container.querySelector('[data-ms-code="chat-button"]');
    let i = 0;

    // Reset any previous visibility
    messages.forEach(msg => msg.classList.remove('visible'));
    if (button) button.classList.remove('visible');

    function showNext() {
      if (i < messages.length) {
        messages[i].classList.add('visible');
        i++;
        setTimeout(showNext, 500); // next message in 500ms
      } else {
        if (button) button.classList.add('visible');

        /* ➕ LOOPING CODE BEGINS HERE */
        setTimeout(() => {
          messages.forEach(msg => msg.classList.remove('visible'));
          if (button) button.classList.remove('visible');
          animateChat(); // 👈 Recursive call to restart the animation
        }, 2000);
        /* To STOP looping: remove or comment out everything from this setTimeout block */
      }
    }

    showNext();
  }

  // Start animation as soon as DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', animateChat);
  } else {
    animateChat();
  }
})();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #164 v0.1 💙 - ANIMATED CHAT CONVERSATION LAYOUT -->

<script>
(function() {
  // Main animation function
  function animateChat() {
    const container = document.querySelector('[data-ms-code="chat-container"]');
    if (!container) return;

    const messages = Array.from(container.querySelectorAll('[data-ms-code="chat-message"]'));
    const button = container.querySelector('[data-ms-code="chat-button"]');
    let i = 0;

    // Reset any previous visibility
    messages.forEach(msg => msg.classList.remove('visible'));
    if (button) button.classList.remove('visible');

    function showNext() {
      if (i < messages.length) {
        messages[i].classList.add('visible');
        i++;
        setTimeout(showNext, 500); // next message in 500ms
      } else {
        if (button) button.classList.add('visible');

        /* ➕ LOOPING CODE BEGINS HERE */
        setTimeout(() => {
          messages.forEach(msg => msg.classList.remove('visible'));
          if (button) button.classList.remove('visible');
          animateChat(); // 👈 Recursive call to restart the animation
        }, 2000);
        /* To STOP looping: remove or comment out everything from this setTimeout block */
      }
    }

    showNext();
  }

  // Start animation as soon as DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', animateChat);
  } else {
    animateChat();
  }
})();
</script>
Ansicht Memberscript
UX

#163 - Text Shuffle On Hover

Adds a playful text shuffle animation on hover—scrambling letters briefly before snapping back.


<!-- 💙 MEMBERSCRIPT #163 v0.1 💙 - TEXT SHUFFLE ON HOVER -->

<script>
(function() {
  // Helper: Shuffle the characters in a string
  function shuffle(str) {
    var arr = str.split('');
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    return arr.join('');
  }

  // Find all elements with data-ms-code="shuffle-text"
  var elements = document.querySelectorAll('[data-ms-code="shuffle-text"]');

  elements.forEach(function(el) {
    var originalText = el.textContent;
    var interval = null;
    var duration = 600; // ms
    var shuffleSpeed = 50; // ms

    el.addEventListener('mouseenter', function() {
      var start = Date.now();
      clearInterval(interval);
      interval = setInterval(function() {
        if (Date.now() - start > duration) {
          clearInterval(interval);
          el.textContent = originalText;
        } else {
          el.textContent = shuffle(originalText);
        }
      }, shuffleSpeed);
    });

    el.addEventListener('mouseleave', function() {
      clearInterval(interval);
      el.textContent = originalText;
    });
  });
})();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #163 v0.1 💙 - TEXT SHUFFLE ON HOVER -->

<script>
(function() {
  // Helper: Shuffle the characters in a string
  function shuffle(str) {
    var arr = str.split('');
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    return arr.join('');
  }

  // Find all elements with data-ms-code="shuffle-text"
  var elements = document.querySelectorAll('[data-ms-code="shuffle-text"]');

  elements.forEach(function(el) {
    var originalText = el.textContent;
    var interval = null;
    var duration = 600; // ms
    var shuffleSpeed = 50; // ms

    el.addEventListener('mouseenter', function() {
      var start = Date.now();
      clearInterval(interval);
      interval = setInterval(function() {
        if (Date.now() - start > duration) {
          clearInterval(interval);
          el.textContent = originalText;
        } else {
          el.textContent = shuffle(originalText);
        }
      }, shuffleSpeed);
    });

    el.addEventListener('mouseleave', function() {
      clearInterval(interval);
      el.textContent = originalText;
    });
  });
})();
</script>
Ansicht Memberscript
UX

#162 - Change Horizontal Tabs on Page Scroll

Auto‑switch horizontal tabs in Webflow as you scroll, locking the scroll inside the tab section.


<!-- 💙 MEMBERSCRIPT #162 v0.1 💙 - CHANGE HORIZONTAL TABS WHEN PAGE IS SCROLLED -->

<script>
(function() {
  // Disable on tablet and mobile (only run on desktop ≥ 992px)
  if (window.matchMedia('(max-width: 991px)').matches) return;

  const tabSection = document.querySelector('[data-ms-code="tab-section"]');
  if (!tabSection) return;

  const tabButtons = Array.from(tabSection.querySelectorAll('[data-ms-code^="tab-"]'))
    .filter(btn => !btn.hasAttribute('data-ms-code') || !btn.getAttribute('data-ms-code').startsWith('tab-content-'));
  const tabContents = Array.from(tabSection.querySelectorAll('[data-ms-code^="tab-content-"]'));

  if (!tabButtons.length || !tabContents.length) return;

  let isLocked = true;
  let isTouching = false;
  let touchStartY = 0;
  let lastTabChange = 0;
  const cooldown = 500; // ms

  function getCurrentTabIndex() {
    return tabButtons.findIndex(btn => btn.classList.contains('w--current'));
  }

  function activateTab(index) {
    if (index < 0 || index >= tabButtons.length) return;
    tabButtons[index].click();
  }

  function isTabSectionInView() {
    const rect = tabSection.getBoundingClientRect();
    return rect.top < window.innerHeight && rect.bottom > 0;
  }

  function tryTabChange(direction, event) {
    const now = Date.now();
    if (now - lastTabChange < cooldown) return;
    let currentTab = getCurrentTabIndex();
    if (direction > 0 && currentTab < tabButtons.length - 1) {
      event.preventDefault();
      activateTab(currentTab + 1);
      isLocked = true;
    } else if (direction < 0 && currentTab > 0) {
      event.preventDefault();
      activateTab(currentTab - 1);
      isLocked = true;
    } else {
      isLocked = false;
      const rect = tabSection.getBoundingClientRect();
      window.scrollBy({
        top: direction > 0 ? rect.bottom - 1 : rect.top - window.innerHeight + 1,
        left: 0,
        behavior: 'smooth'
      });
    }
    lastTabChange = now;
  }

  function onWheel(e) {
    if (!isTabSectionInView()) return;
    if (e.deltaY > 0) {
      tryTabChange(1, e);
    } else if (e.deltaY < 0) {
      tryTabChange(-1, e);
    }
  }

  function onTouchStart(e) {
    if (!isTabSectionInView()) return;
    isTouching = true;
    touchStartY = e.touches[0].clientY;
  }

  function onTouchMove(e) {
    if (!isTabSectionInView() || !isTouching) return;
    const now = Date.now();
    if (now - lastTabChange < cooldown) return;
    const deltaY = touchStartY - e.touches[0].clientY;
    if (Math.abs(deltaY) > 30) {
      tryTabChange(deltaY > 0 ? 1 : -1, e);
      isTouching = false;
    }
  }

  function onTouchEnd() {
    isTouching = false;
  }

  function preventScroll(e) {
    if (isLocked && isTabSectionInView()) {
      e.preventDefault();
      e.stopPropagation();
      return false;
    }
  }

  window.addEventListener('scroll', () => {
    const currentTab = getCurrentTabIndex();
    isLocked = isTabSectionInView() && currentTab > 0 && currentTab < tabButtons.length - 1;
  });

  tabSection.addEventListener('wheel', onWheel, { passive: false });
  tabSection.addEventListener('touchstart', onTouchStart, { passive: false });
  tabSection.addEventListener('touchmove', onTouchMove, { passive: false });
  tabSection.addEventListener('touchend', onTouchEnd, { passive: false });

  document.addEventListener('wheel', preventScroll, { passive: false });
  document.addEventListener('touchmove', preventScroll, { passive: false });

  const initialTab = getCurrentTabIndex();
  isLocked = initialTab > 0 && initialTab < tabButtons.length - 1;
})();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #162 v0.1 💙 - CHANGE HORIZONTAL TABS WHEN PAGE IS SCROLLED -->

<script>
(function() {
  // Disable on tablet and mobile (only run on desktop ≥ 992px)
  if (window.matchMedia('(max-width: 991px)').matches) return;

  const tabSection = document.querySelector('[data-ms-code="tab-section"]');
  if (!tabSection) return;

  const tabButtons = Array.from(tabSection.querySelectorAll('[data-ms-code^="tab-"]'))
    .filter(btn => !btn.hasAttribute('data-ms-code') || !btn.getAttribute('data-ms-code').startsWith('tab-content-'));
  const tabContents = Array.from(tabSection.querySelectorAll('[data-ms-code^="tab-content-"]'));

  if (!tabButtons.length || !tabContents.length) return;

  let isLocked = true;
  let isTouching = false;
  let touchStartY = 0;
  let lastTabChange = 0;
  const cooldown = 500; // ms

  function getCurrentTabIndex() {
    return tabButtons.findIndex(btn => btn.classList.contains('w--current'));
  }

  function activateTab(index) {
    if (index < 0 || index >= tabButtons.length) return;
    tabButtons[index].click();
  }

  function isTabSectionInView() {
    const rect = tabSection.getBoundingClientRect();
    return rect.top < window.innerHeight && rect.bottom > 0;
  }

  function tryTabChange(direction, event) {
    const now = Date.now();
    if (now - lastTabChange < cooldown) return;
    let currentTab = getCurrentTabIndex();
    if (direction > 0 && currentTab < tabButtons.length - 1) {
      event.preventDefault();
      activateTab(currentTab + 1);
      isLocked = true;
    } else if (direction < 0 && currentTab > 0) {
      event.preventDefault();
      activateTab(currentTab - 1);
      isLocked = true;
    } else {
      isLocked = false;
      const rect = tabSection.getBoundingClientRect();
      window.scrollBy({
        top: direction > 0 ? rect.bottom - 1 : rect.top - window.innerHeight + 1,
        left: 0,
        behavior: 'smooth'
      });
    }
    lastTabChange = now;
  }

  function onWheel(e) {
    if (!isTabSectionInView()) return;
    if (e.deltaY > 0) {
      tryTabChange(1, e);
    } else if (e.deltaY < 0) {
      tryTabChange(-1, e);
    }
  }

  function onTouchStart(e) {
    if (!isTabSectionInView()) return;
    isTouching = true;
    touchStartY = e.touches[0].clientY;
  }

  function onTouchMove(e) {
    if (!isTabSectionInView() || !isTouching) return;
    const now = Date.now();
    if (now - lastTabChange < cooldown) return;
    const deltaY = touchStartY - e.touches[0].clientY;
    if (Math.abs(deltaY) > 30) {
      tryTabChange(deltaY > 0 ? 1 : -1, e);
      isTouching = false;
    }
  }

  function onTouchEnd() {
    isTouching = false;
  }

  function preventScroll(e) {
    if (isLocked && isTabSectionInView()) {
      e.preventDefault();
      e.stopPropagation();
      return false;
    }
  }

  window.addEventListener('scroll', () => {
    const currentTab = getCurrentTabIndex();
    isLocked = isTabSectionInView() && currentTab > 0 && currentTab < tabButtons.length - 1;
  });

  tabSection.addEventListener('wheel', onWheel, { passive: false });
  tabSection.addEventListener('touchstart', onTouchStart, { passive: false });
  tabSection.addEventListener('touchmove', onTouchMove, { passive: false });
  tabSection.addEventListener('touchend', onTouchEnd, { passive: false });

  document.addEventListener('wheel', preventScroll, { passive: false });
  document.addEventListener('touchmove', preventScroll, { passive: false });

  const initialTab = getCurrentTabIndex();
  isLocked = initialTab > 0 && initialTab < tabButtons.length - 1;
})();
</script>
Ansicht Memberscript
UX

#161 - Estimate Article Reading Time

Automatically estimates and displays how long it’ll take to read your blog post.


<!-- 💙 MEMBERSCRIPT #161: DYNAMIC READING TIME -->

<script>
document.addEventListener("DOMContentLoaded", () => {
  document.querySelectorAll('[data-ms-code="reading-article"]').forEach(article => {
    const rtEl = article.querySelector('[data-ms-code="reading-time"]');
    const rich = article.querySelector('[data-ms-code="reading-text"]');
    if (!rtEl || !rich) return;

    const text = rich.innerText.trim();
    const words = text ? text.split(/\s+/).length : 0;
    const imgs = rich.querySelectorAll('img').length;
    const WPM = 260, SEC_PER_IMG = 10;

    const totalSec = (words / WPM) * 60 + imgs * SEC_PER_IMG;
    const minutes = Math.max(1, Math.ceil(totalSec / 60));

    rtEl.innerText = `${minutes} min read`;
  });
});
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #161: DYNAMIC READING TIME -->

<script>
document.addEventListener("DOMContentLoaded", () => {
  document.querySelectorAll('[data-ms-code="reading-article"]').forEach(article => {
    const rtEl = article.querySelector('[data-ms-code="reading-time"]');
    const rich = article.querySelector('[data-ms-code="reading-text"]');
    if (!rtEl || !rich) return;

    const text = rich.innerText.trim();
    const words = text ? text.split(/\s+/).length : 0;
    const imgs = rich.querySelectorAll('img').length;
    const WPM = 260, SEC_PER_IMG = 10;

    const totalSec = (words / WPM) * 60 + imgs * SEC_PER_IMG;
    const minutes = Math.max(1, Math.ceil(totalSec / 60));

    rtEl.innerText = `${minutes} min read`;
  });
});
</script>
Ansicht Memberscript
UX

#160 - Limit CMS Items Per Breakpoint

Control how many CMS items are visible on your Webflow site for each device size.


<!-- 💙 MEMBERSCRIPT #160 v0.1 💙 - LIMIT CMS ITEMS PER BREAKPOINT -->

<!-- 
  Dynamically limits how many CMS items show per breakpoint.
  Useful for responsive design where fewer items should show on smaller screens.
-->

<script>
(function() {
  // Set how many items to show per breakpoint (edit as needed)
  const limits = {
    desktop: 6,   // ≥992px
    tablet: 4,    // 768px–991px
    mobile: 2     // <768px
  };

  function getBreakpoint() {
    const width = window.innerWidth;
    if (width >= 992) return 'desktop';
    if (width >= 768) return 'tablet';
    return 'mobile';
  }

  function limitItems() {
    const breakpoint = getBreakpoint();
    const limit = limits[breakpoint] || limits.desktop;
    const lists = document.querySelectorAll('[data-ms-code="cms-list"]');
    lists.forEach(list => {
      const items = list.querySelectorAll('[data-ms-code="cms-item"]');
      items.forEach((item, i) => {
        item.style.display = (i < limit) ? '' : 'none';
      });
    });
  }

  // Run on page load and on resize
  window.addEventListener('DOMContentLoaded', limitItems);
  window.addEventListener('resize', limitItems);
})();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #160 v0.1 💙 - LIMIT CMS ITEMS PER BREAKPOINT -->

<!-- 
  Dynamically limits how many CMS items show per breakpoint.
  Useful for responsive design where fewer items should show on smaller screens.
-->

<script>
(function() {
  // Set how many items to show per breakpoint (edit as needed)
  const limits = {
    desktop: 6,   // ≥992px
    tablet: 4,    // 768px–991px
    mobile: 2     // <768px
  };

  function getBreakpoint() {
    const width = window.innerWidth;
    if (width >= 992) return 'desktop';
    if (width >= 768) return 'tablet';
    return 'mobile';
  }

  function limitItems() {
    const breakpoint = getBreakpoint();
    const limit = limits[breakpoint] || limits.desktop;
    const lists = document.querySelectorAll('[data-ms-code="cms-list"]');
    lists.forEach(list => {
      const items = list.querySelectorAll('[data-ms-code="cms-item"]');
      items.forEach((item, i) => {
        item.style.display = (i < limit) ? '' : 'none';
      });
    });
  }

  // Run on page load and on resize
  window.addEventListener('DOMContentLoaded', limitItems);
  window.addEventListener('resize', limitItems);
})();
</script>
Ansicht Memberscript
UX

#159 - Price Estimation Calculator

This script calculates a real-time total price based on user-selected options.


<!-- 💙 MEMBERSCRIPT #159 v0.1 💙 - PRICE ESTIMATION CALCULATOR -->

<!-- 
  Calculates a dynamic price total and summary based on selected inputs.
  Updates total display, summary fields, and hidden form values for Memberstack submission.
-->

<script>
document.addEventListener("DOMContentLoaded", function () {
  const calculator = document.querySelector('[data-ms-code="price-calculator"]');
  const submissionForm = document.querySelector('[data-ms-code="submission-form"]');

  if (!calculator || !submissionForm) {
    console.error("Calculator or Submission Form not found.");
    return;
  }

  const BASE_PRICE = 500; // Replace with your minimum/base price

  const calculatePriceAndSummary = () => {
    let subTotal = 0;

    // 1. PRICE CALCULATION
    const pricedInputs = calculator.querySelectorAll('[data-price], [data-price-per-unit]');
    pricedInputs.forEach((input) => {
      if (input.type === "checkbox" && !input.checked) return;
      const price = parseFloat(input.dataset.price) || 0;
      const perUnit = parseFloat(input.dataset.pricePerUnit) || 0;
      const value = parseFloat(input.value) || 0;
      subTotal += perUnit > 0 ? value * perUnit : price;
    });

    const total = BASE_PRICE + subTotal;

    // 2. UPDATE TOTAL DISPLAY
    const totalPriceEl = calculator.querySelector('[data-ms-code-price-total]');
    if (totalPriceEl) totalPriceEl.textContent = `$${Math.round(total).toLocaleString()}`;

    // 3. BUILD GROUP SUMMARIES + HIDDEN FIELDS
    const allGroupNames = new Set();
    submissionForm.querySelectorAll("[data-ms-code-hidden]").forEach((el) => {
      allGroupNames.add(el.dataset.msCodeHidden);
    });

    allGroupNames.forEach((group) => {
      let selectedLabels = [];

      const groupInputs = calculator.querySelectorAll(
        `input[data-summary-group="${group}"]:checked, 
         input[data-feature-group="${group}"]:checked, 
         input[data-summary-group="${group}"][type="range"],
         input[data-ms-code-slider="${group}"],
         select[data-summary-group="${group}"]`
      );

      groupInputs.forEach((input) => {
        if (input.type === "range") {
          selectedLabels.push(input.value);
        } else if (input.tagName === "SELECT") {
          const selectedOption = input.options[input.selectedIndex];
          if (selectedOption) selectedLabels.push(selectedOption.textContent);
        } else {
          const label = input.parentElement.querySelector(".w-form-label");
          if (label) selectedLabels.push(label.textContent);
        }
      });

      const summaryText = selectedLabels.length > 0 ? selectedLabels.join(", ") : "None";

      const summaryEl = calculator.querySelector(`[data-ms-code-summary="${group}"]`);
      if (summaryEl) summaryEl.textContent = summaryText;

      const hiddenInput = submissionForm.querySelector(`[data-ms-code-hidden="${group}"]`);
      if (hiddenInput) hiddenInput.value = summaryText;
    });

    // 4. SET TOTAL IN HIDDEN FIELD
    const hiddenPrice = submissionForm.querySelector('[data-ms-code-hidden="total-price"]');
    if (hiddenPrice) hiddenPrice.value = total;

    // 5. HANDLE OUTPUT DISPLAY FOR GROUPS (subtotal or raw value)
    allGroupNames.forEach((group) => {
      let groupTotal = 0;
      const groupInputs = calculator.querySelectorAll(
        `input[data-summary-group="${group}"]:checked, 
         input[data-feature-group="${group}"]:checked, 
         input[data-summary-group="${group}"][type="range"],
         input[data-ms-code-slider="${group}"],
         select[data-summary-group="${group}"]`
      );

      groupInputs.forEach((input) => {
        const price = parseFloat(input.dataset.price) || 0;
        const perUnit = parseFloat(input.dataset.pricePerUnit) || 0;
        const value = parseFloat(input.value) || 0;
        groupTotal += perUnit > 0 ? value * perUnit : price;
      });

      const groupOutput = calculator.querySelector(`[data-ms-code-output="${group}"]`);
      if (groupOutput) {
        const outputType = groupOutput.dataset.outputType || "price";
        if (outputType === "value") {
          const valueInput = calculator.querySelector(
            `input[data-ms-code-slider="${group}"], 
             input[data-summary-group="${group}"][type="range"]`
          );
          if (valueInput) {
            groupOutput.textContent = valueInput.value;
          }
        } else {
          groupOutput.textContent = `$${Math.round(groupTotal).toLocaleString()}`;
        }
      }
    });
  };

  const setupEventListeners = () => {
    calculator.addEventListener("input", calculatePriceAndSummary);

    // Exclusive checkbox behavior
    const checkboxGroups = {};
    calculator.querySelectorAll('input[type="checkbox"][data-feature-group]').forEach((cb) => {
      const groupName = cb.dataset.featureGroup;
      if (!checkboxGroups[groupName]) checkboxGroups[groupName] = [];
      checkboxGroups[groupName].push(cb);
    });

    Object.values(checkboxGroups).forEach((group) => {
      group.forEach((cb) => {
        cb.addEventListener("change", () => {
          if (cb.checked) {
            group.forEach((otherCb) => {
              if (otherCb !== cb) otherCb.checked = false;
            });
          }
          calculatePriceAndSummary();
        });
      });
    });

    // Reset on submission
    submissionForm.addEventListener("submit", () => {
      setTimeout(() => {
        calculator.querySelectorAll('input[type="checkbox"]').forEach((cb) => (cb.checked = false));
        calculator.querySelectorAll('input[type="range"]').forEach((slider) => {
          slider.value = slider.defaultValue || slider.min || "0";
          slider.dispatchEvent(new Event("input", { bubbles: true }));
        });
        calculatePriceAndSummary();
      }, 100);
    });
  };

  setupEventListeners();
  calculatePriceAndSummary();
});
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #159 v0.1 💙 - PRICE ESTIMATION CALCULATOR -->

<!-- 
  Calculates a dynamic price total and summary based on selected inputs.
  Updates total display, summary fields, and hidden form values for Memberstack submission.
-->

<script>
document.addEventListener("DOMContentLoaded", function () {
  const calculator = document.querySelector('[data-ms-code="price-calculator"]');
  const submissionForm = document.querySelector('[data-ms-code="submission-form"]');

  if (!calculator || !submissionForm) {
    console.error("Calculator or Submission Form not found.");
    return;
  }

  const BASE_PRICE = 500; // Replace with your minimum/base price

  const calculatePriceAndSummary = () => {
    let subTotal = 0;

    // 1. PRICE CALCULATION
    const pricedInputs = calculator.querySelectorAll('[data-price], [data-price-per-unit]');
    pricedInputs.forEach((input) => {
      if (input.type === "checkbox" && !input.checked) return;
      const price = parseFloat(input.dataset.price) || 0;
      const perUnit = parseFloat(input.dataset.pricePerUnit) || 0;
      const value = parseFloat(input.value) || 0;
      subTotal += perUnit > 0 ? value * perUnit : price;
    });

    const total = BASE_PRICE + subTotal;

    // 2. UPDATE TOTAL DISPLAY
    const totalPriceEl = calculator.querySelector('[data-ms-code-price-total]');
    if (totalPriceEl) totalPriceEl.textContent = `$${Math.round(total).toLocaleString()}`;

    // 3. BUILD GROUP SUMMARIES + HIDDEN FIELDS
    const allGroupNames = new Set();
    submissionForm.querySelectorAll("[data-ms-code-hidden]").forEach((el) => {
      allGroupNames.add(el.dataset.msCodeHidden);
    });

    allGroupNames.forEach((group) => {
      let selectedLabels = [];

      const groupInputs = calculator.querySelectorAll(
        `input[data-summary-group="${group}"]:checked, 
         input[data-feature-group="${group}"]:checked, 
         input[data-summary-group="${group}"][type="range"],
         input[data-ms-code-slider="${group}"],
         select[data-summary-group="${group}"]`
      );

      groupInputs.forEach((input) => {
        if (input.type === "range") {
          selectedLabels.push(input.value);
        } else if (input.tagName === "SELECT") {
          const selectedOption = input.options[input.selectedIndex];
          if (selectedOption) selectedLabels.push(selectedOption.textContent);
        } else {
          const label = input.parentElement.querySelector(".w-form-label");
          if (label) selectedLabels.push(label.textContent);
        }
      });

      const summaryText = selectedLabels.length > 0 ? selectedLabels.join(", ") : "None";

      const summaryEl = calculator.querySelector(`[data-ms-code-summary="${group}"]`);
      if (summaryEl) summaryEl.textContent = summaryText;

      const hiddenInput = submissionForm.querySelector(`[data-ms-code-hidden="${group}"]`);
      if (hiddenInput) hiddenInput.value = summaryText;
    });

    // 4. SET TOTAL IN HIDDEN FIELD
    const hiddenPrice = submissionForm.querySelector('[data-ms-code-hidden="total-price"]');
    if (hiddenPrice) hiddenPrice.value = total;

    // 5. HANDLE OUTPUT DISPLAY FOR GROUPS (subtotal or raw value)
    allGroupNames.forEach((group) => {
      let groupTotal = 0;
      const groupInputs = calculator.querySelectorAll(
        `input[data-summary-group="${group}"]:checked, 
         input[data-feature-group="${group}"]:checked, 
         input[data-summary-group="${group}"][type="range"],
         input[data-ms-code-slider="${group}"],
         select[data-summary-group="${group}"]`
      );

      groupInputs.forEach((input) => {
        const price = parseFloat(input.dataset.price) || 0;
        const perUnit = parseFloat(input.dataset.pricePerUnit) || 0;
        const value = parseFloat(input.value) || 0;
        groupTotal += perUnit > 0 ? value * perUnit : price;
      });

      const groupOutput = calculator.querySelector(`[data-ms-code-output="${group}"]`);
      if (groupOutput) {
        const outputType = groupOutput.dataset.outputType || "price";
        if (outputType === "value") {
          const valueInput = calculator.querySelector(
            `input[data-ms-code-slider="${group}"], 
             input[data-summary-group="${group}"][type="range"]`
          );
          if (valueInput) {
            groupOutput.textContent = valueInput.value;
          }
        } else {
          groupOutput.textContent = `$${Math.round(groupTotal).toLocaleString()}`;
        }
      }
    });
  };

  const setupEventListeners = () => {
    calculator.addEventListener("input", calculatePriceAndSummary);

    // Exclusive checkbox behavior
    const checkboxGroups = {};
    calculator.querySelectorAll('input[type="checkbox"][data-feature-group]').forEach((cb) => {
      const groupName = cb.dataset.featureGroup;
      if (!checkboxGroups[groupName]) checkboxGroups[groupName] = [];
      checkboxGroups[groupName].push(cb);
    });

    Object.values(checkboxGroups).forEach((group) => {
      group.forEach((cb) => {
        cb.addEventListener("change", () => {
          if (cb.checked) {
            group.forEach((otherCb) => {
              if (otherCb !== cb) otherCb.checked = false;
            });
          }
          calculatePriceAndSummary();
        });
      });
    });

    // Reset on submission
    submissionForm.addEventListener("submit", () => {
      setTimeout(() => {
        calculator.querySelectorAll('input[type="checkbox"]').forEach((cb) => (cb.checked = false));
        calculator.querySelectorAll('input[type="range"]').forEach((slider) => {
          slider.value = slider.defaultValue || slider.min || "0";
          slider.dispatchEvent(new Event("input", { bubbles: true }));
        });
        calculatePriceAndSummary();
      }, 100);
    });
  };

  setupEventListeners();
  calculatePriceAndSummary();
});
</script>
Ansicht Memberscript
Modale
Marketing

#158 - Emoji Feedback Widget for Memberstack

This widget lets your logged-in members quickly share how they feel using simple emoji buttons.


<!-- 💙 MEMBERSCRIPT #158 v1.0 💙 - EMOJI FEEDBACK WIDGET -->

<!-- 
  Collect emoji-based feedback from logged-in members. 
  Saves submission state in localStorage and sends data to Make.com.
-->

<script>
  (function() {
    const msDom = window.$memberstackDom;
    if (!msDom) {
      console.error('Memberstack DOM not found.');
      return;
    }

    // Elements
    const widget   = document.querySelector('[data-ms-code="emoji-feedback-widget"]');
    const closeBtn = widget.querySelector('[data-ms-code="emoji-feedback-close"]');
    const buttons  = widget.querySelectorAll('[data-ms-code="emoji-feedback-btn"]');
    const thanks   = widget.querySelector('[data-ms-code="emoji-feedback-thanks"]');

    // Exit early if feedback is done or dismissed
    if (
      localStorage.getItem('emojiFeedbackDone')   === 'true' ||
      localStorage.getItem('emojiFeedbackClosed') === 'true'
    ) {
      widget.style.display = 'none';
      return;
    }

    // Handle close (×) click
    closeBtn.addEventListener('click', e => {
      e.preventDefault();
      localStorage.setItem('emojiFeedbackClosed', 'true');
      widget.style.display = 'none';
    });

    // Fetch member data
    msDom.getCurrentMember()
      .then(({ data: member }) => {
        buttons.forEach(btn => {
          btn.addEventListener('click', () => {
            const score = btn.getAttribute('data-value');

            // Payload for Make.com
            const payload = {
              memberId:  member.id,
              name:      member.customFields["first-name"] || '',
              email:     member.auth.email               || '',
              pageUrl:   window.location.href,
              feedback:  score,
              timestamp: new Date().toISOString()
            };

            // Send feedback to Make
            fetch('https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm', {
              method:  'POST',
              headers: { 'Content-Type': 'application/json' },
              body:    JSON.stringify(payload)
            })
            .then(res => {
              if (!res.ok) throw new Error(res.statusText);

              // Success state: hide emojis, show thank you
              localStorage.setItem('emojiFeedbackDone', 'true');
              widget.querySelector('[data-ms-code="emoji-feedback-buttons"]').style.display = 'none';
              thanks.style.display = 'block';
            })
            .catch(err => {
              console.error('Emoji feedback error:', err);
              // Optionally add error handling UI here
            });
          });
        });
      })
      .catch(err => console.error('Couldn’t get member:', err));
  })();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #158 v1.0 💙 - EMOJI FEEDBACK WIDGET -->

<!-- 
  Collect emoji-based feedback from logged-in members. 
  Saves submission state in localStorage and sends data to Make.com.
-->

<script>
  (function() {
    const msDom = window.$memberstackDom;
    if (!msDom) {
      console.error('Memberstack DOM not found.');
      return;
    }

    // Elements
    const widget   = document.querySelector('[data-ms-code="emoji-feedback-widget"]');
    const closeBtn = widget.querySelector('[data-ms-code="emoji-feedback-close"]');
    const buttons  = widget.querySelectorAll('[data-ms-code="emoji-feedback-btn"]');
    const thanks   = widget.querySelector('[data-ms-code="emoji-feedback-thanks"]');

    // Exit early if feedback is done or dismissed
    if (
      localStorage.getItem('emojiFeedbackDone')   === 'true' ||
      localStorage.getItem('emojiFeedbackClosed') === 'true'
    ) {
      widget.style.display = 'none';
      return;
    }

    // Handle close (×) click
    closeBtn.addEventListener('click', e => {
      e.preventDefault();
      localStorage.setItem('emojiFeedbackClosed', 'true');
      widget.style.display = 'none';
    });

    // Fetch member data
    msDom.getCurrentMember()
      .then(({ data: member }) => {
        buttons.forEach(btn => {
          btn.addEventListener('click', () => {
            const score = btn.getAttribute('data-value');

            // Payload for Make.com
            const payload = {
              memberId:  member.id,
              name:      member.customFields["first-name"] || '',
              email:     member.auth.email               || '',
              pageUrl:   window.location.href,
              feedback:  score,
              timestamp: new Date().toISOString()
            };

            // Send feedback to Make
            fetch('https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm', {
              method:  'POST',
              headers: { 'Content-Type': 'application/json' },
              body:    JSON.stringify(payload)
            })
            .then(res => {
              if (!res.ok) throw new Error(res.statusText);

              // Success state: hide emojis, show thank you
              localStorage.setItem('emojiFeedbackDone', 'true');
              widget.querySelector('[data-ms-code="emoji-feedback-buttons"]').style.display = 'none';
              thanks.style.display = 'block';
            })
            .catch(err => {
              console.error('Emoji feedback error:', err);
              // Optionally add error handling UI here
            });
          });
        });
      })
      .catch(err => console.error('Couldn’t get member:', err));
  })();
</script>
Ansicht Memberscript
Modale
Marketing

#157 - Range Slider Feedback Widget

A simple and friendly slider widget that lets logged-in members give quick feedback (0–10).


<!-- 💙 MEMBERSCRIPT #157 v1.0 💙 - RANGE SLIDER FEEDBACK WIDGET -->

<!-- 
  A lightweight feedback widget that uses a range slider.
  Prevents duplicate submissions with localStorage, and sends feedback to Make.com via webhook.
-->

<script>
  Webflow.push(function() {
    // Silently disable all form submissions
    $('form').submit(function(e) {
      e.preventDefault();
      return false;
    });
  });

  (function () {
    const msDom = window.$memberstackDom;
    if (!msDom) {
      console.error("Memberstack DOM not found. Did you include data-memberstack-app?");
      return;
    }

    const widget = document.querySelector('[data-ms-code="feedback-widget"]');
    const dialog = widget?.querySelector('[data-ms-code="feedback-dialog"]');
    const toggle = widget?.querySelector('[data-ms-code="feedback-toggle"]');
    const slider = widget?.querySelector('[data-ms-code="feedback-range"]');
    const submit = widget?.querySelector('[data-ms-code="feedback-next"]');
    const form   = widget?.closest("form");

    const done   = localStorage.getItem("feedbackDone") === "true";
    const closed = localStorage.getItem("feedbackClosed") === "true";
    if (!widget || !dialog || !toggle || !slider || !submit || done || closed) {
      if (widget) widget.style.display = "none";
      return;
    }

    // Manual close button logic
    toggle.addEventListener("click", (e) => {
      e.preventDefault();
      localStorage.setItem("feedbackClosed", "true");
      widget.style.display = "none";
    });

    // Fetch logged-in member
    msDom.getCurrentMember()
      .then(({ data: member }) => {
        slider.addEventListener("input", () => {
          submit.disabled = false;
        });

        submit.addEventListener("click", () => {
          const payload = {
            memberId: member.id,
            name: member.customFields["first-name"] || "",
            email: member.auth.email || "",
            pageUrl: window.location.href,
            feedback: slider.value,
            timestamp: new Date().toISOString()
          };

          fetch("https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload)
          })
            .then((res) => {
              if (!res.ok) throw new Error(res.statusText);
              localStorage.setItem("feedbackDone", "true");

              const msg = document.createElement("p");
              msg.textContent = "Thanks for your feedback!";
              msg.style.padding = "1em";
              msg.style.textAlign = "center";
              dialog.innerHTML = "";
              dialog.appendChild(msg);
            })
            .catch((err) => {
              console.error("Feedback error:", err);
              dialog.insertAdjacentHTML(
                "beforeend",
                '<p style="color:red; text-align:center;">Oops! Could not send. Try again?</p>'
              );
            });
        });
      })
      .catch((err) => console.error("Couldn’t get member:", err));
  })();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #157 v1.0 💙 - RANGE SLIDER FEEDBACK WIDGET -->

<!-- 
  A lightweight feedback widget that uses a range slider.
  Prevents duplicate submissions with localStorage, and sends feedback to Make.com via webhook.
-->

<script>
  Webflow.push(function() {
    // Silently disable all form submissions
    $('form').submit(function(e) {
      e.preventDefault();
      return false;
    });
  });

  (function () {
    const msDom = window.$memberstackDom;
    if (!msDom) {
      console.error("Memberstack DOM not found. Did you include data-memberstack-app?");
      return;
    }

    const widget = document.querySelector('[data-ms-code="feedback-widget"]');
    const dialog = widget?.querySelector('[data-ms-code="feedback-dialog"]');
    const toggle = widget?.querySelector('[data-ms-code="feedback-toggle"]');
    const slider = widget?.querySelector('[data-ms-code="feedback-range"]');
    const submit = widget?.querySelector('[data-ms-code="feedback-next"]');
    const form   = widget?.closest("form");

    const done   = localStorage.getItem("feedbackDone") === "true";
    const closed = localStorage.getItem("feedbackClosed") === "true";
    if (!widget || !dialog || !toggle || !slider || !submit || done || closed) {
      if (widget) widget.style.display = "none";
      return;
    }

    // Manual close button logic
    toggle.addEventListener("click", (e) => {
      e.preventDefault();
      localStorage.setItem("feedbackClosed", "true");
      widget.style.display = "none";
    });

    // Fetch logged-in member
    msDom.getCurrentMember()
      .then(({ data: member }) => {
        slider.addEventListener("input", () => {
          submit.disabled = false;
        });

        submit.addEventListener("click", () => {
          const payload = {
            memberId: member.id,
            name: member.customFields["first-name"] || "",
            email: member.auth.email || "",
            pageUrl: window.location.href,
            feedback: slider.value,
            timestamp: new Date().toISOString()
          };

          fetch("https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload)
          })
            .then((res) => {
              if (!res.ok) throw new Error(res.statusText);
              localStorage.setItem("feedbackDone", "true");

              const msg = document.createElement("p");
              msg.textContent = "Thanks for your feedback!";
              msg.style.padding = "1em";
              msg.style.textAlign = "center";
              dialog.innerHTML = "";
              dialog.appendChild(msg);
            })
            .catch((err) => {
              console.error("Feedback error:", err);
              dialog.insertAdjacentHTML(
                "beforeend",
                '<p style="color:red; text-align:center;">Oops! Could not send. Try again?</p>'
              );
            });
        });
      })
      .catch((err) => console.error("Couldn’t get member:", err));
  })();
</script>
Ansicht Memberscript
Sicherheit
Bedingte Sichtbarkeit

#156 - Encrypt Sensitive Data Before Sending to Memberstack

This script protects sensitive user data by encrypting it in the browser before it’s sent to Memberstack.


<!-- 💙 MEMBERSCRIPT #156 v1.0 💙 - ENCRYPT SENSITIVE DATA BEFORE SENDING TO MEMBERSTACK -->

<!-- 
  This script encrypts input fields before they're submitted to Memberstack,
  using AES-GCM with a passphrase-based modal. 
-->

<script>
  document.addEventListener('DOMContentLoaded', function () {
    (function () {
      const enc = new TextEncoder();
      const dec = new TextDecoder();

      // Show the passphrase modal
      function showModal() {
        return new Promise(resolve => {
          const modal = document.querySelector('[data-ms-code="encrypt-modal"]');
          if (!modal) return alert('Encryption modal missing from the page.');

          const input = modal.querySelector('[data-ms-code="pass-input"]');
          const remember = modal.querySelector('[data-ms-code="remember-pass"]');
          const submit = modal.querySelector('[data-ms-code="submit-pass"]');

          const closeButtons = modal.querySelectorAll(
            '[data-ms-code="close-encrypt-modal"], [data-ms-code="close-encrypt-icon"]'
          );

          modal.style.display = 'flex';
          input.value = '';
          input.focus();

          const cleanup = () => {
            modal.style.display = 'none';
          };

          if (submit) {
            submit.onclick = () => {
              const pass = input.value;
              const keep = remember.checked;
              cleanup();
              resolve({ pass, remember: keep });
            };
          }

          closeButtons.forEach(btn => {
            btn.onclick = () => {
              cleanup();
              resolve({ pass: null });
            };
          });
        });
      }

      // Derive AES key using PBKDF2
      async function deriveKey(pass, salt) {
        const keyMaterial = await crypto.subtle.importKey(
          'raw',
          enc.encode(pass),
          { name: 'PBKDF2' },
          false,
          ['deriveKey']
        );
        return crypto.subtle.deriveKey(
          {
            name: 'PBKDF2',
            salt: salt,
            iterations: 100000,
            hash: 'SHA-256'
          },
          keyMaterial,
          { name: 'AES-GCM', length: 256 },
          false,
          ['encrypt', 'decrypt']
        );
      }

      // Encrypt a string
      async function encryptText(text, pass) {
        const salt = crypto.getRandomValues(new Uint8Array(16));
        const iv = crypto.getRandomValues(new Uint8Array(12));
        const key = await deriveKey(pass, salt);
        const encrypted = await crypto.subtle.encrypt(
          { name: 'AES-GCM', iv },
          key,
          enc.encode(text)
        );

        return [
          btoa(String.fromCharCode(...salt)),
          btoa(String.fromCharCode(...iv)),
          btoa(String.fromCharCode(...new Uint8Array(encrypted)))
        ].join(':');
      }

      // Decrypt a string
      async function decryptText(encrypted, pass) {
        const [saltB64, ivB64, dataB64] = encrypted.split(':');
        if (!saltB64 || !ivB64 || !dataB64) throw new Error('Invalid format');

        const salt = Uint8Array.from(atob(saltB64), c => c.charCodeAt(0));
        const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
        const data = Uint8Array.from(atob(dataB64), c => c.charCodeAt(0));
        const key = await deriveKey(pass, salt);
        const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data);
        return dec.decode(decrypted);
      }

      // Encrypt and submit form
      document.querySelectorAll('[data-ms-code-encrypt]').forEach(btn => {
        if (!btn.hasAttribute('data-ms-encrypt-attached')) {
          btn.addEventListener('click', async e => {
            e.preventDefault();

            let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
            if (!passphrase) {
              const { pass, remember } = await showModal();
              if (!pass) return;
              passphrase = pass;
              if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
            }

            const fields = document.querySelectorAll('[data-ms-code-id]');
            for (let field of fields) {
              const value = field.value.trim();
              if (!value) continue;
              try {
                const encrypted = await encryptText(value, passphrase);
                field.value = encrypted;
              } catch (err) {
                console.error('Encryption error:', err);
                alert('Encryption failed.');
                return;
              }
            }

            const form = btn.closest('form');
            if (form) form.requestSubmit();
          });

          btn.setAttribute('data-ms-encrypt-attached', 'true');
        }
      });

      // Add decrypt button logic
      function attachDecryptButton() {
        const decryptBtn = document.querySelector('[data-ms-code="decrypt-all"]');
        if (!decryptBtn || decryptBtn.hasAttribute('data-ms-decrypt-attached')) return;

        decryptBtn.addEventListener('click', async e => {
          e.preventDefault();

          const encryptedFields = document.querySelectorAll('[data-ms-code-id]');
          if (encryptedFields.length === 0) return alert('No fields to decrypt.');

          let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
          if (!passphrase) {
            const { pass, remember } = await showModal();
            if (!pass) return;
            passphrase = pass;
            if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
          }

          for (let field of encryptedFields) {
            const encrypted = field.value.trim();
            if (!encrypted) continue;
            try {
              const decrypted = await decryptText(encrypted, passphrase);
              field.value = decrypted;
            } catch (err) {
              console.error('Decryption error:', err);
              alert('One or more fields failed to decrypt.');
              return;
            }
          }
        });

        decryptBtn.setAttribute('data-ms-decrypt-attached', 'true');
      }

      attachDecryptButton();
    })();
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #156 v1.0 💙 - ENCRYPT SENSITIVE DATA BEFORE SENDING TO MEMBERSTACK -->

<!-- 
  This script encrypts input fields before they're submitted to Memberstack,
  using AES-GCM with a passphrase-based modal. 
-->

<script>
  document.addEventListener('DOMContentLoaded', function () {
    (function () {
      const enc = new TextEncoder();
      const dec = new TextDecoder();

      // Show the passphrase modal
      function showModal() {
        return new Promise(resolve => {
          const modal = document.querySelector('[data-ms-code="encrypt-modal"]');
          if (!modal) return alert('Encryption modal missing from the page.');

          const input = modal.querySelector('[data-ms-code="pass-input"]');
          const remember = modal.querySelector('[data-ms-code="remember-pass"]');
          const submit = modal.querySelector('[data-ms-code="submit-pass"]');

          const closeButtons = modal.querySelectorAll(
            '[data-ms-code="close-encrypt-modal"], [data-ms-code="close-encrypt-icon"]'
          );

          modal.style.display = 'flex';
          input.value = '';
          input.focus();

          const cleanup = () => {
            modal.style.display = 'none';
          };

          if (submit) {
            submit.onclick = () => {
              const pass = input.value;
              const keep = remember.checked;
              cleanup();
              resolve({ pass, remember: keep });
            };
          }

          closeButtons.forEach(btn => {
            btn.onclick = () => {
              cleanup();
              resolve({ pass: null });
            };
          });
        });
      }

      // Derive AES key using PBKDF2
      async function deriveKey(pass, salt) {
        const keyMaterial = await crypto.subtle.importKey(
          'raw',
          enc.encode(pass),
          { name: 'PBKDF2' },
          false,
          ['deriveKey']
        );
        return crypto.subtle.deriveKey(
          {
            name: 'PBKDF2',
            salt: salt,
            iterations: 100000,
            hash: 'SHA-256'
          },
          keyMaterial,
          { name: 'AES-GCM', length: 256 },
          false,
          ['encrypt', 'decrypt']
        );
      }

      // Encrypt a string
      async function encryptText(text, pass) {
        const salt = crypto.getRandomValues(new Uint8Array(16));
        const iv = crypto.getRandomValues(new Uint8Array(12));
        const key = await deriveKey(pass, salt);
        const encrypted = await crypto.subtle.encrypt(
          { name: 'AES-GCM', iv },
          key,
          enc.encode(text)
        );

        return [
          btoa(String.fromCharCode(...salt)),
          btoa(String.fromCharCode(...iv)),
          btoa(String.fromCharCode(...new Uint8Array(encrypted)))
        ].join(':');
      }

      // Decrypt a string
      async function decryptText(encrypted, pass) {
        const [saltB64, ivB64, dataB64] = encrypted.split(':');
        if (!saltB64 || !ivB64 || !dataB64) throw new Error('Invalid format');

        const salt = Uint8Array.from(atob(saltB64), c => c.charCodeAt(0));
        const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
        const data = Uint8Array.from(atob(dataB64), c => c.charCodeAt(0));
        const key = await deriveKey(pass, salt);
        const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data);
        return dec.decode(decrypted);
      }

      // Encrypt and submit form
      document.querySelectorAll('[data-ms-code-encrypt]').forEach(btn => {
        if (!btn.hasAttribute('data-ms-encrypt-attached')) {
          btn.addEventListener('click', async e => {
            e.preventDefault();

            let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
            if (!passphrase) {
              const { pass, remember } = await showModal();
              if (!pass) return;
              passphrase = pass;
              if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
            }

            const fields = document.querySelectorAll('[data-ms-code-id]');
            for (let field of fields) {
              const value = field.value.trim();
              if (!value) continue;
              try {
                const encrypted = await encryptText(value, passphrase);
                field.value = encrypted;
              } catch (err) {
                console.error('Encryption error:', err);
                alert('Encryption failed.');
                return;
              }
            }

            const form = btn.closest('form');
            if (form) form.requestSubmit();
          });

          btn.setAttribute('data-ms-encrypt-attached', 'true');
        }
      });

      // Add decrypt button logic
      function attachDecryptButton() {
        const decryptBtn = document.querySelector('[data-ms-code="decrypt-all"]');
        if (!decryptBtn || decryptBtn.hasAttribute('data-ms-decrypt-attached')) return;

        decryptBtn.addEventListener('click', async e => {
          e.preventDefault();

          const encryptedFields = document.querySelectorAll('[data-ms-code-id]');
          if (encryptedFields.length === 0) return alert('No fields to decrypt.');

          let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
          if (!passphrase) {
            const { pass, remember } = await showModal();
            if (!pass) return;
            passphrase = pass;
            if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
          }

          for (let field of encryptedFields) {
            const encrypted = field.value.trim();
            if (!encrypted) continue;
            try {
              const decrypted = await decryptText(encrypted, passphrase);
              field.value = decrypted;
            } catch (err) {
              console.error('Decryption error:', err);
              alert('One or more fields failed to decrypt.');
              return;
            }
          }
        });

        decryptBtn.setAttribute('data-ms-decrypt-attached', 'true');
      }

      attachDecryptButton();
    })();
  });
</script>
Ansicht Memberscript
Benutzerdefinierte Abläufe
Integration

#155 - Bulk Update Customer Subscriptions via Stripe & Make

Bulk update existing members to a new pricing plan with Stripe and Make

v0.1
Ansicht Memberscript
JSON
Sicherheit

#154 - Two-Factor Authentication (2FA) for Memberstack Logins

Add an extra layer of security to your Memberstack logins by enabling Two-Factor Authentication (2FA).


<!--
  MEMBERSCRIPT #154
  ---------------------------------
  LOGIN PAGE SCRIPT
-->

<script>
(async function() {
  const delay = ms => new Promise(r => setTimeout(r, ms));

  async function routeLogin() {
    try {
      // Check if Memberstack is loaded
      if (!window.$memberstackDom) {
        console.log("Memberstack not loaded yet");
        return;
      }

      // Get current member
      const { data: member } = await window.$memberstackDom.getCurrentMember();
      if (!member) return; // Exit if not logged in

      // Get member JSON data
      const jsonResponse = await window.$memberstackDom.getMemberJSON();
      const memberData = jsonResponse.data || {};
      
      // Check if 2FA is enabled
      const needs2FA = memberData["2fa_enabled"] === true || 
                      jsonResponse["2fa_enabled"] === true;
      
      // Check session storage for verification status
      const verified = sessionStorage.getItem("2fa_verified") === "true";

      console.log("2FA Status:", { 
        enabled: needs2FA, 
        verified: verified,
        currentPath: window.location.pathname
      });

      // Handle 2FA redirect
      if (needs2FA && !verified) {
        if (!window.location.pathname.includes("/2fa-verify")) {
          console.log("Redirecting to /2fa-verify");
          window.location.href = "/2fa-verify";
        }
        return; // Stop further execution
      }

      // Handle success redirect
      if (!window.location.pathname.includes("/success")) {
        console.log("Redirecting to /success");
        
        // Remove Memberstack's auto-redirect if login form exists
        const loginForm = document.querySelector('[data-ms-form="login"]');
        if (loginForm) {
          loginForm.removeAttribute('data-ms-redirect');
        }
        
        window.location.href = "/success";
      }
    } catch (err) {
      console.error("2FA routing error:", err);
    }
  }

  // Wait for Memberstack to initialize
  await delay(300);
  routeLogin();
  
  // Poll with cleanup
  const pollInterval = setInterval(routeLogin, 500);
  setTimeout(() => clearInterval(pollInterval), 10000);
})();
</script>

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  SETTINGS PAGE SCRIPT
-->

<!-- Load otplib preset-browser -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>

<script>
document.addEventListener("DOMContentLoaded", async () => {
  const ms = window.$memberstackDom;
  const { data: member } = await ms.getCurrentMember();
  if (!member) return;

  const checkbox = document.querySelector('[data-ms-code="enable-2fa"]');
  const qrContainer = document.querySelector('[data-ms-code="2fa-qr-container"]');
  const qrImage = document.querySelector('[data-ms-code="2fa-qr-image"]');

  // Hide QR container by default
  qrContainer.style.display = "none";

  // Load member JSON and ensure data object exists
  const jsonObj = await ms.getMemberJSON(); // { data: ... } or { data: null }
  if (!jsonObj.data) jsonObj.data = {};
  const inner = jsonObj.data;

  // Set checkbox initial state
  const enabled = inner["2fa_enabled"] === true;
  checkbox.checked = enabled;

  checkbox.addEventListener("change", async (e) => {
    const isChecked = e.target.checked;

    // Reload member and JSON
    const { data: member } = await ms.getCurrentMember();
    const jsonObj2 = await ms.getMemberJSON();
    if (!jsonObj2.data) jsonObj2.data = {};
    const inner2 = jsonObj2.data;

    if (isChecked) {
      // Enable 2FA: generate secret and QR
      const secret = window.otplib.authenticator.generateSecret();
      const uri = window.otplib.authenticator.keyuri(member.email, "Memberscript #154", secret);
      qrImage.src = "https://api.qrserver.com/v1/create-qr-code/?data=" + encodeURIComponent(uri);
      qrContainer.style.display = "flex";

      inner2["2fa_enabled"] = true;
      inner2["2fa_secret"] = secret;

      await ms.updateMember({
        customFields: { "2fa-enabled": "true" }
      });
    } else {
      // Disable 2FA: remove secret and hide QR
      qrContainer.style.display = "none";

      inner2["2fa_enabled"] = false;
      delete inner2["2fa_secret"];

      await ms.updateMember({
        customFields: { "2fa-enabled": "false" }
      });
    }

    // Persist only nested JSON
    await ms.updateMemberJSON({ json: inner2 });

    // ✅ Debugging: check result
    const check = await ms.getMemberJSON();
    console.log(check);
  });
});
</script>

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  SUCCESS / DASHBOARD PAGE SCRIPT
-->

<script>
  window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
    if (!member) return; // Not logged in, no redirect

    window.$memberstackDom.getMemberJSON().then(json => {
      const enabled = json["2fa_enabled"];
      const verified = sessionStorage.getItem("2fa_verified") === "true";
      if (enabled && !verified && window.location.pathname !== "/2fa-verify") {
        window.location.href = "/2fa-verify";
      }
    });
  });
</script>

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  TWO FACTOR VERIFICATION PAGE SCRIPT
-->

<!-- Include the Buffer polyfill -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>

<!-- Include the otplib library -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>

<script>
document.addEventListener("DOMContentLoaded", async () => {
  const memberstack = window.$memberstackDom;
  const { data: member } = await memberstack.getCurrentMember();
  if (!member) return;

  const form = document.querySelector('[data-ms-form="2fa-verification"]');
  if (!form) return;

  const codeInput = form.querySelector('[data-ms-code="2fa-code"]');
  const errorContainer = form.querySelector('[data-ms-error="2fa-code"]');

  function showError(message) {
    if (errorContainer) {
      errorContainer.textContent = message;
      errorContainer.style.display = 'block';
    } else {
      alert(message);
    }
  }

  function clearError() {
    if (errorContainer) {
      errorContainer.textContent = '';
      errorContainer.style.display = 'none';
    }
  }

  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    e.stopImmediatePropagation(); // important to stop other listeners
    clearError();

    const code = codeInput.value.trim();
    const { data: json } = await memberstack.getMemberJSON();
    const secret = json?.["2fa_secret"];

    if (!secret || !code) {
      const msg = form.getAttribute('data-ms-error-msg-missing') || 'Please enter your 2FA code';
      showError(msg);
      return;
    }

    if (otplib.authenticator.check(code, secret)) {
      sessionStorage.setItem('2fa_verified', 'true');
      window.location.href = '/success';
    } else {
      const msg = form.getAttribute('data-ms-error-msg-invalid') || 'Oops, the 2FA code is incorrect. Try again.';
      showError(msg);
    }
  });
});
</script>
v0.1

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  LOGIN PAGE SCRIPT
-->

<script>
(async function() {
  const delay = ms => new Promise(r => setTimeout(r, ms));

  async function routeLogin() {
    try {
      // Check if Memberstack is loaded
      if (!window.$memberstackDom) {
        console.log("Memberstack not loaded yet");
        return;
      }

      // Get current member
      const { data: member } = await window.$memberstackDom.getCurrentMember();
      if (!member) return; // Exit if not logged in

      // Get member JSON data
      const jsonResponse = await window.$memberstackDom.getMemberJSON();
      const memberData = jsonResponse.data || {};
      
      // Check if 2FA is enabled
      const needs2FA = memberData["2fa_enabled"] === true || 
                      jsonResponse["2fa_enabled"] === true;
      
      // Check session storage for verification status
      const verified = sessionStorage.getItem("2fa_verified") === "true";

      console.log("2FA Status:", { 
        enabled: needs2FA, 
        verified: verified,
        currentPath: window.location.pathname
      });

      // Handle 2FA redirect
      if (needs2FA && !verified) {
        if (!window.location.pathname.includes("/2fa-verify")) {
          console.log("Redirecting to /2fa-verify");
          window.location.href = "/2fa-verify";
        }
        return; // Stop further execution
      }

      // Handle success redirect
      if (!window.location.pathname.includes("/success")) {
        console.log("Redirecting to /success");
        
        // Remove Memberstack's auto-redirect if login form exists
        const loginForm = document.querySelector('[data-ms-form="login"]');
        if (loginForm) {
          loginForm.removeAttribute('data-ms-redirect');
        }
        
        window.location.href = "/success";
      }
    } catch (err) {
      console.error("2FA routing error:", err);
    }
  }

  // Wait for Memberstack to initialize
  await delay(300);
  routeLogin();
  
  // Poll with cleanup
  const pollInterval = setInterval(routeLogin, 500);
  setTimeout(() => clearInterval(pollInterval), 10000);
})();
</script>

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  SETTINGS PAGE SCRIPT
-->

<!-- Load otplib preset-browser -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>

<script>
document.addEventListener("DOMContentLoaded", async () => {
  const ms = window.$memberstackDom;
  const { data: member } = await ms.getCurrentMember();
  if (!member) return;

  const checkbox = document.querySelector('[data-ms-code="enable-2fa"]');
  const qrContainer = document.querySelector('[data-ms-code="2fa-qr-container"]');
  const qrImage = document.querySelector('[data-ms-code="2fa-qr-image"]');

  // Hide QR container by default
  qrContainer.style.display = "none";

  // Load member JSON and ensure data object exists
  const jsonObj = await ms.getMemberJSON(); // { data: ... } or { data: null }
  if (!jsonObj.data) jsonObj.data = {};
  const inner = jsonObj.data;

  // Set checkbox initial state
  const enabled = inner["2fa_enabled"] === true;
  checkbox.checked = enabled;

  checkbox.addEventListener("change", async (e) => {
    const isChecked = e.target.checked;

    // Reload member and JSON
    const { data: member } = await ms.getCurrentMember();
    const jsonObj2 = await ms.getMemberJSON();
    if (!jsonObj2.data) jsonObj2.data = {};
    const inner2 = jsonObj2.data;

    if (isChecked) {
      // Enable 2FA: generate secret and QR
      const secret = window.otplib.authenticator.generateSecret();
      const uri = window.otplib.authenticator.keyuri(member.email, "Memberscript #154", secret);
      qrImage.src = "https://api.qrserver.com/v1/create-qr-code/?data=" + encodeURIComponent(uri);
      qrContainer.style.display = "flex";

      inner2["2fa_enabled"] = true;
      inner2["2fa_secret"] = secret;

      await ms.updateMember({
        customFields: { "2fa-enabled": "true" }
      });
    } else {
      // Disable 2FA: remove secret and hide QR
      qrContainer.style.display = "none";

      inner2["2fa_enabled"] = false;
      delete inner2["2fa_secret"];

      await ms.updateMember({
        customFields: { "2fa-enabled": "false" }
      });
    }

    // Persist only nested JSON
    await ms.updateMemberJSON({ json: inner2 });

    // ✅ Debugging: check result
    const check = await ms.getMemberJSON();
    console.log(check);
  });
});
</script>

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  SUCCESS / DASHBOARD PAGE SCRIPT
-->

<script>
  window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
    if (!member) return; // Not logged in, no redirect

    window.$memberstackDom.getMemberJSON().then(json => {
      const enabled = json["2fa_enabled"];
      const verified = sessionStorage.getItem("2fa_verified") === "true";
      if (enabled && !verified && window.location.pathname !== "/2fa-verify") {
        window.location.href = "/2fa-verify";
      }
    });
  });
</script>

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  TWO FACTOR VERIFICATION PAGE SCRIPT
-->

<!-- Include the Buffer polyfill -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>

<!-- Include the otplib library -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>

<script>
document.addEventListener("DOMContentLoaded", async () => {
  const memberstack = window.$memberstackDom;
  const { data: member } = await memberstack.getCurrentMember();
  if (!member) return;

  const form = document.querySelector('[data-ms-form="2fa-verification"]');
  if (!form) return;

  const codeInput = form.querySelector('[data-ms-code="2fa-code"]');
  const errorContainer = form.querySelector('[data-ms-error="2fa-code"]');

  function showError(message) {
    if (errorContainer) {
      errorContainer.textContent = message;
      errorContainer.style.display = 'block';
    } else {
      alert(message);
    }
  }

  function clearError() {
    if (errorContainer) {
      errorContainer.textContent = '';
      errorContainer.style.display = 'none';
    }
  }

  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    e.stopImmediatePropagation(); // important to stop other listeners
    clearError();

    const code = codeInput.value.trim();
    const { data: json } = await memberstack.getMemberJSON();
    const secret = json?.["2fa_secret"];

    if (!secret || !code) {
      const msg = form.getAttribute('data-ms-error-msg-missing') || 'Please enter your 2FA code';
      showError(msg);
      return;
    }

    if (otplib.authenticator.check(code, secret)) {
      sessionStorage.setItem('2fa_verified', 'true');
      window.location.href = '/success';
    } else {
      const msg = form.getAttribute('data-ms-error-msg-invalid') || 'Oops, the 2FA code is incorrect. Try again.';
      showError(msg);
    }
  });
});
</script>
Ansicht Memberscript
UX
Erreichbarkeit

#153 - Instant Multilingual Site with Google Translate

Make your Webflow site multilingual in minutes with Google Translate and Memberstack



<!-- 💙 MEMBERSCRIPT #153 v1.0 💙 - FREE MULTILINGUAL SITE WITH GOOGLE TRANSLATE -->
<script>
  // 1. Inject Google Translate script dynamically
  const gtScript = document.createElement('script');
  gtScript.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
  document.head.appendChild(gtScript);

  // 2. Inject CSS to hide Google Translate UI
  const style = document.createElement('style');
  style.innerHTML = `
    body { top: 0px !important; position: static !important; }
    .goog-te-banner-frame, .skiptranslate,
    #goog-gt-tt, .goog-te-balloon-frame,
    .goog-text-highlight {
      display: none !important;
      background: none !important;
      box-shadow: none !important;
    }
  `;
  document.head.appendChild(style);

  // 3. Google Translate init
  window.googleTranslateElementInit = function () {
    new google.translate.TranslateElement({
      pageLanguage: 'en',
      layout: google.translate.TranslateElement.FloatPosition.TOP_LEFT
    }, 'google_translate_element');
  };

  // 4. Helper to get cookies
  function getCookie(name) {
    const cookies = document.cookie.split(';');
    for (let cookie of cookies) {
      const [key, value] = cookie.trim().split('=');
      if (key === name) return decodeURIComponent(value);
    }
    return null;
  }

  // 5. Language map
  const languageMap = new Map([
    ["af","Afrikaans"], ["sq","Albanian"], ["ar","Arabic"], ["hy","Armenian"],
    ["az","Azerbaijani"], ["eu","Basque"], ["be","Belarusian"], ["bg","Bulgarian"],
    ["ca","Catalan"], ["zh-CN","ChineseSimplified"], ["zh-TW","ChineseTraditional"], 
    ["hr","Croatian"], ["cs","Czech"], ["da","Danish"], ["nl","Dutch"], ["de","German"],
    ["en","English"], ["et","Estonian"], ["tl","Filipino"], ["fi","Finnish"], 
    ["fr","French"], ["gl","Galician"], ["ka","Georgian"], ["el","Greek"], 
    ["ht","Haitian"], ["iw","Hebrew"], ["hi","Hindi"], ["hu","Hungarian"], 
    ["is","Icelandic"], ["id","Indonesian"], ["ga","Irish"], ["it","Italian"], 
    ["ja","Japanese"], ["ko","Korean"], ["lv","Latvian"], ["lt","Lithuanian"], 
    ["mk","Macedonian"], ["ms","Malay"], ["mt","Maltese"], ["no","Norwegian"], 
    ["fa","Persian"], ["pl","Polish"], ["pt","Portuguese"], ["ro","Romanian"], 
    ["ru","Russian"], ["sr","Serbian"], ["sk","Slovak"], ["sl","Slovenian"], 
    ["es","Spanish"], ["sw","Swahili"], ["sv","Swedish"], ["th","Thai"], 
    ["tr","Turkish"], ["uk","Ukrainian"], ["ur","Urdu"], ["vi","Vietnamese"], 
    ["cy","Welsh"], ["yi","Yiddish"]
  ]);

  // 6. Detect current language
  let currentLang = getCookie("googtrans")?.split("/").pop() || "en";

  // 7. Show language-specific content & set up language switch
  document.addEventListener("DOMContentLoaded", function () {
    const readableLang = languageMap.get(currentLang);
    const langClass = `.languagespecific.${readableLang?.toLowerCase()}specific`;
    const fallbackClass = `.languagespecific.englishspecific`;

    if (document.querySelector(langClass)) {
      document.querySelectorAll(langClass).forEach(el => el.style.display = 'block');
    } else {
      document.querySelectorAll(fallbackClass).forEach(el => el.style.display = 'block');
    }

    document.querySelectorAll('[data-ms-code-lang-select]').forEach(el => {
      el.addEventListener('click', function (e) {
        e.preventDefault();
        const selectedLang = this.getAttribute('data-ms-code-lang');

        if (selectedLang === 'en') {
          document.cookie = "googtrans=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
          document.cookie = "googtrans=;domain=.webflow.io;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
          window.location.hash = "";
          setTimeout(() => location.reload(true), 100);
        } else {
          const combo = document.querySelector('.goog-te-combo');
          if (combo) combo.value = selectedLang;
          window.location.hash = "#googtrans(en|" + selectedLang + ")";
          location.reload();
        }
      });
    });
  });
</script>
v0.1


<!-- 💙 MEMBERSCRIPT #153 v1.0 💙 - FREE MULTILINGUAL SITE WITH GOOGLE TRANSLATE -->
<script>
  // 1. Inject Google Translate script dynamically
  const gtScript = document.createElement('script');
  gtScript.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
  document.head.appendChild(gtScript);

  // 2. Inject CSS to hide Google Translate UI
  const style = document.createElement('style');
  style.innerHTML = `
    body { top: 0px !important; position: static !important; }
    .goog-te-banner-frame, .skiptranslate,
    #goog-gt-tt, .goog-te-balloon-frame,
    .goog-text-highlight {
      display: none !important;
      background: none !important;
      box-shadow: none !important;
    }
  `;
  document.head.appendChild(style);

  // 3. Google Translate init
  window.googleTranslateElementInit = function () {
    new google.translate.TranslateElement({
      pageLanguage: 'en',
      layout: google.translate.TranslateElement.FloatPosition.TOP_LEFT
    }, 'google_translate_element');
  };

  // 4. Helper to get cookies
  function getCookie(name) {
    const cookies = document.cookie.split(';');
    for (let cookie of cookies) {
      const [key, value] = cookie.trim().split('=');
      if (key === name) return decodeURIComponent(value);
    }
    return null;
  }

  // 5. Language map
  const languageMap = new Map([
    ["af","Afrikaans"], ["sq","Albanian"], ["ar","Arabic"], ["hy","Armenian"],
    ["az","Azerbaijani"], ["eu","Basque"], ["be","Belarusian"], ["bg","Bulgarian"],
    ["ca","Catalan"], ["zh-CN","ChineseSimplified"], ["zh-TW","ChineseTraditional"], 
    ["hr","Croatian"], ["cs","Czech"], ["da","Danish"], ["nl","Dutch"], ["de","German"],
    ["en","English"], ["et","Estonian"], ["tl","Filipino"], ["fi","Finnish"], 
    ["fr","French"], ["gl","Galician"], ["ka","Georgian"], ["el","Greek"], 
    ["ht","Haitian"], ["iw","Hebrew"], ["hi","Hindi"], ["hu","Hungarian"], 
    ["is","Icelandic"], ["id","Indonesian"], ["ga","Irish"], ["it","Italian"], 
    ["ja","Japanese"], ["ko","Korean"], ["lv","Latvian"], ["lt","Lithuanian"], 
    ["mk","Macedonian"], ["ms","Malay"], ["mt","Maltese"], ["no","Norwegian"], 
    ["fa","Persian"], ["pl","Polish"], ["pt","Portuguese"], ["ro","Romanian"], 
    ["ru","Russian"], ["sr","Serbian"], ["sk","Slovak"], ["sl","Slovenian"], 
    ["es","Spanish"], ["sw","Swahili"], ["sv","Swedish"], ["th","Thai"], 
    ["tr","Turkish"], ["uk","Ukrainian"], ["ur","Urdu"], ["vi","Vietnamese"], 
    ["cy","Welsh"], ["yi","Yiddish"]
  ]);

  // 6. Detect current language
  let currentLang = getCookie("googtrans")?.split("/").pop() || "en";

  // 7. Show language-specific content & set up language switch
  document.addEventListener("DOMContentLoaded", function () {
    const readableLang = languageMap.get(currentLang);
    const langClass = `.languagespecific.${readableLang?.toLowerCase()}specific`;
    const fallbackClass = `.languagespecific.englishspecific`;

    if (document.querySelector(langClass)) {
      document.querySelectorAll(langClass).forEach(el => el.style.display = 'block');
    } else {
      document.querySelectorAll(fallbackClass).forEach(el => el.style.display = 'block');
    }

    document.querySelectorAll('[data-ms-code-lang-select]').forEach(el => {
      el.addEventListener('click', function (e) {
        e.preventDefault();
        const selectedLang = this.getAttribute('data-ms-code-lang');

        if (selectedLang === 'en') {
          document.cookie = "googtrans=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
          document.cookie = "googtrans=;domain=.webflow.io;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
          window.location.hash = "";
          setTimeout(() => location.reload(true), 100);
        } else {
          const combo = document.querySelector('.goog-te-combo');
          if (combo) combo.value = selectedLang;
          window.location.hash = "#googtrans(en|" + selectedLang + ")";
          location.reload();
        }
      });
    });
  });
</script>
Ansicht Memberscript
Sicherheit
UX

#152 - OTP Verification via WhatsApp in Webflow

Verify phone numbers via WhatsApp before allowing form submissions in Webflow.


<!-- 💙 MEMBERSCRIPT #152 v1.0 💙 - OTP VERIFICATION VIA WHATSAPP IN WEBFLOW -->
<script>
  document.addEventListener("DOMContentLoaded", function () {
    const phoneInput = document.querySelector('[data-ms-code="phone-number"]');
    const form = phoneInput?.closest("form");
    const submitBtn = form?.querySelector('[type="submit"]');

    let isVerified = false;
    let originalMsFormValue = "signup"; // update this if your form uses another Memberstack action

    if (phoneInput && form && submitBtn && window.$WhatsAuthForm) {
      // Temporarily disable Memberstack
      form.removeAttribute("data-ms-form");

      // Create error message
      const errorMsg = document.createElement('div');
      errorMsg.style.color = 'red';
      errorMsg.style.marginTop = '8px';
      errorMsg.style.display = 'none';
      errorMsg.textContent = '⚠️ Please verify your phone number via WhatsApp.';
      phoneInput.parentNode.appendChild(errorMsg);

      // Initially disable submit
      submitBtn.disabled = true;
      submitBtn.style.opacity = 0.6;
      submitBtn.style.cursor = 'not-allowed';

      // Init WhatsAuth
      window.$WhatsAuthForm.init({
        inputSelector: '[data-ms-code="phone-number"]',
        apiKey: "k07Zj8EwdAIzHzLcLPQh-5jCuREbSKXG", //REPLACE WITH YOUR API KEY
        placeholder: phoneInput.getAttribute("data-ms-placeholder") || "",
        primaryColor: phoneInput.getAttribute("data-ms-primary-color") || "",
        secondaryColor: phoneInput.getAttribute("data-ms-secondary-color") || "",
        btnText: phoneInput.getAttribute("data-ms-btn-text") || ""
      });

      // Watch for success class from WhatsAuth
      const observer = new MutationObserver(() => {
        if (phoneInput.classList.contains("whatsauth-success") && !isVerified) {
          isVerified = true;

          // Enable submit
          submitBtn.disabled = false;
          submitBtn.style.opacity = 1;
          submitBtn.style.cursor = 'pointer';

          // Hide error
          errorMsg.style.display = 'none';

          // Re-enable Memberstack
          form.setAttribute("data-ms-form", originalMsFormValue);
        }
      });

      observer.observe(phoneInput, { attributes: true, attributeFilter: ["class"] });

      // Final form safeguard
      form.addEventListener("submit", function (e) {
        if (!isVerified) {
          e.preventDefault();
          errorMsg.style.display = 'block';
        }
      });

      submitBtn.addEventListener("click", function (e) {
        if (!isVerified) {
          e.preventDefault();
          errorMsg.style.display = 'block';
        }
      });
    }
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #152 v1.0 💙 - OTP VERIFICATION VIA WHATSAPP IN WEBFLOW -->
<script>
  document.addEventListener("DOMContentLoaded", function () {
    const phoneInput = document.querySelector('[data-ms-code="phone-number"]');
    const form = phoneInput?.closest("form");
    const submitBtn = form?.querySelector('[type="submit"]');

    let isVerified = false;
    let originalMsFormValue = "signup"; // update this if your form uses another Memberstack action

    if (phoneInput && form && submitBtn && window.$WhatsAuthForm) {
      // Temporarily disable Memberstack
      form.removeAttribute("data-ms-form");

      // Create error message
      const errorMsg = document.createElement('div');
      errorMsg.style.color = 'red';
      errorMsg.style.marginTop = '8px';
      errorMsg.style.display = 'none';
      errorMsg.textContent = '⚠️ Please verify your phone number via WhatsApp.';
      phoneInput.parentNode.appendChild(errorMsg);

      // Initially disable submit
      submitBtn.disabled = true;
      submitBtn.style.opacity = 0.6;
      submitBtn.style.cursor = 'not-allowed';

      // Init WhatsAuth
      window.$WhatsAuthForm.init({
        inputSelector: '[data-ms-code="phone-number"]',
        apiKey: "k07Zj8EwdAIzHzLcLPQh-5jCuREbSKXG", //REPLACE WITH YOUR API KEY
        placeholder: phoneInput.getAttribute("data-ms-placeholder") || "",
        primaryColor: phoneInput.getAttribute("data-ms-primary-color") || "",
        secondaryColor: phoneInput.getAttribute("data-ms-secondary-color") || "",
        btnText: phoneInput.getAttribute("data-ms-btn-text") || ""
      });

      // Watch for success class from WhatsAuth
      const observer = new MutationObserver(() => {
        if (phoneInput.classList.contains("whatsauth-success") && !isVerified) {
          isVerified = true;

          // Enable submit
          submitBtn.disabled = false;
          submitBtn.style.opacity = 1;
          submitBtn.style.cursor = 'pointer';

          // Hide error
          errorMsg.style.display = 'none';

          // Re-enable Memberstack
          form.setAttribute("data-ms-form", originalMsFormValue);
        }
      });

      observer.observe(phoneInput, { attributes: true, attributeFilter: ["class"] });

      // Final form safeguard
      form.addEventListener("submit", function (e) {
        if (!isVerified) {
          e.preventDefault();
          errorMsg.style.display = 'block';
        }
      });

      submitBtn.addEventListener("click", function (e) {
        if (!isVerified) {
          e.preventDefault();
          errorMsg.style.display = 'block';
        }
      });
    }
  });
</script>
Ansicht Memberscript
UX

#151 - Onboarding Tour For New Members

Launch a step-by-step product tour the first time a member logs in. Uses Memberstack’s JS API + Intro.js


<!-- 💙 MEMBERSCRIPT #151 v0.1 💙 - ONBOARDING TOUR FOR NEW MEMBERS -->
<script>
  // 1. Wait for Memberstack v2 DOM
  function ready(fn) {
    if (window.$memberstackReady) return fn();
    document.addEventListener("memberstack.ready", fn);
  }

  // 2. Collect all steps from the DOM
  function collectSteps() {
    // all elements with ms-code-step, in a NodeList
    var els = document.querySelectorAll("[ms-code-step]");
    // build an array of { order, element, intro }
    var steps = Array.prototype.map.call(els, function(el) {
      return {
        order: parseInt(el.getAttribute("ms-code-step"), 10),
        element: el,
        intro: el.getAttribute("ms-code-intro") || ""
      };
    });
    // sort by order ascending
    return steps.sort(function(a, b) {
      return a.order - b.order;
    }).map(function(s) {
      return { element: s.element, intro: s.intro };
    });
  }

  // 3. Kick off the tour for first-time members
  function launchTour(member) {
    if (!member || !member.id) return;                // only for logged-in
    if (localStorage.getItem("ms-code-tour-shown")) return;
    // Build steps dynamically
    var options = {
      steps: collectSteps(),
      showProgress: true,
      exitOnOverlayClick: false
    };
    introJs().setOptions(options).start();
    localStorage.setItem("ms-code-tour-shown", "true");
  }

  // 4. Glue it together
  ready(function() {
    window.$memberstackDom
      .getCurrentMember()
      .then(function(res) {
        launchTour(res.data);
      })
      .catch(function(err) {
        console.error("MS-Code-Tour error:", err);
      });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #151 v0.1 💙 - ONBOARDING TOUR FOR NEW MEMBERS -->
<script>
  // 1. Wait for Memberstack v2 DOM
  function ready(fn) {
    if (window.$memberstackReady) return fn();
    document.addEventListener("memberstack.ready", fn);
  }

  // 2. Collect all steps from the DOM
  function collectSteps() {
    // all elements with ms-code-step, in a NodeList
    var els = document.querySelectorAll("[ms-code-step]");
    // build an array of { order, element, intro }
    var steps = Array.prototype.map.call(els, function(el) {
      return {
        order: parseInt(el.getAttribute("ms-code-step"), 10),
        element: el,
        intro: el.getAttribute("ms-code-intro") || ""
      };
    });
    // sort by order ascending
    return steps.sort(function(a, b) {
      return a.order - b.order;
    }).map(function(s) {
      return { element: s.element, intro: s.intro };
    });
  }

  // 3. Kick off the tour for first-time members
  function launchTour(member) {
    if (!member || !member.id) return;                // only for logged-in
    if (localStorage.getItem("ms-code-tour-shown")) return;
    // Build steps dynamically
    var options = {
      steps: collectSteps(),
      showProgress: true,
      exitOnOverlayClick: false
    };
    introJs().setOptions(options).start();
    localStorage.setItem("ms-code-tour-shown", "true");
  }

  // 4. Glue it together
  ready(function() {
    window.$memberstackDom
      .getCurrentMember()
      .then(function(res) {
        launchTour(res.data);
      })
      .catch(function(err) {
        console.error("MS-Code-Tour error:", err);
      });
  });
</script>
Ansicht Memberscript
JSON

#150 - Save and Unsave Items to Your Collection (Pinterest-style)

A simple save/unsave system that lets members bookmark items into personal collections.


<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 1 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
  const ms = window.$memberstackDom;
  const member = await ms.getCurrentMember();
  const isLoggedIn = !!member;
  let savedItems = {};

  const fetchSavedItems = async () => {
    try {
      const { data } = await ms.getMemberJSON();
      savedItems = data.savedItems || {};
    } catch {
      savedItems = {};
    }
  };

  const persistSavedItems = async () => {
    try {
      await ms.updateMemberJSON({ json: { savedItems } });
    } catch (err) {
      console.error("Error saving items:", err);
    }
  };

  const updateButtons = () => {
    document.querySelectorAll('[ms-code-add-button]').forEach(btn => {
      const id = btn.getAttribute('ms-code-save');
      const category = btn.getAttribute('ms-code-category');
      const exists = savedItems[category]?.some(i => i.id === id);
      btn.style.display = exists ? 'none' : 'inline-block';
    });

    document.querySelectorAll('[ms-code-unsave-button]').forEach(btn => {
      const id = btn.getAttribute('ms-code-unsave');
      const category = btn.getAttribute('ms-code-category');
      const exists = savedItems[category]?.some(i => i.id === id);
      btn.style.display = exists ? 'inline-block' : 'none';
    });
  };

  const onAddClick = async (e) => {
    e.preventDefault();
    if (!isLoggedIn) return;

    const btn = e.currentTarget;
    const container = btn.closest('[ms-code-save-item]');
    const id = btn.getAttribute('ms-code-save');
    const category = btn.getAttribute('ms-code-category');
    const img = container?.querySelector('[ms-code-image]');
    const url = img?.src;

    if (!savedItems[category]) savedItems[category] = [];
    if (!savedItems[category].some(i => i.id === id)) {
      savedItems[category].push({ id, url });
      updateButtons();
      await persistSavedItems();
    }
  };

  const onUnsaveClick = async (e) => {
    e.preventDefault();
    if (!isLoggedIn) return;

    const btn = e.currentTarget;
    const id = btn.getAttribute('ms-code-unsave');
    const category = btn.getAttribute('ms-code-category');

    if (savedItems[category]) {
      savedItems[category] = savedItems[category].filter(i => i.id !== id);
      if (savedItems[category].length === 0) delete savedItems[category];
      updateButtons();
      await persistSavedItems();
    }
  };

  const onDownloadClick = (e) => {
    e.preventDefault();
    const btn = e.currentTarget;
    const container = btn.closest('[ms-code-save-item]');
    const img = container?.querySelector('[ms-code-image]');
    const url = img?.src;

    if (url) {
      const a = document.createElement('a');
      a.href = url;
      a.download = '';
      document.body.appendChild(a);
      a.click();
      a.remove();
    }
  };

  const attachListeners = () => {
    document.querySelectorAll('[ms-code-add-button]').forEach(b => b.addEventListener('click', onAddClick));
    document.querySelectorAll('[ms-code-unsave-button]').forEach(b => b.addEventListener('click', onUnsaveClick));
    document.querySelectorAll('[ms-code-download-button]').forEach(b => b.addEventListener('click', onDownloadClick));
  };

  await fetchSavedItems();
  updateButtons();
  attachListeners();
});
</script>

<!-- GENERATE PINTEREST GRID STYLE -->
<script>
$(document).ready(function () {
  setTimeout(function() {
    function resizeGridItem(item) {
      grid = document.getElementsByClassName("grid")[0];
      rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));
      rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap'));
      rowSpan = Math.ceil((item.querySelector('.content').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));
      item.style.gridRowEnd = "span " + rowSpan;
    }

    function resizeAllGridItems() {
      allItems = document.getElementsByClassName("item");
      for (x = 0; x < allItems.length; x++) {
        resizeGridItem(allItems[x]);
      }
    }

    function resizeInstance(instance) {
      item = instance.elements[0];
      resizeGridItem(item);
    }

    window.onload = resizeAllGridItems();
    window.addEventListener("resize", resizeAllGridItems);

    allItems = document.getElementsByClassName("item");
    for (x = 0; x < allItems.length; x++) {
      imagesLoaded(allItems[x], resizeInstance);
    }

    setTimeout(function() { resizeInstance() }, 100);
  }, 800);
})
</script>

<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 2 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
  const ms = window.$memberstackDom;
  const wrapper = document.querySelector('[ms-code-collections-wrapper]');
  const template = document.querySelector('[ms-code-folder-template]') || document.querySelector('[ms-code-folder]');
  const emptyState = document.querySelector('[ms-code-empty]');
  if (!wrapper || !template) return;

  let member;
  try {
    member = await ms.getCurrentMember();
  } catch {
    wrapper.textContent = "Please log in to view your collections.";
    return;
  }

  let savedItems = {};
  try {
    const { data } = await ms.getMemberJSON();
    savedItems = data?.savedItems || {};
  } catch {
    wrapper.textContent = "Could not load your collections.";
    return;
  }

  if (Object.keys(savedItems).length === 0) {
    wrapper.innerHTML = '';
    if (emptyState) emptyState.style.display = 'block';
    return;
  }

  if (emptyState) emptyState.style.display = 'none';
  wrapper.innerHTML = '';

  const persistSavedItems = async () => {
    try {
      await ms.updateMemberJSON({ json: { savedItems } });
    } catch (err) {
      console.error("Failed to save", err);
    }
  };

  const updateButtons = (modal, id, category) => {
    const addBtn = modal.querySelector('[ms-code-add-button]');
    const unsaveBtn = modal.querySelector('[ms-code-unsave-button]');
    const exists = savedItems[category]?.some(item => item.id === id);
    addBtn.style.display = exists ? 'none' : 'inline-block';
    unsaveBtn.style.display = exists ? 'inline-block' : 'none';
  };

  Object.entries(savedItems).forEach(([category, items]) => {
    const folderClone = template.cloneNode(true);
    const titleEl = folderClone.querySelector('[ms-code-folder-title]');
    if (titleEl) titleEl.textContent = `${category} (${items.length})`;

    const imageContainer = folderClone.querySelector('[ms-code-folder-items]');
    const imageTemplate = folderClone.querySelector('[ms-code-folder-image]');
    if (imageTemplate) imageTemplate.style.display = 'none';

    const modal = folderClone.querySelector('[ms-code-modal]');
    const modalImg = folderClone.querySelector('[ms-code-modal-img]');
    const modalClose = folderClone.querySelector('[ms-code-modal-close]');
    const addButton = folderClone.querySelector('[ms-code-add-button]');
    const unsaveButton = folderClone.querySelector('[ms-code-unsave-button]');
    const downloadButton = folderClone.querySelector('[ms-code-download-button]');
    const hiddenImage = folderClone.querySelector('[ms-code-image]');

    items.forEach(item => {
      const imgClone = imageTemplate.cloneNode(true);
      imgClone.src = item.url;
      imgClone.alt = category;
      imgClone.style.display = 'block';
      imgClone.style.objectFit = 'cover';
      imgClone.style.width = '100%';
      imgClone.style.height = 'auto';
      imgClone.style.maxWidth = '100%';

      imgClone.addEventListener('click', () => {
        if (modal && modalImg) {
          modalImg.src = item.url;
          if (hiddenImage) hiddenImage.src = item.url;

          const id = item.id;
          addButton.onclick = async (e) => {
            e.preventDefault();
            savedItems[category] = savedItems[category] || [];
            if (!savedItems[category].some(i => i.id === id)) {
              savedItems[category].push({ id, url: item.url });
              await persistSavedItems();
              updateButtons(modal, id, category);
            }
          };

          unsaveButton.onclick = async (e) => {
            e.preventDefault();
            savedItems[category] = savedItems[category].filter(i => i.id !== id);
            if (savedItems[category].length === 0) delete savedItems[category];
            await persistSavedItems();
            modal.style.display = 'none';
            location.reload();
          };

          downloadButton.onclick = (e) => {
            e.preventDefault();
            const a = document.createElement('a');
            a.href = item.url;
            a.download = '';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
          };

          updateButtons(modal, id, category);
        }
        modal.style.display = 'flex';
      });

      imageContainer.appendChild(imgClone);
    });

    if (modal && modalClose) {
      modalClose.addEventListener('click', () => {
        modal.style.display = 'none';
        if (modalImg) modalImg.src = '';
      });
    }

    wrapper.appendChild(folderClone);
  });
});
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 1 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
  const ms = window.$memberstackDom;
  const member = await ms.getCurrentMember();
  const isLoggedIn = !!member;
  let savedItems = {};

  const fetchSavedItems = async () => {
    try {
      const { data } = await ms.getMemberJSON();
      savedItems = data.savedItems || {};
    } catch {
      savedItems = {};
    }
  };

  const persistSavedItems = async () => {
    try {
      await ms.updateMemberJSON({ json: { savedItems } });
    } catch (err) {
      console.error("Error saving items:", err);
    }
  };

  const updateButtons = () => {
    document.querySelectorAll('[ms-code-add-button]').forEach(btn => {
      const id = btn.getAttribute('ms-code-save');
      const category = btn.getAttribute('ms-code-category');
      const exists = savedItems[category]?.some(i => i.id === id);
      btn.style.display = exists ? 'none' : 'inline-block';
    });

    document.querySelectorAll('[ms-code-unsave-button]').forEach(btn => {
      const id = btn.getAttribute('ms-code-unsave');
      const category = btn.getAttribute('ms-code-category');
      const exists = savedItems[category]?.some(i => i.id === id);
      btn.style.display = exists ? 'inline-block' : 'none';
    });
  };

  const onAddClick = async (e) => {
    e.preventDefault();
    if (!isLoggedIn) return;

    const btn = e.currentTarget;
    const container = btn.closest('[ms-code-save-item]');
    const id = btn.getAttribute('ms-code-save');
    const category = btn.getAttribute('ms-code-category');
    const img = container?.querySelector('[ms-code-image]');
    const url = img?.src;

    if (!savedItems[category]) savedItems[category] = [];
    if (!savedItems[category].some(i => i.id === id)) {
      savedItems[category].push({ id, url });
      updateButtons();
      await persistSavedItems();
    }
  };

  const onUnsaveClick = async (e) => {
    e.preventDefault();
    if (!isLoggedIn) return;

    const btn = e.currentTarget;
    const id = btn.getAttribute('ms-code-unsave');
    const category = btn.getAttribute('ms-code-category');

    if (savedItems[category]) {
      savedItems[category] = savedItems[category].filter(i => i.id !== id);
      if (savedItems[category].length === 0) delete savedItems[category];
      updateButtons();
      await persistSavedItems();
    }
  };

  const onDownloadClick = (e) => {
    e.preventDefault();
    const btn = e.currentTarget;
    const container = btn.closest('[ms-code-save-item]');
    const img = container?.querySelector('[ms-code-image]');
    const url = img?.src;

    if (url) {
      const a = document.createElement('a');
      a.href = url;
      a.download = '';
      document.body.appendChild(a);
      a.click();
      a.remove();
    }
  };

  const attachListeners = () => {
    document.querySelectorAll('[ms-code-add-button]').forEach(b => b.addEventListener('click', onAddClick));
    document.querySelectorAll('[ms-code-unsave-button]').forEach(b => b.addEventListener('click', onUnsaveClick));
    document.querySelectorAll('[ms-code-download-button]').forEach(b => b.addEventListener('click', onDownloadClick));
  };

  await fetchSavedItems();
  updateButtons();
  attachListeners();
});
</script>

<!-- GENERATE PINTEREST GRID STYLE -->
<script>
$(document).ready(function () {
  setTimeout(function() {
    function resizeGridItem(item) {
      grid = document.getElementsByClassName("grid")[0];
      rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));
      rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap'));
      rowSpan = Math.ceil((item.querySelector('.content').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));
      item.style.gridRowEnd = "span " + rowSpan;
    }

    function resizeAllGridItems() {
      allItems = document.getElementsByClassName("item");
      for (x = 0; x < allItems.length; x++) {
        resizeGridItem(allItems[x]);
      }
    }

    function resizeInstance(instance) {
      item = instance.elements[0];
      resizeGridItem(item);
    }

    window.onload = resizeAllGridItems();
    window.addEventListener("resize", resizeAllGridItems);

    allItems = document.getElementsByClassName("item");
    for (x = 0; x < allItems.length; x++) {
      imagesLoaded(allItems[x], resizeInstance);
    }

    setTimeout(function() { resizeInstance() }, 100);
  }, 800);
})
</script>

<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 2 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
  const ms = window.$memberstackDom;
  const wrapper = document.querySelector('[ms-code-collections-wrapper]');
  const template = document.querySelector('[ms-code-folder-template]') || document.querySelector('[ms-code-folder]');
  const emptyState = document.querySelector('[ms-code-empty]');
  if (!wrapper || !template) return;

  let member;
  try {
    member = await ms.getCurrentMember();
  } catch {
    wrapper.textContent = "Please log in to view your collections.";
    return;
  }

  let savedItems = {};
  try {
    const { data } = await ms.getMemberJSON();
    savedItems = data?.savedItems || {};
  } catch {
    wrapper.textContent = "Could not load your collections.";
    return;
  }

  if (Object.keys(savedItems).length === 0) {
    wrapper.innerHTML = '';
    if (emptyState) emptyState.style.display = 'block';
    return;
  }

  if (emptyState) emptyState.style.display = 'none';
  wrapper.innerHTML = '';

  const persistSavedItems = async () => {
    try {
      await ms.updateMemberJSON({ json: { savedItems } });
    } catch (err) {
      console.error("Failed to save", err);
    }
  };

  const updateButtons = (modal, id, category) => {
    const addBtn = modal.querySelector('[ms-code-add-button]');
    const unsaveBtn = modal.querySelector('[ms-code-unsave-button]');
    const exists = savedItems[category]?.some(item => item.id === id);
    addBtn.style.display = exists ? 'none' : 'inline-block';
    unsaveBtn.style.display = exists ? 'inline-block' : 'none';
  };

  Object.entries(savedItems).forEach(([category, items]) => {
    const folderClone = template.cloneNode(true);
    const titleEl = folderClone.querySelector('[ms-code-folder-title]');
    if (titleEl) titleEl.textContent = `${category} (${items.length})`;

    const imageContainer = folderClone.querySelector('[ms-code-folder-items]');
    const imageTemplate = folderClone.querySelector('[ms-code-folder-image]');
    if (imageTemplate) imageTemplate.style.display = 'none';

    const modal = folderClone.querySelector('[ms-code-modal]');
    const modalImg = folderClone.querySelector('[ms-code-modal-img]');
    const modalClose = folderClone.querySelector('[ms-code-modal-close]');
    const addButton = folderClone.querySelector('[ms-code-add-button]');
    const unsaveButton = folderClone.querySelector('[ms-code-unsave-button]');
    const downloadButton = folderClone.querySelector('[ms-code-download-button]');
    const hiddenImage = folderClone.querySelector('[ms-code-image]');

    items.forEach(item => {
      const imgClone = imageTemplate.cloneNode(true);
      imgClone.src = item.url;
      imgClone.alt = category;
      imgClone.style.display = 'block';
      imgClone.style.objectFit = 'cover';
      imgClone.style.width = '100%';
      imgClone.style.height = 'auto';
      imgClone.style.maxWidth = '100%';

      imgClone.addEventListener('click', () => {
        if (modal && modalImg) {
          modalImg.src = item.url;
          if (hiddenImage) hiddenImage.src = item.url;

          const id = item.id;
          addButton.onclick = async (e) => {
            e.preventDefault();
            savedItems[category] = savedItems[category] || [];
            if (!savedItems[category].some(i => i.id === id)) {
              savedItems[category].push({ id, url: item.url });
              await persistSavedItems();
              updateButtons(modal, id, category);
            }
          };

          unsaveButton.onclick = async (e) => {
            e.preventDefault();
            savedItems[category] = savedItems[category].filter(i => i.id !== id);
            if (savedItems[category].length === 0) delete savedItems[category];
            await persistSavedItems();
            modal.style.display = 'none';
            location.reload();
          };

          downloadButton.onclick = (e) => {
            e.preventDefault();
            const a = document.createElement('a');
            a.href = item.url;
            a.download = '';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
          };

          updateButtons(modal, id, category);
        }
        modal.style.display = 'flex';
      });

      imageContainer.appendChild(imgClone);
    });

    if (modal && modalClose) {
      modalClose.addEventListener('click', () => {
        modal.style.display = 'none';
        if (modalImg) modalImg.src = '';
      });
    }

    wrapper.appendChild(folderClone);
  });
});
</script>
Ansicht Memberscript
UX
Erreichbarkeit

#149 - Favicon for Dark/Light Mode

Use this script to update your website's favicon based on the user's system color scheme preference.


<!-- 💙 MEMBERSCRIPT #149 v0.1 💙 - FAVICON FOR DARK/LIGHT MODE -->
<script>
  // Helper: Retrieve or create a favicon element
  function getFaviconElement() {
    let favicon = document.querySelector('link[rel="icon"]') ||
        document.querySelector('link[rel="shortcut icon"]');
    if (!favicon) {
      favicon = document.createElement('link');
      favicon.rel = 'icon';
      document.head.appendChild(favicon);
    }
    return favicon;
  }

  // Function to update the favicon based on dark mode
  function updateFavicon(e) {
    const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
    const favicon = getFaviconElement();
    // Update these paths to your favicon assets in Webflow’s Asset Manager or a CDN
    favicon.href = darkModeOn
      ? 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f85b2a9f281a373ca_Dark%20Mode%20Logo.png'
      : 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f1c2fa3cebee1b150_Light%20Mode%20Logo.png';
  }

  // Initialize the favicon update on page load
  updateFavicon();

  // Listen for changes in the dark mode media query
  const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  if (typeof darkModeMediaQuery.addEventListener === 'function') {
    darkModeMediaQuery.addEventListener('change', updateFavicon);
  } else if (typeof darkModeMediaQuery.addListener === 'function') {
    darkModeMediaQuery.addListener(updateFavicon);
  }
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #149 v0.1 💙 - FAVICON FOR DARK/LIGHT MODE -->
<script>
  // Helper: Retrieve or create a favicon element
  function getFaviconElement() {
    let favicon = document.querySelector('link[rel="icon"]') ||
        document.querySelector('link[rel="shortcut icon"]');
    if (!favicon) {
      favicon = document.createElement('link');
      favicon.rel = 'icon';
      document.head.appendChild(favicon);
    }
    return favicon;
  }

  // Function to update the favicon based on dark mode
  function updateFavicon(e) {
    const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
    const favicon = getFaviconElement();
    // Update these paths to your favicon assets in Webflow’s Asset Manager or a CDN
    favicon.href = darkModeOn
      ? 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f85b2a9f281a373ca_Dark%20Mode%20Logo.png'
      : 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f1c2fa3cebee1b150_Light%20Mode%20Logo.png';
  }

  // Initialize the favicon update on page load
  updateFavicon();

  // Listen for changes in the dark mode media query
  const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  if (typeof darkModeMediaQuery.addEventListener === 'function') {
    darkModeMediaQuery.addEventListener('change', updateFavicon);
  } else if (typeof darkModeMediaQuery.addListener === 'function') {
    darkModeMediaQuery.addListener(updateFavicon);
  }
</script>
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.