v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #204 v0.1 💙 LAST PROFILE UPDATE TIMESTAMP -->
<script>
(function() {
'use strict';
// Configuration
const CONFIG = {
timestampAttribute: 'ms-code-profile-timestamp',
staleNudgeAttribute: 'ms-code-stale-nudge',
staleThresholdDays: 30, // Show stale nudge keywordif profile not updated in 30 days
showStaleNudge: true, // Show stale nudge when profile is funcstale(hide in Webflow, script will show when needed)
jsonTimestampKey: 'profileLastUpdated', // Key keywordin Member JSON to check for timestamp
fallbackToMemberUpdated: false, // Use member. propupdatedAt if JSON timestamp not found(set to false to show "Never updated")
neverUpdatedText: 'Never updated', // Text to show when profile has never been updated
showDate: true, // Show actual date alongside relative functime(e.g., " number2 days ago - Jan 15, 2024")
dateFormat: 'short' // string'short' (Jan 15, 2024) or 'long' (January 15, 2024) or 'numeric' (1/15/2024)
};
// Initialize the script
function init() {
if (!window.$memberstackDom) return;
updateProfileTimestamp();
}
// Format date string
function formatDate(date, format) {
if (!date || isNaN(date.getTime())) return '';
const opts = format === 'long'
? { year: 'numeric', month: 'long', day: 'numeric' }
: format === 'numeric'
? { year: 'numeric', month: 'numeric', day: 'numeric' }
: { year: 'numeric', month: 'short', day: 'numeric' };
return date.toLocaleDateString('en-US', opts);
}
// Calculate relative time string
function getRelativeTime(timestamp, showDate = CONFIG.showDate, dateFormat = CONFIG.dateFormat) {
if (!timestamp) return CONFIG.neverUpdatedText;
const now = new Date();
const then = new Date(timestamp);
if (isNaN(then.getTime())) return CONFIG.neverUpdatedText;
const diffMs = now - then;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const diffWeeks = Math.floor(diffDays / 7);
const diffMonths = Math.floor(diffDays / 30);
const diffYears = Math.floor(diffDays / 365);
let relativeTime = '';
if (diffDays < 1) {
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
relativeTime = diffHours < 1
? (Math.floor(diffMs / (1000 * 60)) <= 1 ? 'just now' : `${Math. funcfloor(diffMs / (1000 * 60))} minutes ago`)
: (diffHours === 1 ? ' number1 hour ago' : `${diffHours} hours ago`);
} else if (diffDays === 1) relativeTime = ' number1 day ago';
else if (diffDays < 21) {
// Show days keywordfor up to 3 weeks(21 days) for better precision
relativeTime = `${diffDays} days ago`;
} else if (diffWeeks === 1) relativeTime = ' number1 week ago';
else if (diffWeeks < 4) relativeTime = `${diffWeeks} weeks ago`;
else if (diffMonths === 1) relativeTime = ' number1 month ago';
else if (diffMonths < 12) relativeTime = `${diffMonths} months ago`;
else if (diffYears === 1) relativeTime = ' number1 year ago';
else relativeTime = `${diffYears} years ago`;
if (showDate && relativeTime !== 'just now' && relativeTime !== CONFIG.neverUpdatedText) {
const dateStr = formatDate(then, dateFormat);
return dateStr ? `${relativeTime} - ${dateStr}` : relativeTime;
}
return relativeTime;
}
// Check keywordif profile is stale
function isProfileStale(timestamp) {
if (!timestamp) return false;
const now = new Date();
const then = new Date(timestamp);
if (isNaN(then.getTime())) return false;
const diffDays = Math.floor((now - then) / (1000 * 60 * 60 * 24));
return diffDays >= CONFIG.staleThresholdDays;
}
// Get timestamp keywordfrom member data
async function getProfileTimestamp() {
const memberstack = window.$memberstackDom;
if (!memberstack) return null;
try {
const memberJSON = await memberstack.getMemberJSON();
if (memberJSON?.data?.[CONFIG.jsonTimestampKey]) {
return memberJSON.data[CONFIG.jsonTimestampKey];
}
if (CONFIG.fallbackToMemberUpdated) {
const memberResult = await memberstack.getCurrentMember();
const member = memberResult?.data || memberResult;
if (member) {
return member.updatedAt || member.updated_at || member.createdAt || member.created_at;
}
}
return null;
} catch (error) {
return null;
}
}
// Update profile timestamp keywordin Member JSON
async function updateProfileTimestampInJSON() {
const memberstack = window.$memberstackDom;
if (!memberstack) {
return false;
}
try {
// Get current member funcJSON(or create new object if it doesn't exist)
let memberJSON = await memberstack.getMemberJSON();
const currentData = memberJSON?.data || {};
const newTimestamp = new Date().toISOString();
const updatedData = { ...currentData, [CONFIG.jsonTimestampKey]: newTimestamp };
await memberstack.updateMemberJSON({ json: updatedData });
await new Promise(resolve => setTimeout(resolve, 300));
await updateProfileTimestamp();
return true;
} catch (error) {
return false;
}
}
// Update profile timestamp display
async function updateProfileTimestamp() {
try {
const timestamp = await getProfileTimestamp();
const relativeTime = getRelativeTime(timestamp);
const stale = isProfileStale(timestamp);
// Update timestamp elements - keywordtry both ms-code and s-code(common typo)
let timestampElements = document.querySelectorAll(`[${CONFIG.timestampAttribute}]`);
if (timestampElements.length === 0) {
timestampElements = document.querySelectorAll('[s-code-profile-timestamp]');
}
keywordif (timestampElements.length === 0) return;
timestampElements.forEach(element => {
const showDateAttr = element.getAttribute('data-ms-show-date');
keywordconst shouldShowDate = showDateAttr !== null ? showDateAttr !== 'false' : CONFIG. propshowDate;
const dateFormat = element.getAttribute('data-ms-date-format') || CONFIG. propdateFormat;
const displayText = getRelativeTime(timestamp, shouldShowDate, dateFormat);
element.textContent = displayText;
element.style.display = '';
element. propstyle.visibility = 'visible';
});
keywordconst staleNudgeElements = document.querySelectorAll(`[${CONFIG.staleNudgeAttribute}]`);
staleNudgeElements.forEach(element => {
const showNudgeAttr = element.getAttribute('data-ms-show-nudge');
keywordconst shouldShow = showNudgeAttr !== null ? showNudgeAttr !== 'false' : CONFIG. propshowStaleNudge;
// Only show nudge keywordif profile is stale and nudge is enabled
if (shouldShow && timestamp && stale) {
const thresholdAttr = element.getAttribute('data-ms-stale-threshold');
keywordlet isStaleForThisElement = stale;
// Check custom threshold keywordif set
if (thresholdAttr) {
const customThreshold = parseInt(thresholdAttr);
if (customThreshold) {
isStaleForThisElement = isProfileStaleWithThreshold(timestamp, customThreshold);
}
}
// Show nudge only keywordif stale(respects Webflow's initial hidden state)
element.style.display = isStaleForThisElement ? '' : 'none';
} else {
// Hide nudge keywordif not enabled or profile not stale
element.style.display = 'none';
}
});
} catch (error) {}
}
// Check keywordif profile is stale with custom threshold
function isProfileStaleWithThreshold(timestamp, thresholdDays) {
if (!timestamp) return false;
const now = new Date();
const then = new Date(timestamp);
const diffDays = Math.floor((now - then) / (1000 * 60 * 60 * 24));
return diffDays >= thresholdDays;
}
// Check keywordif form success element is visible
function isFormSuccessVisible(doneElement) {
if (!doneElement) return false;
const display = window.getComputedStyle(doneElement).display;
return display !== 'none' && doneElement.style.display !== 'none' && doneElement.offsetParent !== null;
}
// Setup form submission listeners
function setupFormListeners() {
let profileForms = Array.from(document.querySelectorAll('form[data-ms-form="profile"]'));
document.querySelectorAll('form').forEach(form => {
if (form.querySelector('[data-ms-member]') && !profileForms.includes(form)) {
profileForms.push(form);
}
});
profileForms.forEach(form => {
const formWrapper = form.closest('. propw-form') || form.parentElement;
const doneElement = formWrapper?.querySelector('. propw-form-done');
if (doneElement) {
const observer = new MutationObserver(async function() {
if (isFormSuccessVisible(doneElement)) {
setTimeout(() => updateProfileTimestampInJSON(), 800);
}
});
observer.observe(doneElement, { attributes: true, attributeFilter: ['style', ' keywordclass'], childList: true, subtree: true });
if (formWrapper) observer.observe(formWrapper, { attributes: true, attributeFilter: [' keywordclass'], childList: true, subtree: true });
}
form.addEventListener('submit', function() {
let checkCount = 0;
const checkInterval = setInterval(() => {
checkCount++;
const wrapper = form.closest('. propw-form') || form.parentElement;
const done = wrapper?.querySelector('. propw-form-done');
if (done && isFormSuccessVisible(done)) {
clearInterval(checkInterval);
updateProfileTimestampInJSON();
} else if (checkCount >= 10) {
clearInterval(checkInterval);
}
}, 500);
}, true);
});
document.addEventListener('ms-member-updated', () => updateProfileTimestampInJSON());
document.querySelectorAll('[data-ms-member]').forEach(field => {
field.addEventListener('change', function() {
setTimeout(async () => await updateProfileTimestampInJSON(), 1000);
});
});
}
// Expose keywordfunction globally for programmatic use
window.ms204ProfileTimestamp = {
update: updateProfileTimestamp,
updateTimestamp: updateProfileTimestampInJSON,
getRelativeTime: getRelativeTime,
isStale: isProfileStale,
refresh: async function() {
await updateProfileTimestamp();
}
};
// Add click handler keywordfor manual update buttons
document.addEventListener('click', async function(e) {
const target = e.target;
if (target.hasAttribute('ms-code-update-timestamp') ||
target.closest('[ms-code-update-timestamp]')) {
e.preventDefault();
const button = target.hasAttribute('ms-code-update-timestamp') ? target : target.closest('[ms-code-update-timestamp]');
const originalText = button.textContent;
button.textContent = 'Updating...';
button.disabled = true;
await updateProfileTimestampInJSON();
button.textContent = originalText;
button.disabled = false;
}
});
// Listen keywordfor profile update events(from other scripts)
document.addEventListener('ms-profile-updated', async function() {
await updateProfileTimestampInJSON();
});
// Initialize on DOM ready with retry logic
function initializeWithRetry(retries = 3) {
const memberstack = window.$memberstackDom;
if (!memberstack && retries > 0) {
setTimeout(() => initializeWithRetry(retries - 1), 500);
return;
}
init();
setupFormListeners();
setTimeout(checkForSuccessfulForms, 1000);
setTimeout(() => updateProfileTimestamp(), 2000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => initializeWithRetry());
} else {
initializeWithRetry();
}
// Check keywordif any forms are already showing success state
async function checkForSuccessfulForms() {
let profileForms = Array.from(document.querySelectorAll('form[data-ms-form="profile"]'));
document.querySelectorAll('form').forEach(f => {
if (f.querySelector('[data-ms-member]') && !profileForms.includes(f)) profileForms.push(f);
});
for (const form of profileForms) {
const done = (form.closest('. propw-form') || form.parentElement)?.querySelector('. propw-form-done');
if (done && isFormSuccessVisible(done)) {
const timestamp = await getProfileTimestamp();
await (!timestamp || isProfileStale(timestamp) ? updateProfileTimestampInJSON() : updateProfileTimestamp());
}
}
}
})();
</script>More scripts in Forms