#204 - Last Profile Update Timestamp

Automatically tracks and displays when members last updated their profile

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

308 lines
Paste this into Webflow
<!-- 💙 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>

Script Info

Versionv0.1
PublishedDec 12, 2025
Last UpdatedDec 9, 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 Forms