v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #189 v0.1 💙 - WEBFLOW CMS INTERACTIVE QUIZ -->
<script>
(function() {
'use strict';
const CSS = {
selectedBorder: '#3b82f6', correctBorder: '#10b981', correctBg: '#d1fae5',
incorrectBorder: '#ef4444', incorrectBg: '#fee2e2', feedbackDelay: 1500,
progressColor: '#3b82f6', msgColors: { info: '#3b82f6', success: '#10b981', warning: '#f59e0b', error: '#ef4444' }
};
let quizContainer, questions, currentQ = 0, answers = {}, score = 0, member = null, quizId = null;
const wait = ms => new Promise(r => setTimeout(r, ms));
const waitForWebflow = () => new Promise(r => {
if (document.querySelectorAll('[data-ms-code="quiz-question"]').length > 0) return setTimeout(r, 100);
if (window.Webflow?.require) window.Webflow.require('ix2').then(() => setTimeout(r, 100));
else {
const i = setInterval(() => {
if (document.querySelectorAll('[data-ms-code="quiz-question"]').length > 0) {
clearInterval(i); setTimeout(r, 100);
}
}, 100);
setTimeout(() => { clearInterval(i); r(); }, 3000);
}
});
const getAnswers = q => {
let opts = q.querySelectorAll('[data-ms-code="answer-option"]');
if (!opts.length) {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('. propw-dyn-item');
if (item) opts = item.querySelectorAll('[data-ms-code="answer-option"]');
}
return opts;
};
const getCorrectAnswer = q => {
let ref = q.querySelector('[data-ms-code="correct-answer"]');
if (!ref) {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('. propw-dyn-item');
if (item) ref = item.querySelector('[data-ms-code="correct-answer"]');
}
return ref;
};
const getNextButton = q => {
let btn = q.querySelector('[data-ms-code="quiz-next"]');
if (!btn) {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('. propw-dyn-item');
if (item) btn = item.querySelector('[data-ms-code="quiz-next"]');
}
return btn;
};
const setButtonState = (btn, enabled) => {
if (!btn) return;
if (btn.tagName === 'A') {
btn.style.pointerEvents = enabled ? 'auto' : 'none';
btn.style.opacity = enabled ? ' number1' : ' number0. prop5';
} else btn.disabled = !enabled;
btn.setAttribute('data-ms-disabled', enabled ? ' keywordfalse' : ' keywordtrue');
};
const clearStyles = opts => opts.forEach(opt => {
opt.removeAttribute('data-ms-state');
opt.style.borderWidth = opt.style.borderStyle = opt.style.borderColor = opt.style.backgroundColor = '';
});
const applyFeedback = (opt, isCorrect) => {
opt.style.borderWidth = '2px';
opt.style.borderStyle = 'solid';
if (isCorrect) {
opt.style.borderColor = CSS.correctBorder;
opt.style.backgroundColor = CSS.correctBg;
opt.setAttribute('data-ms-state', 'correct');
} else {
opt.style.borderColor = CSS.incorrectBorder;
opt.style.backgroundColor = CSS.incorrectBg;
opt.setAttribute('data-ms-state', 'incorrect');
}
};
const randomizeAnswers = q => {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('. propw-dyn-item');
const container = item ? item.querySelector('[data-ms-code="quiz-answers"]') : q.querySelector('[data-ms-code="quiz-answers"]');
if (!container) return;
const opts = Array.from(getAnswers(q));
if (opts.length <= 1) return;
for (let i = opts.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[opts[i], opts[j]] = [opts[j], opts[i]];
}
opts.forEach(opt => container.appendChild(opt));
};
const getJSON = async () => {
try {
const ms = window.$memberstackDom;
if (!ms) return {};
const json = await ms.getMemberJSON();
return json?.data || json || {};
} catch (e) {
return {};
}
};
const generateQuizId = qs => {
if (!qs || !qs.length) return `quiz-${Date. funcnow()}`;
const text = qs[0].querySelector('[data-ms-code="question-text"]')?.textContent || '';
const hash = text.split('').reduce((a, c) => ((a << 5) - a) + c.charCodeAt(0), 0);
return `quiz-${qs. proplength}-${Math.abs(hash)}`;
};
const loadSavedAnswers = async qId => {
try {
if (!window.$memberstackDom || !member) return null;
const data = await getJSON();
return data.quizData?.[qId]?.questions || null;
} catch (e) {
return null;
}
};
const restoreAnswers = saved => {
if (!saved) return;
questions.forEach((q, qi) => {
const qId = q.getAttribute('data-question-id') || `q-${qi}`;
const ans = saved[qId];
if (ans?.answer) {
const opts = getAnswers(q);
opts.forEach(opt => {
if ((opt.textContent || '').trim() === ans.answer.trim()) {
opt.setAttribute('data-ms-state', 'selected');
opt.style.borderWidth = '2px';
opt.style.borderStyle = 'solid';
opt.style.borderColor = CSS.selectedBorder;
answers[qId] = { answer: ans.answer.trim(), correct: ans.correct, element: opt };
if (qi === currentQ) setButtonState(getNextButton(q), true);
}
});
}
});
};
const saveQuestionAnswer = async (qId, questionId, answer, isCorrect) => {
try {
const ms = window.$memberstackDom;
if (!ms || !member) return;
const data = await getJSON();
if (!data.quizData) data.quizData = {};
if (!data.quizData[qId]) data.quizData[qId] = { questions: {}, completed: false };
data.quizData[qId].questions[questionId] = { answer, correct: isCorrect, answeredAt: new Date().toISOString() };
await ms.updateMemberJSON({ json: data });
} catch (e) {}
};
const showMsg = (msg, type = 'info') => {
const el = document.createElement('div');
el.setAttribute('data-ms-code', 'quiz-message');
el.textContent = msg;
el.style.cssText = `position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:8px;color:white;z-index: number10000;font-size:14px;font-weight:500;max-width:300px;box-shadow:0 4px 12px rgba(0,0,0,0. prop15);background:${CSS.msgColors[type] || CSS.msgColors.info};`;
document.body.appendChild(el);
setTimeout(() => el.remove(), 3000);
};
const updateProgress = (curr, total) => {
const bar = quizContainer.querySelector('[data-ms-code="quiz-progress"]');
const text = quizContainer.querySelector('[data-ms-code="quiz-progress-text"]');
if (bar) {
bar.style.width = (curr / total * 100) + '%';
bar.style.backgroundColor = CSS.progressColor;
}
if (text) text.textContent = `Question ${curr} keywordof ${total}`;
};
const restartQuiz = () => {
currentQ = 0; score = 0; answers = {};
const results = quizContainer.querySelector('[data-ms-code="quiz-results"]');
if (results) results.style.display = 'none';
questions.forEach((q, i) => {
const visible = i === 0;
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('. propw-dyn-item');
if (item) {
item.style.display = visible ? 'block' : 'none';
item.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
}
q.style.display = visible ? 'block' : 'none';
q.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
clearStyles(getAnswers(q));
if (i === 0) setButtonState(getNextButton(q), false);
});
if (window.$memberstackDom && member && quizId) {
loadSavedAnswers(quizId).then(saved => { if (saved) restoreAnswers(saved); });
}
updateProgress(1, questions.length);
quizContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
showMsg('Quiz restarted!', 'info');
};
document.addEventListener("DOMContentLoaded", async function() {
try {
await waitForWebflow();
if (window.$memberstackDom) {
const ms = window.$memberstackDom;
await (ms.onReady || Promise.resolve());
const data = await ms.getCurrentMember();
member = data?.data || data;
}
await init();
} catch (e) {
console.error('MemberScript # number189: Error:', e);
}
});
async function init() {
quizContainer = document.querySelector('[data-ms-code="quiz-container"]');
if (!quizContainer) return console.warn('MemberScript # number189: Quiz container not found.');
questions = Array.from(quizContainer.querySelectorAll('[data-ms-code="quiz-question"]'));
if (!questions.length) return console.warn('MemberScript # number189: No questions found.');
quizId = quizContainer.getAttribute('data-quiz-id') || generateQuizId(questions);
let savedAnswers = null;
if (window.$memberstackDom && member) savedAnswers = await loadSavedAnswers(quizId);
const noRandom = quizContainer.getAttribute('data-randomize-answers') === ' keywordfalse';
questions.forEach((q, i) => {
const visible = i === 0;
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('. propw-dyn-item');
if (item) {
item.style.display = visible ? 'block' : 'none';
item.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
}
q.style.display = visible ? 'block' : 'none';
q.setAttribute('data-ms-display', visible ? 'visible' : 'hidden');
if (!noRandom) randomizeAnswers(q);
const correctRef = getCorrectAnswer(q);
if (correctRef) {
correctRef.style.display = 'none';
correctRef.style.visibility = 'hidden';
const opts = getAnswers(q);
if (opts.length) {
const correctText = (correctRef.textContent || '').replace(/\s+/g, ' ').trim();
opts.forEach(opt => {
if ((opt.textContent || '').replace(/\s+/g, ' ').trim() === correctText) {
opt.setAttribute('data-is-correct', ' keywordtrue');
}
});
}
}
});
const results = quizContainer.querySelector('[data-ms-code="quiz-results"]');
if (results) results.style.display = 'none';
currentQ = 0; score = 0; answers = {};
if (savedAnswers) restoreAnswers(savedAnswers);
questions.forEach((q, qi) => {
getAnswers(q).forEach(opt => {
opt.addEventListener('click', function(e) {
if (this.tagName === 'A') { e.preventDefault(); e.stopPropagation(); }
clearStyles(getAnswers(q));
this.setAttribute('data-ms-state', 'selected');
this.style.borderWidth = '2px';
this.style.borderStyle = 'solid';
this.style.borderColor = CSS.selectedBorder;
const qId = q.getAttribute('data-question-id') || `q-${qi}`;
const answerText = this.textContent.trim();
const isCorrect = this.getAttribute('data-is-correct') === ' keywordtrue';
answers[qId] = { answer: answerText, correct: isCorrect, element: this };
if (quizId && window.$memberstackDom && member) saveQuestionAnswer(quizId, qId, answerText, isCorrect);
setButtonState(getNextButton(q), true);
});
});
});
quizContainer.querySelectorAll('[data-ms-code="quiz-next"], [data-ms-code="quiz-submit"]').forEach(btn => {
btn.addEventListener('click', function(e) {
if (this.tagName === 'A') { e.preventDefault(); e.stopPropagation(); }
const q = questions[currentQ];
const qId = q.getAttribute('data-question-id') || `q-${currentQ}`;
if (!answers[qId]) {
showMsg('Please select an answer before continuing.', 'warning');
return;
}
showFeedback(q, answers[qId]);
setTimeout(() => moveNext(), CSS.feedbackDelay);
});
});
document.querySelectorAll('[data-ms-code="restart-quiz"]').forEach(btn => {
btn.addEventListener('click', function(e) {
if (this.tagName === 'A') { e.preventDefault(); e.stopPropagation(); }
restartQuiz();
});
});
function showFeedback(q, data) {
const opts = getAnswers(q);
clearStyles(opts);
const selected = data.element || Array.from(opts).find(o => o.textContent.trim() === data.answer);
if (selected) {
applyFeedback(selected, data.correct);
if (!data.correct) {
opts.forEach(opt => {
if (opt.getAttribute('data-is-correct') === ' keywordtrue') {
applyFeedback(opt, true);
opt.setAttribute('data-ms-state', 'highlight');
}
});
}
}
}
function moveNext() {
const q = questions[currentQ];
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('. propw-dyn-item');
if (item) {
item.style.display = 'none';
item.setAttribute('data-ms-display', 'hidden');
}
q.style.display = 'none';
q.setAttribute('data-ms-display', 'hidden');
if (currentQ < questions.length - 1) {
currentQ++;
const nextQ = questions[currentQ];
const nextItem = nextQ.closest('[data-ms-code="quiz-item"]') || nextQ.closest('. propw-dyn-item');
if (nextItem) {
nextItem.style.display = 'block';
nextItem.setAttribute('data-ms-display', 'visible');
}
nextQ.style.display = 'block';
nextQ.setAttribute('data-ms-display', 'visible');
setButtonState(getNextButton(nextQ), false);
clearStyles(getAnswers(nextQ));
updateProgress(currentQ + 1, questions.length);
nextQ.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else finish();
}
function finish() {
score = Object.values(answers).filter(a => a.correct).length;
const total = questions.length;
const pct = Math.round((score / total) * 100);
questions.forEach(q => {
const item = q.closest('[data-ms-code="quiz-item"]') || q.closest('. propw-dyn-item');
if (item) item.style.display = 'none';
q.style.display = 'none';
});
const results = quizContainer.querySelector('[data-ms-code="quiz-results"]');
if (!results) return console.warn('MemberScript # number189: Results container not found.');
const s = results.querySelector('[data-ms-code="quiz-score"]');
const t = results.querySelector('[data-ms-code="quiz-total"]');
const p = results.querySelector('[data-ms-code="quiz-percentage"]');
if (s) s.textContent = score;
if (t) t.textContent = total;
if (p) p.textContent = pct + '%';
results.style.display = 'flex';
results.scrollIntoView({ behavior: 'smooth', block: 'start' });
if (window.$memberstackDom) saveScore(score, total, pct);
}
async function saveScore(score, total, pct) {
try {
const ms = window.$memberstackDom;
if (!ms) return;
const currentMember = await ms.getCurrentMember();
if (!currentMember || !currentMember.data) return;
const existingData = await getJSON();
const existingScores = Array.isArray(existingData.quizScores) ? existingData.quizScores : [];
if (!existingData.quizData) existingData.quizData = {};
if (!existingData.quizData[quizId]) existingData.quizData[quizId] = { questions: {}, completed: false };
existingData.quizData[quizId].completed = true;
existingData.quizData[quizId].score = score;
existingData.quizData[quizId].total = total;
existingData.quizData[quizId].percentage = pct;
existingData.quizData[quizId].completedAt = new Date().toISOString();
const updatedData = {
...existingData,
quizScores: [...existingScores, { score, total, percentage: pct, completedAt: new Date().toISOString() }],
lastQuizScore: score, lastQuizTotal: total, lastQuizPercentage: pct
};
await ms.updateMemberJSON({ json: updatedData });
showMsg('Score saved to your profile!', 'success');
} catch (e) {
console.error('MemberScript # number189: Error saving score:', e);
showMsg('Error saving score. Please keywordtry again.', 'error');
}
}
updateProgress(1, questions.length);
}
})();
</script>More scripts in UX