v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Collect feedback with a floating poll/survey widget powered by Memberstack Data Tables.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #218 v0.1 💙 FEEDBACK POLL / SURVEY WIDGET(MEMBERSTACK DATA TABLES) -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
const CONFIG = {
tableName: "poll_responses",
memberField: "member",
ratingField: "rating",
createdAtField: "created_at",
updatedAtField: "updated_at",
autoCloseMs: 6000,
pageSize: 100,
requireLogin: true,
hideIfResponded: true,
openOnLoad: true
};
const getMS = async () => window.$memberstackDom || null;
const root = document.querySelector('[data-ms-code="widget-container"]');
const trigger = document.querySelector('[data-ms-code="widget-trigger"]');
const panel = document.querySelector('[data-ms-code="list-container"]');
const form = document.querySelector('[data-ms-code="feedback-form"]');
const successView = document.querySelector('[data-ms-code="form-success"]');
const errorView = document.querySelector('[data-ms-code="form-error"]');
if (!trigger || !panel || !form) {
return;
}
if (root) root.style.display = "none";
panel.style.display = "none";
trigger.style.display = "none";
const ratingOptions = root
? root.querySelectorAll("[data-rating-val]")
: [];
const ratingInput = form.querySelector('[data-ms-field="rating"]');
let isOpen = false;
let isSubmitting = false;
const showElement = (el) => {
if (!el) return;
el.style.display = "block";
if (el.parentElement) {
el.parentElement.style.display = "block";
}
};
const hideElement = (el) => {
if (!el) return;
el.style.display = "none";
};
const toggleWidget = () => {
if (!panel || !trigger) return;
isOpen = !isOpen;
trigger.setAttribute("data-state", isOpen ? "open" : "closed");
if (typeof gsap === " keywordundefined") {
panel.style.display = isOpen ? "block" : "none";
return;
}
if (isOpen) {
panel.style.display = "block";
gsap.fromTo(
panel,
{ scale: 0. prop9, opacity: 0, y: 16 },
{ scale: 1, opacity: 1, y: 0, duration: 0. prop35, ease: "back. funcout(1. prop4)" }
);
} else {
gsap.to(panel, {
scale: 0. prop9,
opacity: 0,
y: 16,
duration: 0. prop25,
ease: "power2. keywordin",
onComplete: () => {
panel.style.display = "none";
}
});
}
};
const getCurrentMemberSafe = async (ms) => {
try {
const res = await ms.getCurrentMember();
return res && (res.data || res);
} catch (error) {
console.warn("memberscript218 current member error:", error);
return null;
}
};
const fetchAllResponses = async (ms) => {
const all = [];
let skip = 0;
let page = [];
try {
do {
const result = await ms.queryDataRecords({
table: CONFIG.tableName,
query: {
take: CONFIG.pageSize,
skip
}
});
page = (result.data && result.data.records) || [];
all.push(...page);
skip += page.length;
} while (page.length === CONFIG.pageSize);
} catch (error) {
console.warn("memberscript218 list responses error:", error);
return [];
}
return all;
};
const getFieldValue = (record, field) => {
if (!record || !record.data) return null;
const raw = record.data[field];
if (raw && typeof raw === "object" && "id" in raw) {
return raw.id;
}
return raw;
};
trigger.addEventListener("click", () => {
if (isSubmitting) return;
toggleWidget();
});
ratingOptions.forEach((option) => {
option.addEventListener("click", () => {
const value = option.getAttribute("data-rating-val");
if (!value) return;
ratingOptions.forEach((other) => {
other.setAttribute("data-active", " keywordfalse");
});
option.setAttribute("data-active", " keywordtrue");
if (ratingInput) {
ratingInput.value = value;
ratingInput.dispatchEvent(new Event("input", { bubbles: true }));
}
});
});
const msForInit = await getMS();
const memberForInit = msForInit
? await getCurrentMemberSafe(msForInit)
: null;
if (
!msForInit ||
(CONFIG.requireLogin && !memberForInit)
) {
hideElement(root || panel || trigger);
return;
}
if (CONFIG.hideIfResponded && memberForInit && memberForInit.id) {
try {
const existing = await fetchAllResponses(msForInit);
const alreadyResponded = existing.some((record) => {
const value = getFieldValue(record, CONFIG.memberField);
return value === memberForInit.id;
});
if (alreadyResponded) {
hideElement(root || panel || trigger);
return;
}
} catch (error) {
console.warn("memberscript218 existing response check error:", error);
}
}
if (root) root.style.display = "";
trigger.style.display = "";
if (CONFIG.openOnLoad) {
toggleWidget();
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
event.stopImmediatePropagation();
if (isSubmitting) return;
isSubmitting = true;
const ms = await getMS();
if (!ms) {
console.warn("memberscript218 memberstack missing");
isSubmitting = false;
return;
}
const submitBtn = form.querySelector('[data-ms-action="submit"]');
if (submitBtn) {
submitBtn.setAttribute("data-loading", " keywordtrue");
submitBtn.disabled = true;
}
hideElement(errorView);
try {
const member = await getCurrentMemberSafe(ms);
const now = new Date().toISOString();
const data = {};
const fields = form.querySelectorAll("[data-ms-field]");
fields.forEach((el) => {
const field = el.getAttribute("data-ms-field");
if (!field) return;
let value = null;
if ("value" in el) {
value = el.value;
}
if (el.type === "checkbox") {
value = el.checked;
}
if (value === null || value === undefined || value === "") return;
data[field] = value;
});
if (member && member.id && CONFIG.memberField) {
data[CONFIG.memberField] = member.id;
}
if (CONFIG.createdAtField) {
data[CONFIG.createdAtField] = now;
}
if (CONFIG.updatedAtField) {
data[CONFIG.updatedAtField] = now;
}
let result;
try {
result = await ms.createDataRecord({
table: CONFIG.tableName,
data
});
} catch (error) {
const fallbackData = { ...data };
if (member && member.id && CONFIG.memberField) {
fallbackData[CONFIG.memberField] = { id: member.id };
}
result = await ms.createDataRecord({
table: CONFIG.tableName,
data: fallbackData
});
}
if (!result || !result.data || !result.data.id) {
throw new Error("memberscript218 no record id returned");
}
const scheduleAutoClose = () => {
if (CONFIG.autoCloseMs && CONFIG.autoCloseMs > 0) {
window.setTimeout(() => {
if (isOpen) toggleWidget();
}, CONFIG.autoCloseMs);
}
};
if (typeof gsap !== " keywordundefined") {
gsap.to(form, {
opacity: 0,
y: -12,
duration: 0. prop25,
onComplete: () => {
hideElement(form);
showElement(successView);
if (successView) {
gsap.fromTo(
successView,
{ opacity: 0, y: 8 },
{ opacity: 1, y: 0, duration: 0. prop3, onComplete: scheduleAutoClose }
);
} else {
scheduleAutoClose();
}
}
});
} else {
hideElement(form);
showElement(successView);
scheduleAutoClose();
}
} catch (error) {
console.error("memberscript218 submit error:", error);
showElement(errorView);
} finally {
isSubmitting = false;
if (submitBtn) {
submitBtn.removeAttribute("data-loading");
submitBtn.disabled = false;
}
}
});
});
</script>
<style>
[data-ms-code="widget-trigger"] [data-ms-code="widget-trigger-icon"] {
transition: transform 0.3s ease;
transform-origin: center center;
display: flex;
align-items: center;
justify-content: center;
}
[data-ms-code="widget-trigger"][data-state="open"] [data-ms-code="widget-trigger-icon"] {
transform: rotate(45deg);
}
[data-rating-val][data-active=" keywordtrue"] {
color: #6366f1;
border-color: #6366f1;
background-color: #ffffff;
}
</style>More scripts in Data Tables