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

3. Klicken Sie auf das kleine Kästchen mit den 3 Punkten und dann auf Blaupause importieren...

4. Laden Sie Ihre Datei hoch und voila! Sie sind bereit, Ihre eigenen Konten zu verknüpfen.
Brauchen Sie Hilfe mit diesem MemberScript?
Alle Memberstack-Kunden können im 2.0 Slack um Unterstützung bitten. Bitte beachten Sie, dass dies keine offiziellen Funktionen sind und der Support nicht garantiert werden kann.
Treten Sie dem 2.0 Slack beiAutorisierung und Zahlungen für Webflow-Websites
Fügen Sie Ihrer Webflow-Website Logins, Abonnements, Gated Content und vieles mehr hinzu - einfach und vollständig anpassbar.
.webp)
"Wir verwenden Memberstack schon seit langem, und Es hat uns geholfen, Dinge zu erreichen, die wir mit Webflow nie für möglich gehalten hätten. Es hat uns ermöglicht, Plattformen mit großer Tiefe und Funktionalität zu bauen, und das Team dahinter war immer sehr hilfsbereit und offen für Feedback.

"Ich habe eine Mitgliedschaftsseite mit Memberstack und Jetboost für einen Kunden erstellt. Es fühlt sich wie Magie an, mit diesen Tools zu bauen. Als jemand, der in einer Agentur gearbeitet hat, wo einige dieser Anwendungen von Grund auf neu programmiert wurden, verstehe ich jetzt endlich den Hype. Das ist viel schneller und viel billiger."

"Eines der besten Produkte, um eine Mitgliederseite zu starten - ich mag die Benutzerfreundlichkeit von Memberstack. Ich war in der Lage, meine Mitgliederseite innerhalb eines Tages einzurichten und zu betreiben.. Einfacher geht's nicht. Außerdem bietet es die Funktionalität, die ich brauche, um die Benutzererfahrung individueller zu gestalten."

"Mein Geschäft wäre ohne Memberstack nicht das, was es ist. Wenn Sie denken, dass $30/Monat teuer sind, versuchen Sie mal, einen Entwickler zu engagieren, der für diesen Preis individuelle Empfehlungen in Ihre Website integriert. Unglaublich flexible Tools für diejenigen, die bereit sind, einige minimale Anstrengungen zu unternehmen und die gut zusammengestellte Dokumentation zu lesen."


"Die Slack-Community ist eine der aktivsten, die ich kenne, und andere Kunden sind bereit, Fragen zu beantworten und Lösungen anzubieten. Ich habe ausführliche Bewertungen von alternativen Tools durchgeführt und wir kommen immer wieder auf Memberstack zurück - sparen Sie sich die Zeit und probieren Sie es aus."

Brauchen Sie Hilfe mit diesem MemberScript? Treten Sie unserer Slack-Community bei!
Treten Sie der Memberstack-Community Slack bei und fragen Sie los! Erwarten Sie eine prompte Antwort von einem Team-Mitglied, einem Memberstack-Experten oder einem anderen Community-Mitglied.
Unserem Slack beitreten
.png)