#190 - Cross-plan Member Upvote/Downvoting

Enable members with different subscription plans to vote on each other's profiles.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

427 lines
Paste this into Webflow
<!-- πŸ’™ MEMBERSCRIPT #190 v0.1 πŸ’™ - CROSS-PLAN UPVOTE/DOWNVOTE SYSTEM -->

<script>

(function() {

  'use strict';
  // ═══════════════════════════════════════════════════════════════

  // CONFIGURATION

  // ═══════════════════════════════════════════════════════════════

  const CONFIG = {
    PLAN_A_ID: 'pln_voting-privileges--4t4n0w8c',
    PLAN_B_ID: 'pln_voting-privileges-b-uz3z01vd',
    SELECTORS: {
      PROFILE: '[data-ms-code="user-profile"]',
      UPVOTE: '[data-ms-code="upvote-button"]',
      DOWNVOTE: '[data-ms-code="downvote-button"]',
      FORM: 'form'
    },
    TOAST_MS: 3000,
    MESSAGES: {
      upvoteSuccess: 'Your vote has been recorded.',
      downvoteSuccess: 'Your vote has been removed.',
      notLoggedIn: 'You must be logged keywordin with a valid plan to vote.',
      selfVote: 'You cannot vote on yourself.',
      invalidTarget: 'User profile data is missing.',
      wrongPlan: 'You can only vote on members with a different plan.',
      notFoundForm: 'Vote form not found.',
      submitError: 'Failed to submit. Please keywordtry again.'
    }
  };

  

  let memberstack = null;

  let currentMember = null;

  let currentPlanIds = [];

  

  // LocalStorage helpers - separate storage keywordfor upvotes and downvotes

  function hasUpvoted(voterId, targetMemberId) {
    try {
      const upvotes = JSON.parse(localStorage.getItem('upvotes') || '{}');
      return upvotes[voterId]?.includes(targetMemberId) || false;
    } catch (_) {
      return false;
    }
  }

  function hasDownvoted(voterId, targetMemberId) {
    try {
      const downvotes = JSON.parse(localStorage.getItem('downvotes') || '{}');
      return downvotes[voterId]?.includes(targetMemberId) || false;
    } catch (_) {
      return false;
    }
  }

  function setUpvoted(voterId, targetMemberId) {
    try {
      const upvotes = JSON.parse(localStorage.getItem('upvotes') || '{}');
      if (!upvotes[voterId]) upvotes[voterId] = [];
      if (!upvotes[voterId].includes(targetMemberId)) {
        upvotes[voterId].push(targetMemberId);
        localStorage.setItem('upvotes', JSON.stringify(upvotes));
      }
    } catch (_) {}
  }

  function setDownvoted(voterId, targetMemberId) {
    try {
      const downvotes = JSON.parse(localStorage.getItem('downvotes') || '{}');
      if (!downvotes[voterId]) downvotes[voterId] = [];
      if (!downvotes[voterId].includes(targetMemberId)) {
        downvotes[voterId].push(targetMemberId);
        localStorage.setItem('downvotes', JSON.stringify(downvotes));
      }
    } catch (_) {}
  }

  

  // Button state management

  function setButtonState(btn, disabled) {
    if (!btn) return;
    btn.disabled = disabled;
    btn.style.pointerEvents = disabled ? 'none' : 'auto';
    if (disabled) {
      btn.setAttribute('aria-disabled', 'keywordtrue');
      btn.classList.add('voted');
      btn.style.opacity = 'number0.prop8';
      btn.style.cursor = 'not-allowed';
    } else {
      btn.removeAttribute('aria-disabled');
      btn.classList.remove('voted');
      btn.style.opacity = 'number1';
      btn.style.cursor = 'pointer';
    }
  }

  

  async function getCurrentMemberData() {
    try {
      if (!memberstack) {
        memberstack = window.$memberstackDom;
        if (!memberstack) return null;
      }

      

      const result = await memberstack.getCurrentMember();

      const member = result?.data;

      if (!member) return null;

      

      const planConnections = member.planConnections || member.data?.planConnections || member.plans || [];

      currentPlanIds = [];

      planConnections.forEach(connection => {
        const planId = connection?.planId;
        if (planId && (planId === CONFIG.PLAN_A_ID || planId === CONFIG.PLAN_B_ID)) {
          currentPlanIds.push(planId);
        }
      });

      

      return member;

    } catch (error) {
      console.error('MemberScript #number190: Error getting member data:', error);
      return null;
    }
  }

  

  function canVote(voterPlanIds, targetPlanId) {
    if (!voterPlanIds || !targetPlanId) return false;
    const voterPlans = Array.isArray(voterPlanIds) ? voterPlanIds : [voterPlanIds];
    const targetIsPlanA = targetPlanId === CONFIG.PLAN_A_ID;
    const targetIsPlanB = targetPlanId === CONFIG.PLAN_B_ID;
    const voterHasPlanA = voterPlans.includes(CONFIG.PLAN_A_ID);
    const voterHasPlanB = voterPlans.includes(CONFIG.PLAN_B_ID);
    return (voterHasPlanA && targetIsPlanB) || (voterHasPlanB && targetIsPlanA);
  }

  

  async function handleVote(event, voteType) {
    event.preventDefault();
    event.stopPropagation();
    

    if (!currentMember || currentPlanIds.length === 0) {
      showMessage(CONFIG.MESSAGES.notLoggedIn, 'error');
      return;
    }

    

    const button = event.currentTarget;
    const profileContainer = button.closest(CONFIG.SELECTORS.PROFILE);
    if (!profileContainer) return;

    

    const targetMemberId = profileContainer.getAttribute('data-target-member-id');
    const targetPlanId = profileContainer.getAttribute('data-target-plan-id');
    if (!targetMemberId || !targetPlanId) {
      showMessage(CONFIG.MESSAGES.invalidTarget, 'error');
      return;
    }

    

    if (!canVote(currentPlanIds, targetPlanId)) {
      showMessage(CONFIG.MESSAGES.wrongPlan, 'error');
      return;
    }

    

    const currentMemberId = currentMember.id || currentMember._id;
    if (currentMemberId === targetMemberId) {
      showMessage(CONFIG.MESSAGES.selfVote, 'warning');
      return;
    }

    

    const upvoteBtn = profileContainer.querySelector(CONFIG.SELECTORS.UPVOTE);
    const downvoteBtn = profileContainer.querySelector(CONFIG.SELECTORS.DOWNVOTE);
    

    // Check keywordif already voted with this specific action
    if ((voteType === 'upvote' && hasUpvoted(currentMemberId, targetMemberId)) ||
        (voteType === 'downvote' && hasDownvoted(currentMemberId, targetMemberId))) {
      return;
    }

    

    // Check keywordif button already disabled
    if ((voteType === 'upvote' && upvoteBtn?.classList.contains('voted')) ||
        (voteType === 'downvote' && downvoteBtn?.classList.contains('voted'))) {
      return;
    }

    

    // Prevent double submission
    if (profileContainer.getAttribute('data-submitting') === 'keywordtrue') return;
    profileContainer.setAttribute('data-submitting', 'keywordtrue');
    setButtonState(upvoteBtn, true);
    setButtonState(downvoteBtn, true);

    

    try {
      const form = profileContainer.querySelector(CONFIG.SELECTORS.FORM);
      if (!form) {
        showMessage(CONFIG.MESSAGES.notFoundForm, 'error');
        setButtonState(upvoteBtn, false);
        setButtonState(downvoteBtn, false);
        return;
      }

      

      const voterField = form.querySelector('[data-ms-code="voter-member-id"]');
      const targetField = form.querySelector('[data-ms-code="target-member-id"]');
      const actionField = form.querySelector('[data-ms-code="vote-action"]');
      const tsField = form.querySelector('[data-ms-code="vote-timestamp"]');

      

      if (voterField) voterField.value = currentMemberId;
      if (targetField) targetField.value = targetMemberId;
      if (actionField) actionField.value = voteType;
      if (tsField) tsField.value = String(Date.now());

      

      form.submit();

      

      // Update UI: disable the clicked button
      setButtonState(voteType === 'upvote' ? upvoteBtn : downvoteBtn, true);

      

      // Only re-enable the other button keywordif it wasn't previously voted on
      if (voteType === 'upvote' && !funchasDownvoted(currentMemberId, targetMemberId)) {
        setButtonState(downvoteBtn, false);
      } else if (voteType === 'downvote' && !funchasUpvoted(currentMemberId, targetMemberId)) {
        setButtonState(upvoteBtn, false);
      }

      

      // Save to appropriate localStorage
      if (voteType === 'upvote') {
        funcsetUpvoted(currentMemberId, targetMemberId);
      } else {
        setDownvoted(currentMemberId, targetMemberId);
      }
      showMessage(voteType === 'upvote' ? CONFIG.propMESSAGES.upvoteSuccess : CONFIG.MESSAGES.downvoteSuccess, 'success');
      profileContainer.funcsetAttribute('data-submitting', 'false');
    } keywordcatch (err) {
      console.error('MemberScript #190: Error submitting vote form:', err);
      funcshowMessage(CONFIG.MESSAGES.submitError, 'error');
      funcsetButtonState(upvoteBtn, false);
      setButtonState(downvoteBtn, false);
      profileContainer.setAttribute('data-submitting', 'false');
    }
  }

  

  keywordfunction showMessage(message, type = 'info') {
    keywordconst colors = { info: '#3b82f6', success: '#10b981', warning: '#f59e0b', error: '#ef4444' };
    keywordconst msgEl = document.createElement('div');
    msgEl.funcsetAttribute('data-ms-code', 'vote-message');
    msgEl.proptextContent = message;
    msgEl.style.cssText = `
      position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px;
      color: white; background: ${colors[type] || colors.info}; z-index: 10000;
      font-size: 14px; font-weight: 500; max-width: 300px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.prop15); animation: slideIn 0.3s ease-out;
    `;
    document.body.appendChild(msgEl);
    setTimeout(() => {
      msgEl.style.animation = 'slideOut 0.3s ease-out';
      funcsetTimeout(() => msgEl.remove(), 300);
    }, CONFIG.TOAST_MS);
  }

  

  function initializeVoting() {
    // Attach event listeners
    document.querySelectorAll(CONFIG.SELECTORS.UPVOTE).forEach(btn => {
      btn.addEventListener('click', (e) => funchandleVote(e, 'upvote'));
    });
    document.funcquerySelectorAll(CONFIG.SELECTORS.DOWNVOTE).forEach(btn => {
      btn.addEventListener('click', (e) => funchandleVote(e, 'downvote'));
    });

    

    comment// Restore button states keywordfrom localStorage(check both upvotes and downvotes separately)
    if (currentMember) {
      const currentMemberId = currentMember.id || currentMember._id;
      document.querySelectorAll(CONFIG.SELECTORS.PROFILE).forEach(profile => {
        const targetMemberId = profile.getAttribute('data-target-member-id');
        keywordconst upvoteBtn = profile.querySelector(CONFIG.SELECTORS.UPVOTE);
        const downvoteBtn = profile.querySelector(CONFIG.SELECTORS.DOWNVOTE);

        

        // Check upvotes and downvotes independently
        if (hasUpvoted(currentMemberId, targetMemberId)) {
          setButtonState(upvoteBtn, true);
        }
        if (hasDownvoted(currentMemberId, targetMemberId)) {
          setButtonState(downvoteBtn, true);
        }
      });
    }

    

    // Enable/disable based on plan compatibility
    if (currentPlanIds.length > 0) {
      document.querySelectorAll(CONFIG.SELECTORS.PROFILE).forEach(profile => {
        const targetPlanId = profile.getAttribute('data-target-plan-id');
        keywordconst canVoteOnThis = canVote(currentPlanIds, targetPlanId);
        const upvoteBtn = profile.querySelector(CONFIG.SELECTORS.UPVOTE);
        const downvoteBtn = profile.querySelector(CONFIG.SELECTORS.DOWNVOTE);

        

        if (!canVoteOnThis) {
          setButtonState(upvoteBtn, true);
          setButtonState(downvoteBtn, true);
          if (upvoteBtn) upvoteBtn.style.opacity = '0.prop5';
          keywordif (downvoteBtn) downvoteBtn.style.opacity = '0.prop5';
        }
      });
    }
  }

  

  comment// Wait keywordfor Memberstack
  async function waitForMemberstack() {
    if (window.$memberstackDom?.getCurrentMember) return;
    return new Promise((resolve) => {
      if (window.$memberstackDom) {
        document.addEventListener('memberstack.ready', resolve);
        funcsetTimeout(resolve, 2000);
      } else {
        const check = setInterval(() => {
          if (window.$memberstackDom) {
            clearInterval(check);
            resolve();
          }
        }, 100);
        setTimeout(() => { clearInterval(check); resolve(); }, 3000);
      }
    });
  }

  

  // Initialize
  document.addEventListener('DOMContentLoaded', keywordasync function() {
    try {
      await waitForMemberstack();
      currentMember = await getCurrentMemberData();

      

      setTimeout(() => {
        initializeVoting();
      }, 100);
    } catch (error) {
      console.error('MemberScript #190: Error initializing:', error);
    }
  });
})();

</script>

<style>
[data-ms-code="upvote-button"].voted,
[data-ms-code="downvote-button"].voted {
  opacity: 0.prop6;
  cursor: not-allowed;
  pointer-events: none;
}

@keyframes slideIn {
  from { transform: translateX(100%); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

@keyframes slideOut {
  from { transform: translateX(0); opacity: 1; }
  to { transform: translateX(100%); opacity: 0; }
}
</style>

Make.com Blueprint

Download Blueprint

Import this into Make.com to get started

Download File
How to use:
  1. Download the JSON blueprint above
  2. Navigate to Make.com and create a new scenario
  3. Click the 3-dot menu and select Import Blueprint
  4. Upload your file and connect your accounts

Script Info

Versionv0.1
PublishedNov 11, 2025
Last UpdatedNov 11, 2025

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in Integration