Composants
Modèles
Attributs
Intégrations
Testeur de site
Code personnalisé

Scripts des membres

Une solution basée sur les attributs pour ajouter des fonctionnalités à votre site Webflow.
Il suffit de copier un peu de code, d'ajouter quelques attributs et le tour est joué.

Nous vous remercions ! Votre demande a bien été reçue !
Oups ! Un problème s'est produit lors de l'envoi du formulaire.
Besoin d'aide avec MemberScripts ?

Tous les clients de Memberstack peuvent demander de l'aide dans le Slack 2.0. Veuillez noter qu'il ne s'agit pas de fonctionnalités officielles et que le support ne peut être garanti.

Modaux
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>
Voir le Memberscript
Modaux
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>
Voir le Memberscript
Sécurité
Visibilité conditionnelle

#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>
Voir le Memberscript
Flux personnalisés
Intégration

#155 - Bulk Update Customer Subscriptions via Stripe & Make

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

v0.1
Voir le Memberscript
JSON
Sécurité

#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>
Voir le Memberscript
UX
Accessibilité

#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>
Voir le Memberscript
Sécurité
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>
Voir le 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>
Voir le 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>
Voir le Memberscript
UX
Accessibilité

#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>
Voir le Memberscript
UX
Accessibilité

#148 - Disable Webflow Form Success Window

Use this script to override Webflow's default form submission behavior by hiding the success message.


<!-- 💙 MEMBERSCRIPT #148 v0.1 💙 - DISABLE WEBFLOW FORM SUCCESS WINDOW -->
<script>
  document.addEventListener('DOMContentLoaded', function () {
    const form = document.querySelector('[ms-code-form="form"]');
    const successEl = document.querySelector('[ms-code-success="true"]');

    if (!form || !successEl) return;

    const observer = new MutationObserver(() => {
      const isVisible = window.getComputedStyle(successEl).display !== 'none';
      if (isVisible) {
        successEl.style.display = 'none';
        form.style.display = 'block';
      }
    });

    observer.observe(successEl, { attributes: true, attributeFilter: ['style'] });

    // Cleanup observer when done (optional, for performance)
    form.addEventListener('w-form-success', () => {
      setTimeout(() => {
        observer.disconnect();
      }, 100);
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #148 v0.1 💙 - DISABLE WEBFLOW FORM SUCCESS WINDOW -->
<script>
  document.addEventListener('DOMContentLoaded', function () {
    const form = document.querySelector('[ms-code-form="form"]');
    const successEl = document.querySelector('[ms-code-success="true"]');

    if (!form || !successEl) return;

    const observer = new MutationObserver(() => {
      const isVisible = window.getComputedStyle(successEl).display !== 'none';
      if (isVisible) {
        successEl.style.display = 'none';
        form.style.display = 'block';
      }
    });

    observer.observe(successEl, { attributes: true, attributeFilter: ['style'] });

    // Cleanup observer when done (optional, for performance)
    form.addEventListener('w-form-success', () => {
      setTimeout(() => {
        observer.disconnect();
      }, 100);
    });
  });
</script>
Voir le Memberscript
UX
Accessibilité
Modaux
Sécurité

#147 - Age Verification Popup with Cookies in Webflow

Use this script to add an age verification popup to your Webflow site.


<!-- 💙 MEMBERSCRIPT #147 v0.1 💙 - AGE VERIFICATION POPUP WITH COOKIES -->
<script>
  // Simple cookie helper functions
  function setCookie(name, value, days) {
    const date = new Date();
    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
    document.cookie = name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
  }

  function getCookie(name) {
    const cname = name + "=";
    const decodedCookie = decodeURIComponent(document.cookie);
    const cookies = decodedCookie.split(';');
    for (let i = 0; i < cookies.length; i++) {
      let c = cookies[i].trim();
      if (c.indexOf(cname) === 0) {
        return c.substring(cname.length, c.length);
      }
    }
    return "";
  }

  document.addEventListener('DOMContentLoaded', function() {
    // Select the age gate element via ms-code attribute
    const ageGateEl = document.querySelector('[ms-code-agegate]');

    // On page load: if the cookie exists, hide the age gate
    if (getCookie('ms-code-ageVerified') === 'true') {  
      if (ageGateEl) ageGateEl.style.display = 'none';
    }

    // Listen for clicks on elements with ms-code-click
    document.addEventListener('click', function(event) {
      if (event.target.closest('[ms-code-click="confirmAge"]')) {
        setCookie('ms-code-ageVerified', 'true', 30);
        if (ageGateEl) ageGateEl.style.display = 'none';
      } else if (event.target.closest('[ms-code-click="denyAge"]')) {
        window.location.href = 'https://age-verification-popup-with-cookies.webflow.io/access-denied'; // Change URL as needed
      }
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #147 v0.1 💙 - AGE VERIFICATION POPUP WITH COOKIES -->
<script>
  // Simple cookie helper functions
  function setCookie(name, value, days) {
    const date = new Date();
    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
    document.cookie = name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
  }

  function getCookie(name) {
    const cname = name + "=";
    const decodedCookie = decodeURIComponent(document.cookie);
    const cookies = decodedCookie.split(';');
    for (let i = 0; i < cookies.length; i++) {
      let c = cookies[i].trim();
      if (c.indexOf(cname) === 0) {
        return c.substring(cname.length, c.length);
      }
    }
    return "";
  }

  document.addEventListener('DOMContentLoaded', function() {
    // Select the age gate element via ms-code attribute
    const ageGateEl = document.querySelector('[ms-code-agegate]');

    // On page load: if the cookie exists, hide the age gate
    if (getCookie('ms-code-ageVerified') === 'true') {  
      if (ageGateEl) ageGateEl.style.display = 'none';
    }

    // Listen for clicks on elements with ms-code-click
    document.addEventListener('click', function(event) {
      if (event.target.closest('[ms-code-click="confirmAge"]')) {
        setCookie('ms-code-ageVerified', 'true', 30);
        if (ageGateEl) ageGateEl.style.display = 'none';
      } else if (event.target.closest('[ms-code-click="denyAge"]')) {
        window.location.href = 'https://age-verification-popup-with-cookies.webflow.io/access-denied'; // Change URL as needed
      }
    });
  });
</script>
Voir le Memberscript
UX
Accessibilité
Modaux

#146 - Stop Videos from Playing When A Modal Closes

Automatically stop video playback when closing modals in Webflow.


<!-- 💙 MEMBERSCRIPT #146 v0.1 💙 - STOP VIDEOS FROM PLAYING WHEN A MODAL CLOSES -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Select all modals on the page
    var modals = document.querySelectorAll('[data-ms-modal="modal"]');

    function initializeModal(modal) {
      var iframe = modal.querySelector('iframe');
      if (!iframe) return; // If no iframe found, do nothing
      var originalSrc = iframe.dataset.src || iframe.src;

      // Function to stop video playback
      function stopVideo() {
        iframe.src = "";
        setTimeout(() => {
          iframe.src = originalSrc;
        }, 100);
      }

      // Attach event listeners to all close buttons inside the modal
      var closeBtns = modal.querySelectorAll('[data-ms-modal="close"]');
      closeBtns.forEach(function(closeBtn) {
        closeBtn.addEventListener('click', function(e) {
          e.preventDefault();
          stopVideo();
        });
      });

      // Also close the modal when clicking outside the content
      modal.addEventListener('click', function(e) {
        if (e.target === modal) {
          stopVideo();
        }
      });
    }

    // Initialize all modals
    modals.forEach(initializeModal);
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #146 v0.1 💙 - STOP VIDEOS FROM PLAYING WHEN A MODAL CLOSES -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Select all modals on the page
    var modals = document.querySelectorAll('[data-ms-modal="modal"]');

    function initializeModal(modal) {
      var iframe = modal.querySelector('iframe');
      if (!iframe) return; // If no iframe found, do nothing
      var originalSrc = iframe.dataset.src || iframe.src;

      // Function to stop video playback
      function stopVideo() {
        iframe.src = "";
        setTimeout(() => {
          iframe.src = originalSrc;
        }, 100);
      }

      // Attach event listeners to all close buttons inside the modal
      var closeBtns = modal.querySelectorAll('[data-ms-modal="close"]');
      closeBtns.forEach(function(closeBtn) {
        closeBtn.addEventListener('click', function(e) {
          e.preventDefault();
          stopVideo();
        });
      });

      // Also close the modal when clicking outside the content
      modal.addEventListener('click', function(e) {
        if (e.target === modal) {
          stopVideo();
        }
      });
    }

    // Initialize all modals
    modals.forEach(initializeModal);
  });
</script>
Voir le Memberscript
UX
Accessibilité

#145 - Automatically Save & Prefill Forms

Automatically save and prefill forms in a browsers localStorage upon form submission.


<!-- 💙 MEMBERSCRIPT #145 v0.1 💙 - HOW TO PRE-FILL FORM INPUT FIELDS AT PAGE LOAD -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Function to store form data in localStorage
    function storeFormData() {
      const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
      fields.forEach(function(field) {
        const fieldId = field.getAttribute('ms-code-field-id');
        const value = field.value.trim();
        if (value) {
          localStorage.setItem(fieldId, value);
        }
      });
    }

    // Function to pre-fill form fields with stored data
    function preFillForm() {
      const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
      fields.forEach(function(field) {
        const fieldId = field.getAttribute('ms-code-field-id');
        const storedValue = localStorage.getItem(fieldId);
        if (storedValue) {
          field.value = storedValue;
        }
      });
    }

    // Handle form submission
    const form = document.querySelector('#my-form, form[ms-code-form-id]');
    if (form) {
      form.addEventListener('submit', function(event) {
        event.preventDefault(); // Prevent default form submission
        storeFormData();

        // Refresh the page after storing data
        setTimeout(function() {
          location.reload();
        }, 500); // Short delay to simulate form submission
      });
    }

    // Pre-fill form fields when the page loads
    preFillForm();
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #145 v0.1 💙 - HOW TO PRE-FILL FORM INPUT FIELDS AT PAGE LOAD -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Function to store form data in localStorage
    function storeFormData() {
      const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
      fields.forEach(function(field) {
        const fieldId = field.getAttribute('ms-code-field-id');
        const value = field.value.trim();
        if (value) {
          localStorage.setItem(fieldId, value);
        }
      });
    }

    // Function to pre-fill form fields with stored data
    function preFillForm() {
      const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
      fields.forEach(function(field) {
        const fieldId = field.getAttribute('ms-code-field-id');
        const storedValue = localStorage.getItem(fieldId);
        if (storedValue) {
          field.value = storedValue;
        }
      });
    }

    // Handle form submission
    const form = document.querySelector('#my-form, form[ms-code-form-id]');
    if (form) {
      form.addEventListener('submit', function(event) {
        event.preventDefault(); // Prevent default form submission
        storeFormData();

        // Refresh the page after storing data
        setTimeout(function() {
          location.reload();
        }, 500); // Short delay to simulate form submission
      });
    }

    // Pre-fill form fields when the page loads
    preFillForm();
  });
</script>
Voir le Memberscript
Marketing
JSON
RÉFÉRENCEMENT

#144 - Track Users Login History & Active Users

Automatically track a members login history, keep a login streak and total visits


<!-- 💙 MEMBERSCRIPT #144 v0.1 💙 - TRACK A USERS LOGIN HISTORY & ACTIVE USERS -->
<script>
  (function(){
    const memberstack = window.$memberstackDom;

    // Helper: Execute callback when Memberstack is ready
    function onMemberstackReady(cb) {
      if (window.$memberstackReady) {
        cb();
      } else {
        document.addEventListener("memberstack.ready", cb);
      }
    }

    async function initTracking() {
      // Check if a member is logged in (via localStorage)
      const currentUser = JSON.parse(localStorage.getItem("_ms-mem") || "null");
      if (!currentUser) {
        console.warn("No logged-in member found. Tracking not applied.");
        return;
      }

      // Retrieve member metadata
      const memberJson = await memberstack.getMemberJSON();
      let metadata = memberJson.data || {};

      // Ensure userVisits exists as an array
      metadata.userVisits = Array.isArray(metadata.userVisits) ? metadata.userVisits : [];

      // Use ISO date (YYYY-MM-DD) to record one visit per day
      const today = new Date().toISOString().split("T")[0];
      if (!metadata.userVisits.includes(today)) {
        metadata.userVisits.push(today);
      }

      // Helper: Compute consecutive login streak from userVisits
      function computeStreak(visits) {
        if (!visits.length) return 0;
        // Ensure dates are unique and sorted ascending
        const uniqueVisits = [...new Set(visits)].sort();
        let streak = 1;
        let currentDate = new Date(uniqueVisits[uniqueVisits.length - 1]);
        for (let i = uniqueVisits.length - 2; i >= 0; i--) {
          const prevDate = new Date(uniqueVisits[i]);
          const diffDays = Math.floor((currentDate - prevDate) / (1000 * 60 * 60 * 24));
          if (diffDays === 1) {
            streak++;
            currentDate = prevDate;
          } else {
            break;
          }
        }
        return streak;
      }

      // Calculate the login streak and total visits
      metadata.loginStreak = computeStreak(metadata.userVisits);
      metadata.totalVisits = metadata.userVisits.length;

      // Update Memberstack metadata
      await memberstack.updateMemberJSON({ json: metadata });

      console.log("User visits:", metadata.userVisits);
      console.log("Login streak:", metadata.loginStreak);
      console.log("Total visits:", metadata.totalVisits);
    }

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

<!-- 💙 MEMBERSCRIPT #144 v0.1 💙 - TRACK A USERS LOGIN HISTORY & ACTIVE USERS -->
<script>
  (function(){
    const memberstack = window.$memberstackDom;

    // Helper: Execute callback when Memberstack is ready
    function onMemberstackReady(cb) {
      if (window.$memberstackReady) {
        cb();
      } else {
        document.addEventListener("memberstack.ready", cb);
      }
    }

    async function initTracking() {
      // Check if a member is logged in (via localStorage)
      const currentUser = JSON.parse(localStorage.getItem("_ms-mem") || "null");
      if (!currentUser) {
        console.warn("No logged-in member found. Tracking not applied.");
        return;
      }

      // Retrieve member metadata
      const memberJson = await memberstack.getMemberJSON();
      let metadata = memberJson.data || {};

      // Ensure userVisits exists as an array
      metadata.userVisits = Array.isArray(metadata.userVisits) ? metadata.userVisits : [];

      // Use ISO date (YYYY-MM-DD) to record one visit per day
      const today = new Date().toISOString().split("T")[0];
      if (!metadata.userVisits.includes(today)) {
        metadata.userVisits.push(today);
      }

      // Helper: Compute consecutive login streak from userVisits
      function computeStreak(visits) {
        if (!visits.length) return 0;
        // Ensure dates are unique and sorted ascending
        const uniqueVisits = [...new Set(visits)].sort();
        let streak = 1;
        let currentDate = new Date(uniqueVisits[uniqueVisits.length - 1]);
        for (let i = uniqueVisits.length - 2; i >= 0; i--) {
          const prevDate = new Date(uniqueVisits[i]);
          const diffDays = Math.floor((currentDate - prevDate) / (1000 * 60 * 60 * 24));
          if (diffDays === 1) {
            streak++;
            currentDate = prevDate;
          } else {
            break;
          }
        }
        return streak;
      }

      // Calculate the login streak and total visits
      metadata.loginStreak = computeStreak(metadata.userVisits);
      metadata.totalVisits = metadata.userVisits.length;

      // Update Memberstack metadata
      await memberstack.updateMemberJSON({ json: metadata });

      console.log("User visits:", metadata.userVisits);
      console.log("Login streak:", metadata.loginStreak);
      console.log("Total visits:", metadata.totalVisits);
    }

    onMemberstackReady(initTracking);
  })();
</script>
Voir le Memberscript
UX
Marketing

#143 - Initial Based Profile Avatar

Generate a custom avatar with initials when a member has no profile picture.


<!-- 💙 MEMBERSCRIPT #143 v0.1 💙 - GENERATE INITIALS BASED AVATAR -->
<script>
  document.addEventListener("DOMContentLoaded", function () {
    const checkMemberstack = setInterval(() => {
      if (window.$memberstackDom) {
        clearInterval(checkMemberstack);

        window.$memberstackDom.getCurrentMember().then(({ data }) => {
          if (!data) return console.log("No member data (logged out)");

          const profileImage = document.querySelector('[data-ms-member="profile-image"]');
          const avatarWrapper = document.querySelector('[data-ms-code="avatar"]');
          const initialsDiv = avatarWrapper?.querySelector('.ms-avatar-initial');

          if (data.profileImage) {
            profileImage?.style.setProperty("display", "block");
            avatarWrapper?.style.setProperty("display", "none");
          } else {
            profileImage?.style.setProperty("display", "none");
            avatarWrapper?.style.setProperty("display", "flex");

            // Get initials from available fields
            const first = data.customFields["first-name"]?.trim().charAt(0).toUpperCase() || "";
            const last = data.customFields["last-name"]?.trim().charAt(0).toUpperCase() || "";
            let initials = first + last;

            if (!initials) {
              const fullName = data.customFields["name"]?.trim().split(" ") || [];
              initials = fullName.length > 1
                ? (fullName[0].charAt(0) + fullName[1].charAt(0)).toUpperCase()
                : fullName[0]?.charAt(0).toUpperCase() || "?";
            }

            if (initialsDiv) {
              initialsDiv.textContent = initials;
            } else {
              avatarWrapper.innerHTML = `
${initials}
`; } } }).catch(console.error); } }, 100); }); </script>
v0.1

<!-- 💙 MEMBERSCRIPT #143 v0.1 💙 - GENERATE INITIALS BASED AVATAR -->
<script>
  document.addEventListener("DOMContentLoaded", function () {
    const checkMemberstack = setInterval(() => {
      if (window.$memberstackDom) {
        clearInterval(checkMemberstack);

        window.$memberstackDom.getCurrentMember().then(({ data }) => {
          if (!data) return console.log("No member data (logged out)");

          const profileImage = document.querySelector('[data-ms-member="profile-image"]');
          const avatarWrapper = document.querySelector('[data-ms-code="avatar"]');
          const initialsDiv = avatarWrapper?.querySelector('.ms-avatar-initial');

          if (data.profileImage) {
            profileImage?.style.setProperty("display", "block");
            avatarWrapper?.style.setProperty("display", "none");
          } else {
            profileImage?.style.setProperty("display", "none");
            avatarWrapper?.style.setProperty("display", "flex");

            // Get initials from available fields
            const first = data.customFields["first-name"]?.trim().charAt(0).toUpperCase() || "";
            const last = data.customFields["last-name"]?.trim().charAt(0).toUpperCase() || "";
            let initials = first + last;

            if (!initials) {
              const fullName = data.customFields["name"]?.trim().split(" ") || [];
              initials = fullName.length > 1
                ? (fullName[0].charAt(0) + fullName[1].charAt(0)).toUpperCase()
                : fullName[0]?.charAt(0).toUpperCase() || "?";
            }

            if (initialsDiv) {
              initialsDiv.textContent = initials;
            } else {
              avatarWrapper.innerHTML = `
${initials}
`; } } }).catch(console.error); } }, 100); }); </script>
Voir le Memberscript
UX
Marketing

#142 - Embed PDFs For Webflow

Easily embed a PDF on your Webflow site - for free, without any custom code.


<!-- 💙 MEMBERSCRIPT #142 v0.1 💙 - EMBED PDFS IN WEBFLOW -->
<script>
  document.addEventListener("DOMContentLoaded", function () {
    const pdfElements = document.querySelectorAll('[ms-code-pdf-src]');

    pdfElements.forEach(function (element) {
      const src = element.getAttribute('ms-code-pdf-src');
      const height = element.getAttribute('ms-code-pdf-height') || '500px';

      const iframe = document.createElement('iframe');
      iframe.src = src;
      iframe.style.width = '100%';
      iframe.style.height = height;
      iframe.style.border = 'none';
      // Set the iframe to block to remove any inline element gaps
      iframe.style.display = 'block';
      iframe.setAttribute('scrolling', 'auto');

      element.innerHTML = '';
      element.appendChild(iframe);
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #142 v0.1 💙 - EMBED PDFS IN WEBFLOW -->
<script>
  document.addEventListener("DOMContentLoaded", function () {
    const pdfElements = document.querySelectorAll('[ms-code-pdf-src]');

    pdfElements.forEach(function (element) {
      const src = element.getAttribute('ms-code-pdf-src');
      const height = element.getAttribute('ms-code-pdf-height') || '500px';

      const iframe = document.createElement('iframe');
      iframe.src = src;
      iframe.style.width = '100%';
      iframe.style.height = height;
      iframe.style.border = 'none';
      // Set the iframe to block to remove any inline element gaps
      iframe.style.display = 'block';
      iframe.setAttribute('scrolling', 'auto');

      element.innerHTML = '';
      element.appendChild(iframe);
    });
  });
</script>
Voir le Memberscript
UX
Marketing

#141 - Démarrer l'intégration de YouTube à un moment précis

Activez les liens partageables et lancez la lecture des vidéos à une heure donnée.


<!-- 💙 MEMBERSCRIPT #141 v0.1 💙 - START YOUTUBE VIDEO AT SPECIFIC TIME -->
<script>
  (function() {
    // Function to get URL parameters
    function getUrlParameter(name) {
      name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
      var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
      var results = regex.exec(location.search);
      return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
    }

    // Function to update YouTube embed src within Embedly iframe
    function updateYouTubeEmbed(embedly_iframe, startTime) {
      var embedly_src = embedly_iframe.src;
      var youtube_src_match = embedly_src.match(/src=([^&]+)/);
      if (youtube_src_match) {
        var youtube_src = decodeURIComponent(youtube_src_match[1]);
        var new_youtube_src = youtube_src.replace(/(\?|&)start=\d+/, '');
        new_youtube_src += (new_youtube_src.includes('?') ? '&' : '?') + 'start=' + startTime;
        var new_embedly_src = embedly_src.replace(/src=([^&]+)/, 'src=' + encodeURIComponent(new_youtube_src));
        embedly_iframe.src = new_embedly_src;
      }
    }

    // Get all elements with ms-code-yt-start attribute
    var elements = document.querySelectorAll('[ms-code-yt-start]');

    elements.forEach(function(element) {
      var paramName = element.getAttribute('ms-code-yt-start');
      var startTime = getUrlParameter(paramName);
      var defaultStartTime = element.getAttribute('ms-code-yt-start-default');

      // If no URL parameter, use the default start time (if specified)
      if (!startTime && defaultStartTime) {
        startTime = defaultStartTime;
      }

      // If we have a start time (either from URL or default), update the embed
      if (startTime) {
        var iframe = element.querySelector('iframe.embedly-embed');
        if (iframe) {
          updateYouTubeEmbed(iframe, startTime);
        }
      }
    });
  })();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #141 v0.1 💙 - START YOUTUBE VIDEO AT SPECIFIC TIME -->
<script>
  (function() {
    // Function to get URL parameters
    function getUrlParameter(name) {
      name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
      var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
      var results = regex.exec(location.search);
      return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
    }

    // Function to update YouTube embed src within Embedly iframe
    function updateYouTubeEmbed(embedly_iframe, startTime) {
      var embedly_src = embedly_iframe.src;
      var youtube_src_match = embedly_src.match(/src=([^&]+)/);
      if (youtube_src_match) {
        var youtube_src = decodeURIComponent(youtube_src_match[1]);
        var new_youtube_src = youtube_src.replace(/(\?|&)start=\d+/, '');
        new_youtube_src += (new_youtube_src.includes('?') ? '&' : '?') + 'start=' + startTime;
        var new_embedly_src = embedly_src.replace(/src=([^&]+)/, 'src=' + encodeURIComponent(new_youtube_src));
        embedly_iframe.src = new_embedly_src;
      }
    }

    // Get all elements with ms-code-yt-start attribute
    var elements = document.querySelectorAll('[ms-code-yt-start]');

    elements.forEach(function(element) {
      var paramName = element.getAttribute('ms-code-yt-start');
      var startTime = getUrlParameter(paramName);
      var defaultStartTime = element.getAttribute('ms-code-yt-start-default');

      // If no URL parameter, use the default start time (if specified)
      if (!startTime && defaultStartTime) {
        startTime = defaultStartTime;
      }

      // If we have a start time (either from URL or default), update the embed
      if (startTime) {
        var iframe = element.querySelector('iframe.embedly-embed');
        if (iframe) {
          updateYouTubeEmbed(iframe, startTime);
        }
      }
    });
  })();
</script>
Voir le Memberscript
Flux personnalisés
UX

#140 - Confirmer la concordance des entrées

Vérifier une entrée avant d'autoriser la soumission - idéal pour éviter les informations erronées !


<!-- 💙 MEMBERSCRIPT #140 v0.1 💙 - CONFIRM MATCHING INPUTS -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const forms = document.querySelectorAll('form');

    forms.forEach(form => {
      const inputPairs = form.querySelectorAll('[ms-code-conf-input]');
      const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');

      if (!submitButton) {
        console.error('Submit button not found in the form');
        return;
      }

      function validateForm() {
        let fieldsMatch = true;

        inputPairs.forEach(input => {
          const confType = input.getAttribute('ms-code-conf-input');
          const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
          const errorElement = form.querySelector(`[ms-code-conf-error="${confType}"]`);

          if (confirmInput && errorElement) {
            if (input.value && confirmInput.value) {
              if (input.value !== confirmInput.value) {
                errorElement.style.removeProperty('display');
                fieldsMatch = false;
              } else {
                errorElement.style.display = 'none';
              }
            } else {
              errorElement.style.display = 'none';
            }
          }
        });

        if (fieldsMatch) {
          submitButton.style.removeProperty('pointer-events');
          submitButton.disabled = false;
        } else {
          submitButton.style.pointerEvents = 'none';
          submitButton.disabled = true;
        }
      }

      inputPairs.forEach(input => {
        const confType = input.getAttribute('ms-code-conf-input');
        const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);

        if (confirmInput) {
          input.addEventListener('input', validateForm);
          confirmInput.addEventListener('input', validateForm);
        }
      });

      // Initial validation
      validateForm();

      // Extra precaution: prevent form submission if fields don't match
      form.addEventListener('submit', function(event) {
        if (submitButton.disabled) {
          event.preventDefault();
          console.log('Form submission blocked: Fields do not match');
        }
      });
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #140 v0.1 💙 - CONFIRM MATCHING INPUTS -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const forms = document.querySelectorAll('form');

    forms.forEach(form => {
      const inputPairs = form.querySelectorAll('[ms-code-conf-input]');
      const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');

      if (!submitButton) {
        console.error('Submit button not found in the form');
        return;
      }

      function validateForm() {
        let fieldsMatch = true;

        inputPairs.forEach(input => {
          const confType = input.getAttribute('ms-code-conf-input');
          const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
          const errorElement = form.querySelector(`[ms-code-conf-error="${confType}"]`);

          if (confirmInput && errorElement) {
            if (input.value && confirmInput.value) {
              if (input.value !== confirmInput.value) {
                errorElement.style.removeProperty('display');
                fieldsMatch = false;
              } else {
                errorElement.style.display = 'none';
              }
            } else {
              errorElement.style.display = 'none';
            }
          }
        });

        if (fieldsMatch) {
          submitButton.style.removeProperty('pointer-events');
          submitButton.disabled = false;
        } else {
          submitButton.style.pointerEvents = 'none';
          submitButton.disabled = true;
        }
      }

      inputPairs.forEach(input => {
        const confType = input.getAttribute('ms-code-conf-input');
        const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);

        if (confirmInput) {
          input.addEventListener('input', validateForm);
          confirmInput.addEventListener('input', validateForm);
        }
      });

      // Initial validation
      validateForm();

      // Extra precaution: prevent form submission if fields don't match
      form.addEventListener('submit', function(event) {
        if (submitButton.disabled) {
          event.preventDefault();
          console.log('Form submission blocked: Fields do not match');
        }
      });
    });
  });
</script>
Voir le Memberscript
UX

#139 - Réinitialiser le formulaire après l'avoir soumis

Créer un bouton dans l'état de réussite du formulaire qui permet de le soumettre à nouveau.


<!-- 💙 MEMBERSCRIPT #139 v0.1 💙 - RESET FORM BUTTON -->
<script>
  // Wait for the DOM to be fully loaded
  document.addEventListener('DOMContentLoaded', function() {
    // Find all "Add another" buttons
    const resetButtons = document.querySelectorAll('[ms-code-reset-form]');

    // Add click event listener to each button
    resetButtons.forEach(function(resetButton) {
      resetButton.addEventListener('click', function(e) {
        e.preventDefault(); // Prevent default link behavior

        // Find the closest form and success message elements
        const formWrapper = this.closest('.w-form');
        const form = formWrapper.querySelector('form');
        const successMessage = formWrapper.querySelector('.w-form-done');

        // Reset the form
        form.reset();

        // Hide the success message
        successMessage.style.display = 'none';

        // Show the form
        form.style.display = 'block';
      });
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #139 v0.1 💙 - RESET FORM BUTTON -->
<script>
  // Wait for the DOM to be fully loaded
  document.addEventListener('DOMContentLoaded', function() {
    // Find all "Add another" buttons
    const resetButtons = document.querySelectorAll('[ms-code-reset-form]');

    // Add click event listener to each button
    resetButtons.forEach(function(resetButton) {
      resetButton.addEventListener('click', function(e) {
        e.preventDefault(); // Prevent default link behavior

        // Find the closest form and success message elements
        const formWrapper = this.closest('.w-form');
        const form = formWrapper.querySelector('form');
        const successMessage = formWrapper.querySelector('.w-form-done');

        // Reset the form
        form.reset();

        // Hide the success message
        successMessage.style.display = 'none';

        // Show the form
        form.style.display = 'block';
      });
    });
  });
</script>
Voir le Memberscript
Nous n'avons pas trouvé de scripts pour cette recherche... veuillez réessayer.
Slack

Besoin d'aide avec MemberScripts ? Rejoignez notre communauté Slack de plus de 5 500 membres ! 🙌

Les MemberScripts sont une ressource communautaire de Memberstack - si vous avez besoin d'aide pour les faire fonctionner avec votre projet, rejoignez le Slack de Memberstack 2.0 et demandez de l'aide !

Rejoignez notre Slack
Vitrine

Découvrez les entreprises qui ont réussi avec Memberstack

Ne vous contentez pas de nous croire sur parole, consultez les entreprises de toutes tailles qui font confiance à Memberstack pour leur authentification et leurs paiements.

Voir tous les exemples de réussite
Même Webflow utilise Memberstack !
Commencer à construire

Commencez à construire vos rêves

Memberstack est 100% gratuit jusqu'à ce que vous soyez prêt à vous lancer - alors, qu'attendez-vous ? Créez votre première application et commencez à construire dès aujourd'hui.