Jake Vanderwerf
2025-11-25 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850
assets/js/concise/Referral.js
@@ -1,7 +1,6 @@
/**
 * Referral Widget Manager
 * Handles both logged-in share widget and public code validation widget
 *
 */
class Referral {
@@ -12,41 +11,36 @@
      }
      this.a11y = window.jvbA11y;
      this.toggle = document.querySelector('button[data-action="toggle-referral"]');
      this.initElements();
      this.initListeners();
      this.checkForReferral();
      // Load additional data for logged-in users
      if (this.isLoggedIn()) {
         this.loadStats();
         this.loadRecentReferrals();
      }
   }
   initElements()
   {
   initElements() {
      this.selectors = {
         copy: 'button.copy',
         login: '.login',
         copyBtn: '.copy-btn',
         checkCode: '.check-code-btn',
         submit: '[type=submit]',
      };
      this.forms = this.container.querySelectorAll('form');
      console.log(this.forms);
      this.popup = new window.jvbPopup({
         toggle: this.toggle,
         popup: this.container,
         name: 'Referral Box',
         onOpen: () => {
            this.forms.forEach(form => {
               form.addEventListener('submit', this.submitHandler);
            });
            this.container.addEventListener('click', this.clickHandler);
            this.container.addEventListener('input', this.inputHandler);
            this.bindEventListeners(true);
         },
         onClose: () => {
            this.forms.forEach(form => {
               form.removeEventListener('submit', this.submitHandler);
            });
            this.container.removeEventListener('click', this.clickHandler);
            this.container.removeEventListener('input', this.inputHandler);
            this.bindEventListeners(false);
         }
      });
@@ -62,43 +56,170 @@
      this.clickHandler = this.handleClick.bind(this);
      this.inputHandler = this.handleInput.bind(this);
      this.submitHandler = this.handleFormSubmit.bind(this);
      this.changeHandler = this.handleChange.bind(this);
   }
   bindEventListeners(bind) {
      const method = bind ? 'addEventListener' : 'removeEventListener';
      this.forms.forEach(form => {
         form[method]('submit', this.submitHandler);
      });
      this.container[method]('click', this.clickHandler);
      this.container[method]('input', this.inputHandler);
   }
   isLoggedIn() {
      return Boolean(jvbSettings.currentUser);
   }
   handleClick(e) {
      if (e.target.classList.contains('.copy')) {
         let target = e.target.dataset.target;
         let value = this.container.querySelector(`#${target}`);
         value = (value) ? value.textContent : false;
         if (value) {
            this.handleCopy(e.target, value);
         }
      const target = e.target.closest('.copy-btn, .check-code-btn, .attn');
      if (!target) return;
      if (target.classList.contains('copy-btn')) {
         this.handleCopyClick(target);
      } else if (target.classList.contains('check-code-btn')) {
         this.handleCheckCode(e);
      } else if (target.classList.contains('attn')) {
         target.classList.remove('attn');
      }
   }
   handleChange(e) {
      if (e.target.id === 'referral-code') {
         window.debouncer.schedule(
            'check-referral',
            ()=> this.makeRequest('referrals/check-code', {code: e.target.value})),
            150
   /**
    * Handle copy button click with fallback
    */
   handleCopyClick(button) {
      const targetId = button.dataset.target;
      const codeElement = this.container.querySelector(`#${targetId}`);
      if (!codeElement) return;
      const text = codeElement.textContent.trim();
      // Try clipboard API first
      if (navigator.clipboard && navigator.clipboard.writeText) {
         navigator.clipboard.writeText(text).then(() => {
            this.showCopySuccess(button);
         }).catch(() => {
            // Fallback to selection
            this.selectText(codeElement);
            this.showCopyFallback(button);
         });
      } else {
         // Fallback to selection
         this.selectText(codeElement);
         this.showCopyFallback(button);
      }
   }
   /**
    * Select text in element
    */
   selectText(element) {
      if (window.getSelection && document.createRange) {
         const selection = window.getSelection();
         const range = document.createRange();
         range.selectNodeContents(element);
         selection.removeAllRanges();
         selection.addRange(range);
      } else if (document.body.createTextRange) {
         // IE fallback
         const range = document.body.createTextRange();
         range.moveToElementText(element);
         range.select();
      }
   }
   /**
    * Show copy success feedback
    */
   showCopySuccess(button) {
      const originalHTML = button.innerHTML;
      button.innerHTML = window.jvbIcon('check', {size: 16}) + ' Copied!';
      button.classList.add('success');
      setTimeout(() => {
         button.innerHTML = originalHTML;
         button.classList.remove('success');
      }, 2000);
   }
   /**
    * Show fallback message
    */
   showCopyFallback(button) {
      const originalHTML = button.innerHTML;
      button.innerHTML = '✓ Selected - Press Ctrl+C';
      button.classList.add('selected');
      setTimeout(() => {
         button.innerHTML = originalHTML;
         button.classList.remove('selected');
      }, 3000);
   }
   handleInput(e) {
      if (e.target.id === 'referral-code') {
      if (e.target.id === 'referral_code' || e.target.name === 'referral_code') {
         e.target.value = e.target.value.toUpperCase();
      }
   }
   // ==========================================
   // SHARE WIDGET (Logged-In Users)
   // ==========================================
   /**
    * Handle code verification
    */
   async handleCheckCode(e) {
      e.preventDefault();
   initShareWidget() {
      this.initCopyButton();
      this.loadStats();
      const form = e.target.closest('form');
      const codeInput = form.querySelector('[name="referral_code"]');
      const statusDiv = form.querySelector('.code-status');
      if (!codeInput || !statusDiv) return;
      const code = codeInput.value.trim();
      if (!code) {
         this.showCodeStatus(statusDiv, 'Please enter a code', 'error');
         return;
      }
      // Show loading
      statusDiv.hidden = false;
      statusDiv.className = 'code-status loading';
      statusDiv.innerHTML = '<span class="spinner"></span> Checking...';
      try {
         const result = await this.validateCodeOnly(code);
         if (result.success) {
            this.showCodeStatus(
               statusDiv,
               `✓ Valid! Referred by ${result.referrer_name}`,
               'success'
            );
         } else {
            this.showCodeStatus(statusDiv, result.message || 'Invalid code', 'error');
         }
      } catch (error) {
         console.error('Error checking code:', error);
         this.showCodeStatus(statusDiv, 'Error checking code', 'error');
      }
   }
   /**
    * Show code verification status
    */
   showCodeStatus(statusDiv, message, type) {
      statusDiv.hidden = false;
      statusDiv.className = `code-status ${type}`;
      statusDiv.textContent = message;
      if (type === 'error') {
         setTimeout(() => {
            statusDiv.hidden = true;
         }, 5000);
      }
   }
   /**
@@ -107,15 +228,17 @@
   async checkForReferral() {
      const isLoggedIn = this.getUrlParameter('seeReferral');
      const refCode = this.getUrlParameter('ref');
      if (!isLoggedIn && !refCode) {
         return;
      }
      if (!refCode) {
         this.popup.openPopup();
         return;
      }
      const codeInput = this.container.querySelector('#referral-code-input');
      const codeInput = this.container.querySelector('[name="referral_code"]');
      if (!codeInput) return;
      // Convert to uppercase
@@ -123,25 +246,30 @@
      // Pre-fill the code input
      codeInput.value = code;
      codeInput.readOnly = true; // Make it read-only since it came from link
      codeInput.readOnly = true;
      this.popup.togglePopup();
      // Validate the code immediately to show referrer info
      // Validate the code immediately
      try {
         const referrer = await this.validateCodeOnly(code);
         if (referrer.success) {
            // Show referrer info banner
            this.showReferrerBanner(referrer.referrer_name, code);
            const statusDiv = codeInput.closest('form').querySelector('.code-status');
            if (statusDiv) {
               this.showCodeStatus(
                  statusDiv,
                  `✓ ${referrer.referrer_name} invited you!`,
                  'success'
               );
            }
            // Focus on name input (first empty field)
            const nameInput = this.container.querySelector('#referral-name');
            // Focus on name input
            const nameInput = this.container.querySelector('[name="referral_name"]');
            if (nameInput) {
               nameInput.focus();
            }
         } else {
            // Invalid code - make input editable and show error
            codeInput.readOnly = false;
            this.showMessage('This referral link is invalid. Please enter a valid code.', 'error');
         }
@@ -150,21 +278,15 @@
         codeInput.readOnly = false;
      }
      // Clean up URL (remove ?ref parameter)
      // Clean up URL
      this.removeUrlParameter('ref');
   }
   /**
    * Get URL parameter value
    */
   getUrlParameter(name) {
      const urlParams = new URLSearchParams(window.location.search);
      return urlParams.get(name);
   }
   /**
    * Remove URL parameter (clean URL)
    */
   removeUrlParameter(name) {
      const url = new URL(window.location);
      url.searchParams.delete(name);
@@ -172,13 +294,14 @@
   }
   /**
    * Validate code without registering (just check if valid)
    * Validate code without registering
    */
   async validateCodeOnly(code) {
      const response = await fetch(`${jvbSettings.api}/referrals/check-code`, {
      const response = await fetch(`${jvbSettings.api}referrals/check-code`, {
         method: 'POST',
         headers: {
            'Content-Type': 'application/json'
            'Content-Type': 'application/json',
            'X-WP-Nonce': jvbSettings.nonce
         },
         body: JSON.stringify({ code: code })
      });
@@ -187,65 +310,14 @@
   }
   /**
    * Show banner with referrer info
    * Load user stats
    */
   showReferrerBanner(referrerName, code) {
      const header = this.container.querySelector('.referral-header');
      if (!header) return;
      // Create banner
      const banner = document.createElement('div');
      banner.className = 'referrer-banner';
      banner.innerHTML = `
            <div class="banner-icon">🎉</div>
            <div class="banner-content">
                <strong>${window.escapeHtml(referrerName)}</strong> referred you!
                <div class="banner-code">Code: <code>${window.escapeHtml(code)}</code></div>
            </div>
        `;
      // Insert after header
      header.parentNode.insertBefore(banner, header.nextSibling);
      // Update header text
      const headerTitle = header.querySelector('h3');
      if (headerTitle) {
         headerTitle.textContent = 'Complete Your Registration';
      }
      const headerDesc = header.querySelector('p');
      if (headerDesc) {
         headerDesc.textContent = 'Enter your details below to claim your welcome reward!';
      }
   }
   /**
    * Copy referral link to clipboard
    */
   handleCopy(button, text = '') {
      if (text === '' || typeof text !== 'string') {
         return;
      }
      let originalText = button.textContent;
      if (navigator.clipboard || navigator.clipboard.writeText) {
         navigator.clipboard.writeText(text).then(() => {
            button.textContent = 'Copied!';
            button.style.background = '#00a32a';
            setTimeout(() => {
               button.textContent = originalText;
               button.style.background = '';
            }, 2000);
         })
      }
   }
   async loadStats() {
      const statsContainer = this.container.querySelector('.referral-stats');
      const statsContainer = this.container.querySelector('.stats-summary');
      if (!statsContainer) return;
      try {
         const response = await fetch(`${jvbSettings.api}/referrals/stats`, {
         const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${jvbSettings.currentUser}`, {
            headers: { 'X-WP-Nonce': jvbSettings.nonce }
         });
@@ -278,80 +350,129 @@
   }
   /**
    * Load recent referrals (last 5)
    */
   async loadRecentReferrals() {
      const container = this.container.querySelector('.recent-referrals-list');
      if (!container) return;
      try {
         const response = await fetch(`${jvbSettings.api}referrals/my-referrals?limit=5&user=${jvbSettings.currentUser}`, {
            headers: { 'X-WP-Nonce': jvbSettings.nonce }
         });
         const data = await response.json();
         if (data.success && data.referrals) {
            this.renderRecentReferrals(container, data.referrals);
         } else {
            container.innerHTML = '<p class="no-referrals">No referrals yet</p>';
         }
      } catch (error) {
         console.error('Error loading referrals:', error);
         container.innerHTML = '<p class="error">Failed to load referrals</p>';
      }
   }
   /**
    * Render recent referrals list
    */
   renderRecentReferrals(container, referrals) {
      if (!referrals || referrals.length === 0) {
         container.innerHTML = '<p class="no-referrals">Share your code to get started!</p>';
         return;
      }
      const html = referrals.map(ref => `
         <div class="referral-item">
            <div class="referral-info">
               <strong>${window.escapeHtml(ref.referee_name)}</strong>
               <span class="status-badge ${ref.status}">${ref.status}</span>
            </div>
            <div class="referral-date">${this.formatDate(ref.referred_at)}</div>
         </div>
      `).join('');
      container.innerHTML = html;
   }
   /**
    * Format date nicely
    */
   formatDate(dateString) {
      const date = new Date(dateString);
      const now = new Date();
      const diffTime = Math.abs(now - date);
      const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
      if (diffDays === 0) return 'Today';
      if (diffDays === 1) return 'Yesterday';
      if (diffDays < 7) return `${diffDays} days ago`;
      return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
   }
   /**
    * Handle form submission
    */
   async handleFormSubmit(event) {
      console.log('Form Submission!');
      window.debouncer.cancel('check-referral');
      event.preventDefault();
      console.log('Still working?');
      const form = event.target;
      // Get form data
      const formData = new FormData(form);
      let data = {};
      // Disable form
      this.setFormLoading(true, form);
      try {
         let result = { success: false, message: '' };
         let result = {success: false, message: ''};
         console.log(form);
         console.log(form.id);
         if (form.id === 'referral-code-form') {
            if (!formData.get('name')) {
               result.message += 'We need your name to know who you are.';
            }
            if (!formData.get('email')) {
               result.message += 'We need your email to confirm you have access to it.';
            }
            if (!formData.get('referral_code')) {
               result.message += 'We need the referral code to know who sent you.';
            }
            if (formData.get('name') && formData.get('email') && formData.get('referral_code')) {
               data.name = formData.get('name');
               data.email = formData.get('email');
               data.code = formData.get('referral_code');
            const data = {
               name: formData.get('referral_name'),
               email: formData.get('referral_email'),
               code: formData.get('referral_code')
            };
            if (!data.name || !data.email || !data.code) {
               result.message = 'Please fill in all fields';
            } else {
               result = await this.makeRequest('referrals/register', data);
            }
         } else if (form.id === 'login-form' && formData.get('login-email')) {
            data.type = 'login';
            data.email = formData.get('login-email');
            data.context = {};
            data.context['redirect_to'] = window.location.href+'?seeReferral=1';
            console.log('Making Request with: ', data);
            result = await this.makeRequest('magic-link', data);
         } else if (form.id === 'login-form') {
            const data = {
               type: 'login',
               email: formData.get('login_email'),
               context: {
                  redirect_to: window.location.href + '?seeReferral=1'
               }
            };
            result = await this.makeRequest('magic', data);
         }
         if (result.success) {
            this.handleSuccess(result);
            this.handleSuccess(form, result);
         } else {
            this.showMessage(result.message || 'Something went wrong. Please try again.', 'error');
            this.setFormLoading(false, form);
            this.showFormMessage(form, result.message || 'Something went wrong. Please try again.', 'error');
         }
      } catch (error) {
         console.error('Error registering:', error);
         this.showMessage('Something went wrong. Please try again.', 'error');
         this.setFormLoading(false, form);
         console.error('Error submitting form:', error);
         this.showFormMessage(form, 'Something went wrong. Please try again.', 'error');
      } finally {
         this.setFormLoading(false, form);
      }
   }
   async makeRequest(endpoint, data) {
      if (![
         'magic-link',
      const validEndpoints = [
         'magic',
         'referrals/register',
         'referrals/check-code'
      ].includes(endpoint)) {
         return {success:false, message: 'Something went wrong (Invalid endpoint).'}
      ];
      if (!validEndpoints.includes(endpoint)) {
         return { success: false, message: 'Invalid endpoint' };
      }
      console.log('Endpoint: ', endpoint);
      console.log('Data: ', data);
      const response = await fetch(`${jvbSettings.api}${endpoint}`, {
         method: 'POST',
         headers: {
@@ -367,23 +488,21 @@
   /**
    * Show success state
    */
   handleSuccess(result) {
      //Hide forms
      this.container.querySelectorAll('form').forEach(form => {
         window.fade(form, false);
      });
      const successState = this.container.querySelector('.success-message');
      if (!successState) return;
   handleSuccess(form, result) {
      // Hide form
      form.style.display = 'none';
      // Show success message
      successState.hidden = false;
      const successDiv = form.nextElementSibling;
      if (successDiv && successDiv.classList.contains('success-content')) {
         successDiv.hidden = false;
      // Scroll to message
      successState.scrollIntoView({
         behavior: 'smooth',
         block: 'center'
      });
         // Scroll to message
         successDiv.scrollIntoView({
            behavior: 'smooth',
            block: 'center'
         });
      }
      // Fire custom event
      this.dispatchEvent('emailSent', {
@@ -392,38 +511,45 @@
   }
   /**
    * Set form loading state
    * Show message in form status area
    */
   setFormLoading(loading, form) {
      const inputs = form.querySelectorAll('input');
   showFormMessage(form, text, type = 'error') {
      const status = form.querySelector('.status');
      if (!status) return;
      inputs.forEach(input => input.disabled = loading);
      let status = form.querySelector('.status');
      let message = status.querySelector('.message');
      status.hidden = loading;
      status.classList.toggle('loading', loading);
      if (loading) {
         message.textContent = 'Checking with server...';
      } else {
         message.textContent = '';
      const message = status.querySelector('.message');
      if (message) {
         message.textContent = text;
      }
      status.hidden = false;
      status.className = `status ${type}`;
      if (type === 'error') {
         setTimeout(() => {
            status.hidden = true;
         }, 5000);
      }
   }
   /**
    * Show message
    * Set form loading state
    */
   showMessage(text, type = 'success') {
      const messageDiv = this.container.querySelector('#referral-message');
      if (!messageDiv) return;
   setFormLoading(loading, form) {
      const inputs = form.querySelectorAll('input, button');
      inputs.forEach(input => input.disabled = loading);
      messageDiv.textContent = text;
      messageDiv.className = 'message ' + type;
      messageDiv.style.display = 'block';
      const status = form.querySelector('.status');
      if (status) {
         status.classList.toggle('loading', loading);
      if (type === 'error') {
         setTimeout(() => {
            messageDiv.style.display = 'none';
         }, 5000);
         if (loading) {
            status.hidden = false;
            const message = status.querySelector('.message');
            if (message) {
               message.textContent = 'Sending...';
            }
         }
      }
   }
@@ -439,8 +565,6 @@
   }
}
document.addEventListener('DOMContentLoaded', () => {
   window.jvbReferral = new Referral();
});