Jake Vanderwerf
2025-12-23 25be5747a6e462a3d09fc6607b3639b79e4d9374
assets/js/concise/Referral.js
@@ -15,7 +15,6 @@
      this.hasCopy = navigator.clipboard && navigator.clipboard.writeText;
      this.initElements();
      this.storesInited = false;
      this.initStore();
      this.initListeners();
      this.checkForReferral();
@@ -27,16 +26,12 @@
         checkCode: '.check-code-btn',
         submit: '[type=submit]',
         recentList: '.recent-referrals-list',
         invite: 'form.invite',
         adminList: '.items-list.referral',
         dash: '.replace .referral-dashboard',
         stats: {
            codeUsed: '[data-stat="code_used"]',
            consultations: '[data-stat="consultations"]',
            treatments: '[data-stat="treatments"]',
            rewards: '[data-stat="total_rewards"]'
         },
         list: '.referrals-list'
      };
      this.forms = this.container.querySelectorAll('form');
@@ -61,10 +56,7 @@
      this.ui = window.uiFromSelectors(this.selectors);
      this.dashTabs = null;
      if (this.ui.dash) {
         this.dashTabs = new window.jvbTabs(this.ui.dash);
      }
      if (!this.hasCopy) {
         document.querySelectorAll(this.selectors.copyBtn).forEach(btn => {
@@ -72,32 +64,6 @@
         });
      }
      this.formController = null;
      if (this.ui.invite) {
         this.formController = new window.jvbForm();
         this.formController.registerForm(
            this.ui.invite,
            {
               autosave: true,
               endpoint: 'referrals',
               formStatus: false,
            }
         );
         this.formController.subscribe((event, data) => {
            if (event === 'form-submit') {
               data = data.fullData;
               data.action = 'invite';
               window.jvbQueue.addToQueue(
                  {
                     endpoint: 'referrals',
                     data: data,
                     title: 'Submitting invitations',
                  }
               );
            }
         });
      }
   }
   initStore() {
@@ -147,27 +113,9 @@
      if (this.listStore) {
         this.listStore.subscribe(this.handleListEvent.bind(this));
      }
      if (this.ui.dash) {
         this.initViewController();
      }
   }
   initViewController() {
      if (!this.listStore || !this.ui.adminList) return;
      this.view = new window.jvbViews(this.ui.adminList, this.listStore);
      this.view.subscribe((event, data) => {
         switch(event) {
            case 'item-action':
               this.handleItemAction(data);
               break;
            case 'bulk-action':
               this.handleBulkAction(data);
               break;
         }
      });
   }
   initListeners() {
      this.clickHandler = this.handleClick.bind(this);
@@ -254,82 +202,7 @@
      }
   }
   /**
    * Handle item actions (remove, resend)
    */
   handleItemAction(data) {
      const { action, itemId } = data;
      switch(action) {
         case 'remove':
            this.removeReferral(itemId);
            break;
         case 'resend':
            this.resendInvite(itemId);
            break;
      }
   }
   /**
    * Remove referral from list
    */
   async removeReferral(id) {
      if (!confirm('Remove this referral from your list?')) return;
      try {
         const response = await fetch(`${jvbSettings.api}referrals`, {
            method: 'POST',
            headers: {
               'Content-Type': 'application/json',
               'X-WP-Nonce': window.auth.getNonce()
            },
            body: JSON.stringify({
               action: 'remove',
               referral_id: id
            })
         });
         const result = await response.json();
         if (result.success) {
            // Refresh DataStore
            if (this.listStore) this.listStore.fetch();
            if (this.statsStore) this.statsStore.fetch();
            this.a11y?.announce('Referral removed');
         }
      } catch (error) {
         console.error('Error removing referral:', error);
      }
   }
   /**
    * Resend invite email
    */
   async resendInvite(id) {
      try {
         const response = await fetch(`${jvbSettings.api}referrals`, {
            method: 'POST',
            headers: {
               'Content-Type': 'application/json',
               'X-WP-Nonce': window.auth.getNonce()
            },
            body: JSON.stringify({
               action: 'resend',
               referral_id: id
            })
         });
         const result = await response.json();
         if (result.success) {
            this.a11y?.announce('Invitation resent');
         } else {
            alert(result.message || 'Cannot resend yet. Wait 7 days between invites.');
         }
      } catch (error) {
         console.error('Error resending invite:', error);
      }
   }
   handleClick(e) {
@@ -359,65 +232,203 @@
      // Try clipboard API first
      if (this.hasCopy) {
         navigator.clipboard.writeText(text).then(() => {
            this.showCopySuccess(button);
         }).catch(() => {
            // Fallback to selection
            this.selectText(codeElement);
            this.showCopyFallback(button);
            button.classList.toggle('success');
            setTimeout(() => {
               button.classList.remove('success');
            }, 1500);
         });
      }
   }
   /**
    * Select text in element
    * Handle error response with field-specific feedback
    */
   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();
   handleError(form, result) {
      const { message, code, field } = result;
      // If there's a specific field, highlight it
      if (field) {
         this.showFieldError(form, field, message);
      } else {
         // Show general form error using FormController pattern
         this.showFormStatus(form, 'error', message || 'Something went wrong. Please try again.');
      }
      // Handle specific error codes
      switch(code) {
         case 'duplicate_email':
            // Could add additional UI feedback
            break;
         case 'invalid_code':
            // Unlock the referral code field so user can correct it
            const codeInput = form.querySelector('[name="referral_code"]');
            if (codeInput) {
               codeInput.readOnly = false;
               codeInput.focus();
            }
            break;
         case 'turnstile_failed':
            // Refresh Turnstile widget if available
            if (window.turnstile && form.querySelector('.cf-turnstile')) {
               window.turnstile.reset();
            }
            break;
      }
   }
   /**
    * Show copy success feedback
    * Show error for specific field
    */
   showCopySuccess(button) {
      const originalHTML = button.innerHTML;
      button.innerHTML = window.jvbIcon('check', {size: 16}) + ' Copied!';
      button.classList.add('success');
   showFieldError(form, fieldName, message) {
      // Find the field wrapper (handles both direct names and referral_ prefixed names)
      let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"]`);
      if (!fieldWrapper) {
         fieldWrapper = form.querySelector(`.field[data-field="referral_${fieldName}"]`);
      }
      setTimeout(() => {
         button.innerHTML = originalHTML;
         button.classList.remove('success');
      }, 2000);
      if (!fieldWrapper) {
         // If no field wrapper found, show as general form error
         this.showFormStatus(form, 'error', message);
         return;
      }
      const input = fieldWrapper.querySelector('input, textarea, select');
      const validationMessage = fieldWrapper.querySelector('.validation-message');
      const errorIcon = fieldWrapper.querySelector('.validation-icon.error');
      const successIcon = fieldWrapper.querySelector('.validation-icon.success');
      if (!input) {
         this.showFormStatus(form, 'error', message);
         return;
      }
      // Apply error state (following FormController pattern)
      fieldWrapper.classList.remove('has-success');
      fieldWrapper.classList.add('has-error');
      input.classList.add('error');
      input.setAttribute('aria-invalid', 'true');
      // Show error icon, hide success icon
      if (errorIcon) errorIcon.hidden = false;
      if (successIcon) successIcon.hidden = true;
      // Show error message
      if (validationMessage) {
         validationMessage.textContent = message;
         validationMessage.hidden = false;
      }
      // Focus the problematic field
      input.focus();
      // Announce to screen readers
      this.a11y?.announce(`Error in ${fieldName}: ${message}`);
   }
   showFormStatus(form, status, message = '') {
      const statusWrap = form.querySelector('.fstatus');
      if (!statusWrap) {
         console.warn('No .fstatus element found in form');
         return;
      }
      statusWrap.hidden = false;
      const statusElement = statusWrap.querySelector('.message');
      // Clear previous state
      statusWrap.querySelector('.icon')?.remove();
      statusWrap.querySelector('.actions')?.remove();
      // Status messages
      const messages = {
         'saving': 'Sending...',
         'submitted': 'Sent successfully!',
         'error': 'Something went wrong',
         'checking': 'Checking code...'
      };
      // Status icons (using window.getIcon like FormController)
      const icons = {
         'submitted': 'check-circle',
         'error': 'close-circle',
         'checking': 'loading'
      };
      // Add icon if available
      if (icons[status] && window.getIcon) {
         const icon = window.getIcon(icons[status]);
         if (icon) {
            statusWrap.prepend(icon);
         }
      }
      // Set message
      if (statusElement) {
         statusElement.textContent = message || messages[status] || status;
      }
      // Add loading class for pending states
      statusWrap.classList.toggle('loading', ['saving', 'checking'].includes(status));
      // Auto-hide success messages
      if (status === 'submitted') {
         setTimeout(() => statusWrap.hidden = true, 3000);
      }
      // Announce to screen readers
      if (this.a11y) {
         this.a11y.announce(message || messages[status] || status);
      }
   }
   /**
    * Show fallback message
    * Clear all form errors
    */
   showCopyFallback(button) {
      const originalHTML = button.innerHTML;
      button.innerHTML = '✓ Selected - Press Ctrl+C';
      button.classList.add('selected');
   clearFormErrors(form) {
      // Clear field-level errors
      form.querySelectorAll('.field.has-error, .field.has-success').forEach(fieldWrapper => {
         this.clearFieldValidation(fieldWrapper);
      });
      setTimeout(() => {
         button.innerHTML = originalHTML;
         button.classList.remove('selected');
      }, 3000);
      // Hide form status
      const statusWrap = form.querySelector('.fstatus');
      if (statusWrap) {
         statusWrap.hidden = true;
      }
   }
   clearFieldValidation(fieldWrapper) {
      if (!fieldWrapper) return;
      const input = fieldWrapper.querySelector('input, textarea, select');
      const validationMessage = fieldWrapper.querySelector('.validation-message');
      const validationIcons = fieldWrapper.querySelectorAll('.validation-icon');
      // Remove classes
      fieldWrapper.classList.remove('has-error', 'has-success');
      if (input) {
         input.classList.remove('error');
         input.removeAttribute('aria-invalid');
      }
      // Hide icons and messages
      validationIcons.forEach(icon => icon.hidden = true);
      if (validationMessage) {
         validationMessage.hidden = true;
         validationMessage.textContent = '';
      }
   }
   handleInput(e) {
      if (e.target.id === 'referral_code' || e.target.name === 'referral_code') {
         e.target.value = e.target.value.toUpperCase();
      }
      // Clear field error when user types
      const fieldWrapper = e.target.closest('.field');
      if (fieldWrapper && fieldWrapper.classList.contains('has-error')) {
         this.clearFieldValidation(fieldWrapper);
      }
   }
   /**
@@ -585,63 +596,6 @@
   }
   /**
    * Load user stats
    */
   async loadStats() {
      const statsContainer = this.container.querySelector('.stats-summary');
      if (!statsContainer) return;
      try {
         const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${window.auth.getUser()}`, {
            headers: { 'X-WP-Nonce': window.auth.getNonce() }
         });
         const data = await response.json();
         if (data.success && data.stats) {
            this.updateStats(data.stats);
         }
      } catch (error) {
         console.error('Error loading stats:', error);
      }
   }
   async loadSidebarStats() {
      try {
         const response = await fetch(
            `${jvbSettings.api}referrals/stats?user=${window.auth.getUser()}&type=quick`,
            { headers: { 'X-WP-Nonce': window.auth.getNonce() } }
         );
         const data = await response.json();
         if (data.success && data.stats) {
            this.updateSidebarStats(data.stats);
         }
      } catch (error) {
         console.error('Error loading sidebar stats:', error);
      }
   }
   /**
    * Update stats display
    */
   updateStats(stats) {
      const elements = {
         total: this.container.querySelector('[data-stat="total"]'),
         treated: this.container.querySelector('[data-stat="treated"]'),
         pending: this.container.querySelector('[data-stat="pending"]'),
         rewards: this.container.querySelector('[data-stat="rewards"]')
      };
      if (elements.total) elements.total.textContent = stats.total_referrals || 0;
      if (elements.treated) elements.treated.textContent = stats.treated_count || 0;
      if (elements.pending) elements.pending.textContent = stats.pending_count || 0;
      if (elements.rewards) {
         elements.rewards.textContent = '$' + parseFloat(stats.available_rewards || 0).toFixed(2);
      }
   }
   /**
    * Render recent referrals list
    */
   renderRecentReferrals() {
@@ -664,22 +618,6 @@
   }
   /**
    * 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) {
@@ -688,6 +626,8 @@
      const form = event.target;
      const formData = new FormData(form);
      // Clear any existing errors
      this.clearFormErrors(form);
      this.setFormLoading(true, form);
      try {
@@ -695,32 +635,43 @@
         if (form.id === 'referral-code-form') {
            // Registration with referral code - goes to LoginRoutes
            const data = {
            let data = {
               name: formData.get('referral_name'),
               email: formData.get('referral_email'),
               referral_code: formData.get('referral_code')
            };
            if (formData.get('cf-turnstile-response')) {
               data['cf-turnstile-response'] = formData.get('cf-turnstile-response');
            }
            if (!data.name || !data.email || !data.referral_code) {
               result.message = 'Please fill in all fields';
            } else {
               result = await this.makeRequest('auth/register', data); // UPDATED endpoint
            }
         } else if (form.id === 'login-form') {
            const data = {
            let data = {
               type: 'login',
               email: formData.get('login_email'),
               context: {
                  redirect_to: window.location.href + '?seeReferral=1'
               }
            };
            result = await this.makeRequest('magic', data);
            if (formData.get('cf-turnstile-response')) {
               data['cf-turnstile-response'] = formData.get('cf-turnstile-response');
            }
            if (!data.email) {
               result.message = 'Please fill in your email';
            } else {
               result = await this.makeRequest('magic', data);
            }
         }
         if (result.success) {
            this.handleSuccess(form, result);
         } else {
            this.showFormMessage(form, result.message || 'Something went wrong. Please try again.', 'error');
            this.handleError(form, result);
         }
      } catch (error) {
         console.error('Error submitting form:', error);
@@ -814,20 +765,11 @@
    * Set form loading state
    */
   setFormLoading(loading, form) {
      const inputs = form.querySelectorAll('input, button');
      const inputs = form.querySelectorAll('input, button, textarea, select');
      inputs.forEach(input => input.disabled = loading);
      const status = form.querySelector('.status');
      if (status) {
         status.classList.toggle('loading', loading);
         if (loading) {
            status.hidden = false;
            const message = status.querySelector('.message');
            if (message) {
               message.textContent = 'Sending...';
            }
         }
      if (loading) {
         this.showFormStatus(form, 'saving');
      }
   }
@@ -841,6 +783,8 @@
      });
      this.container.dispatchEvent(event);
   }
}
document.addEventListener('DOMContentLoaded', async function () {