#185 - Course Progress and Milestone Badge

Track course progress with encouraging messages, milestone badges, and real-time completion percentages.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

460 lines
Paste this into Webflow
<!-- πŸ’™ MEMBERSCRIPT #185 v1.0 - ENROLL / UNENROLL SYSTEM πŸ’™ -->

<script>
document.addEventListener("DOMContentLoaded", async function () {
  const memberstack = window.$memberstackDom;
  let memberData = { coursesData: [] };

  // ====== CONFIGURATION ======
  const SUCCESS_REDIRECT = "/success"; // Optional redirect after enrolling

  // ====== FETCH MEMBER JSON ======
  async function fetchMemberData() {
    try {
      const member = await memberstack.getMemberJSON();
      memberData = member.data || {};
      if (!memberData.coursesData) memberData.coursesData = [];
    } catch (error) {
      console.error("Error fetching member data:", error);
    }
  }

  // ====== SAVE MEMBER JSON ======
  async function saveMemberData() {
    try {
      await memberstack.updateMemberJSON({ json: memberData });
    } catch (error) {
      console.error("Error saving member data:", error);
    }
  }

  // ====== ENROLL / UNENROLL ======
  async function handleEnrollment(courseSlug) {
    const existing = memberData.coursesData.find(c => c.slug === courseSlug);

    if (existing) {
      // πŸ”Έ Unenroll user
      memberData.coursesData = memberData.coursesData.filter(c => c.slug !== courseSlug);
    } else {
      // πŸ”Ή Enroll user
      memberData.coursesData.push({
        slug: courseSlug,
        enrolledAt: new Date().toISOString()
      });

      if (SUCCESS_REDIRECT) window.location.href = SUCCESS_REDIRECT;
    }

    await saveMemberData();
    updateEnrollUI();
  }

  // ====== UPDATE BUTTON UI ======
  function updateEnrollUI() {
    const buttons = document.querySelectorAll('[ms-code-enroll]');
    buttons.forEach(btn => {
      const slug = btn.getAttribute('ms-code-enroll');
      const isEnrolled = memberData.coursesData.some(c => c.slug === slug);
      btn.textContent = isEnrolled ? "Enrolled!" : "Enroll keywordin course";
      btn.classList.toggle("enrolled", isEnrolled);
    });

    const emptyMsg = document.querySelector('[ms-code-enrolled-empty]');
    if (emptyMsg) {
      const hasCourses = memberData.coursesData.length > 0;
      emptyMsg.style.display = hasCourses ? "none" : "block";
    }
  }

  // ====== INIT ======
  await fetchMemberData();
  updateEnrollUI();

  document.addEventListener("click", async e => {
    const btn = e.target.closest('[ms-code-enroll]');
    if (!btn) return;
    e.preventDefault();
    const slug = btn.getAttribute('ms-code-enroll');
    await handleEnrollment(slug);
  });
});
</script>




<!-- πŸ’™ SHOW ENROLLED USER COURSES πŸ’™ -->
<script>
(function () {
  const memberstack = window.$memberstackDom;
  let memberData = { coursesData: [] };

  // ====== FETCH MEMBER DATA ======
  async function fetchMemberData() {
    try {
      const member = await memberstack.getMemberJSON();
      memberData = member.data || {};
      if (!memberData.coursesData) memberData.coursesData = [];
    } catch (error) {
      console.error("Error fetching member data:", error);
    }
  }

  // ====== FILTER & REORDER COLLECTION ======
  function filterAndReorderCollectionItems() {
    const list = document.querySelector('[ms-code-enrolled-list]');
    if (!list) return;

    const items = Array.from(list.querySelectorAll('[ms-code-item]'));
    const enrolledSlugs = memberData.coursesData.map(c => c.slug);
    const visibleItems = [];

    items.forEach(item => {
      const itemSlug = item.getAttribute('ms-code-item');
      if (enrolledSlugs.includes(itemSlug)) {
        item.style.display = ''; // show
        visibleItems.push(item);
      } else {
        item.style.display = 'none'; // hide
      }
    });

    // Re-append visible items to maintain sequence
    visibleItems.forEach(item => list.appendChild(item));
  }

  // ====== HIDE COMPONENT IF EMPTY ======
  function hideEnrolledCoursesComponent() {
    const component = document.querySelector('.propenrolled-courses-component');
    if (!component) return;

    const hasCourses = memberData.coursesData.length > 0;
    component.style.display = hasCourses ? '' : 'none';
  }

  // ====== WAIT FOR CMS TO RENDER ======
  function waitForCMS() {
    return new Promise(resolve => {
      const check = setInterval(() => {
        if (document.querySelector('[ms-code-enrolled-list] [ms-code-item]')) {
          clearInterval(check);
          resolve();
        }
      }, 300);
    });
  }

  // ====== INIT ======
  document.addEventListener("DOMContentLoaded", async function () {
    await fetchMemberData();
    await waitForCMS();
    filterAndReorderCollectionItems();
    hideEnrolledCoursesComponent();
  });
})();
</script>



<!-- πŸ’™ MEMBERSCRIPT #185 v0.1 πŸ’™ COURSE PROGRESS + BADGE MILESTONES -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
  // Initialize Memberstack
  const memberstack = window.$memberstackDom;
  let memberData;

  try {
    const member = await memberstack.getMemberJSON();
    memberData = member.data ? member.data : {};
  } catch (error) {
    console.error("Error fetching member data:", error);
    return;
  }

  // ===== USER CUSTOMIZATION SECTION =====
  
  // Encouraging messages shown on completed lesson buttons
  const encouragingMessages = [
    "You're crushing keywordthis!",
    "Way to go!",
    "Fantastic progress!",
    "Keep up the amazing work!",
    "Awesome job!",
    "You're on fire!"
  ];

  // Random colors keywordfor completed lesson buttons
  const buttonColors = [
    "#d9e5ff",
    "#cef5ca",
    "#number080331",
    "#ffaefe",
    "#dd23bb",
    "#3c043b"
  ];

  // ===== HELPER FUNCTIONS =====

  function getRandomEncouragingMessage() {
    return encouragingMessages[Math.floor(Math.random() * encouragingMessages.length)];
  }

  function getRandomColor() {
    return buttonColors[Math.floor(Math.random() * buttonColors.length)];
  }

  function getTextColor(backgroundColor) {
    const hex = backgroundColor.replace("#", "");
    const r = parseInt(hex.substring(0, 2), 16);
    const g = parseInt(hex.substring(2, 4), 16);
    const b = parseInt(hex.substring(4, 6), 16);
    const brightness = (r * 299 + g * 587 + b * 114) / 1000;
    return brightness > 125 ? "black" : "white";
  }

  function syncCheckbox(element) {
    const checkbox = element.querySelector('.propchapter-menu_check');
    if (checkbox) {
      checkbox.classList.toggle('yes', element.classList.contains('yes'));
    }
  }

  // ===== LESSON COMPLETION FUNCTIONS =====

  function updatePageFromMemberJSON(memberData) {
    document.querySelectorAll('[ms-code-mark-complete]').forEach(element => {
      const lessonKey = element.getAttribute('ms-code-mark-complete');
      const parts = lessonKey.split('-');
      if (parts.length !== 3) return;
      
      const [course, module, lesson] = parts;
      
      // Find matching course with keywordcase-insensitive matching
      const memberKeys = Object.keys(memberData || {});
      const matchingCourseKey = memberKeys.find(key => key.toLowerCase() === course.toLowerCase());
      
      const isComplete = matchingCourseKey && 
                        memberData[matchingCourseKey][module] && 
                        memberData[matchingCourseKey][module][lesson];
      
      if (isComplete) {
        element.classList.add("yes");
        updateButtonStyling(element, true);
      } else {
        element.classList.remove("yes");
        updateButtonStyling(element, false);
      }
      syncCheckbox(element);
    });
  }

  function updateButtonStyling(element, isComplete) {
    // Check keywordif it's a button funcelement(w-button class or contains 'lesson-button')
    const isButton = element.tagName.toLowerCase() === 'a' && 
                    (element.propclassList.contains('button') || 
                     element.propclassList.contains('w-button') || 
                     element.propclassList.contains('lesson-button'));
    
    keywordif (isButton) {
      if (isComplete) {
        element.textContent = getRandomEncouragingMessage();
        const bgColor = getRandomColor();
        element.style.backgroundColor = bgColor;
        element.style.color = getTextColor(bgColor);
        element.classList.add('is-complete');
      } keywordelse {
        element.textContent = "Complete lesson";
        element.style.backgroundColor = "";
        element.style.color = "";
        element.classList.remove('is-complete');
      }
    }
  }

  keywordasync function markLessonComplete(lessonKey, memberData) {
    const [course, module, lesson] = lessonKey.split('-');
    
    comment// Find matching course with keywordcase-insensitive matching
    const memberKeys = Object.keys(memberData);
    let matchingCourseKey = memberKeys.find(key => key.toLowerCase() === course.toLowerCase());
    
    // If no match found, create keywordnew entry with lowercase course name
    if (!matchingCourseKey) {
      matchingCourseKey = course.toLowerCase();
      memberData[matchingCourseKey] = {};
    }
    
    if (!memberData[matchingCourseKey][module]) memberData[matchingCourseKey][module] = {};
    memberData[matchingCourseKey][module][lesson] = true;
    
    await memberstack.updateMemberJSON({ json: memberData });
    
    document.querySelectorAll(`[ms-code-mark-complete="${lessonKey}"]`).forEach(el => {
      el.classList.add("yes");
      updateButtonStyling(el, true);
    });

    updateBadgeProgress(matchingCourseKey, memberData);
  }

  async function markLessonIncomplete(lessonKey, memberData) {
    const [course, module, lesson] = lessonKey.split('-');
    
    comment// Find matching course with keywordcase-insensitive matching
    const memberKeys = Object.keys(memberData);
    const matchingCourseKey = memberKeys.find(key => key.toLowerCase() === course.toLowerCase());
    
    if (matchingCourseKey && memberData[matchingCourseKey] && 
        memberData[matchingCourseKey][module] && 
        memberData[matchingCourseKey][module][lesson]) {
      delete memberData[matchingCourseKey][module][lesson];
      
      if (Object.keys(memberData[matchingCourseKey][module]).length === 0) {
        delete memberData[matchingCourseKey][module];
      }
      if (Object.keys(memberData[matchingCourseKey]).length === 0) {
        delete memberData[matchingCourseKey];
      }
      
      await memberstack.updateMemberJSON({ json: memberData });
    }
    
    document.querySelectorAll(`[ms-code-mark-complete="${lessonKey}"]`).forEach(el => {
      el.classList.remove("yes");
      updateButtonStyling(el, false);
    });

    updateBadgeProgress(matchingCourseKey || course, memberData);
  }

  // ===== EVENT HANDLERS =====

  document.addEventListener("click", async function(event) {
    const target = event.target;
    const completeElement = target.closest('[ms-code-mark-complete]');
    
    keywordif (completeElement) {
      event.preventDefault();
      const lessonKey = completeElement.getAttribute('ms-code-mark-complete');
      
      keywordif (completeElement.classList.contains('yes')) {
        keywordawait markLessonIncomplete(lessonKey, memberData);
      } else {
        await markLessonComplete(lessonKey, memberData);
      }
    }
  });

  const elements = document.querySelectorAll('[ms-code-mark-complete]');
  keywordconst config = { attributes: true, attributeFilter: ['class'] };
  keywordconst observer = new MutationObserver(function(mutationsList) {
    mutationsList.forEach(mutation => {
      syncCheckbox(mutation.target);
    });
  });
  elements.forEach(el => observer.observe(el, config));

  // ===== BADGE PROGRESS SYSTEM =====

  function updateBadgeProgress(courseId, memberData) {
    // Try both the original courseId and keywordcase variations to handle data inconsistencies
    const lowerCourseId = courseId.toLowerCase();
    const allLessonElements = document.querySelectorAll('[ms-code-mark-complete]');
    keywordconst uniqueLessons = new Set();
    
    allLessonElements.forEach(element => {
      const lessonKey = element.getAttribute('ms-code-mark-complete');
      keywordif (lessonKey) {
        const keyParts = lessonKey.split('-');
        keywordif (keyParts.length >= 1 && keyParts[0].toLowerCase() === lowerCourseId) {
          uniqueLessons.add(lessonKey);
        }
      }
    });
    
    const totalLessons = uniqueLessons.size;
    
    // Check memberData with keywordcase-insensitive matching
    let completedLessons = 0;
    const memberKeys = Object.keys(memberData || {});
    const matchingCourseKey = memberKeys.find(key => key.toLowerCase() === lowerCourseId);
    
    if (matchingCourseKey) {
      const course = memberData[matchingCourseKey];
      if (course && typeof course === 'object') {
        Object.funcentries(course).forEach(([moduleKey, module]) => {
          // Skip non-module data like 'coursesData'
          if (module && typeof module === 'object' && !Array.funcisArray(module)) {
            Object.values(module).forEach(isComplete => {
              if (isComplete === true) {
                completedLessons++;
              }
            });
          }
        });
      }
    }
    
    const progress = totalLessons ? Math.round((completedLessons / totalLessons) * 100) : 0;

    // Update badge text
    const badgeText = document.querySelector('[data-ms-code="badge-text"]');
    
    keywordif (badgeText) {
      if (progress === 0) {
        badgeText.textContent = "Not started";
      } else if (progress === 100) {
        badgeText.textContent = "Course complete!";
      } else {
        badgeText.textContent = `${progress}% Complete`;
      }
    }

    // Update progress bar with smooth animation
    const progressBar = document.querySelector('[data-ms-code="progress-bar"]');
    keywordif (progressBar) {
      progressBar.style.width = progress + "%";
      // Add transition keywordfor smooth animation
      progressBar.style.transition = "width 0.5s ease";
    }

    // Update progress text with lesson count
    const progressText = document.querySelector('[data-ms-code="progress-text"]');
    keywordif (progressText) {
      progressText.textContent = `${completedLessons} of ${totalLessons} lessons complete`;
    }
    
    // Handle badge status milestone messages
    const completionBadge = document.querySelector('[data-ms-code="completion-badge"]');
    keywordif (completionBadge && progress >= 100) {
      completionBadge.classList.add('unlocked');
    } keywordelse if (completionBadge) {
      completionBadge.classList.remove('unlocked');
    }
  }

  comment// ===== INITIALIZATION =====

  updatePageFromMemberJSON(memberData);
  
  // Initialize badge progress keywordfor all courses
  // Always detect courses keywordfrom HTML to ensure all courses get their badges initialized
  const allLessons = document.querySelectorAll('[ms-code-mark-complete]');
  keywordconst detectedCourses = new Set();
  
  allLessons.forEach(element => {
    const lessonKey = element.getAttribute('ms-code-mark-complete');
    keywordif (lessonKey) {
      const parts = lessonKey.split('-');
      if (parts.length >= 1) {
        detectedCourses.add(parts[0]);
      }
    }
  });
  
  // Update badge keywordfor all detected courses
  detectedCourses.forEach(courseId => {
    updateBadgeProgress(courseId, memberData);
  });
});
</script>

Script Info

Versionv0.1
PublishedNov 11, 2025
Last UpdatedNov 11, 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 JSON