| | |
| | | /** |
| | | * Referral Widget Manager |
| | | * Handles both logged-in share widget and public code validation widget |
| | | * |
| | | */ |
| | | |
| | | class Referral { |
| | | constructor() { |
| | | this.container = document.querySelector('.jvb-referral'); |
| | | this.container = document.querySelector('aside.referral'); |
| | | if (!this.container) { |
| | | return; |
| | | } |
| | | |
| | | this.a11y = window.jvbA11y; |
| | | |
| | | this.toggle = document.querySelector('button[data-action="toggle-referral"]'); |
| | | |
| | | this.hasCopy = navigator.clipboard && navigator.clipboard.writeText; |
| | | this.initElements(); |
| | | this.initStore(); |
| | | this.initListeners(); |
| | | this.checkForReferral(); |
| | | } |
| | | |
| | | initElements() |
| | | { |
| | | initElements() { |
| | | this.selectors = { |
| | | copy: 'button.copy', |
| | | login: '.login', |
| | | copyBtn: '.copy-btn', |
| | | checkCode: '.check-code-btn', |
| | | submit: '[type=submit]', |
| | | recentList: '.recent-referrals-list', |
| | | stats: { |
| | | codeUsed: '[data-stat="code_used"]', |
| | | consultations: '[data-stat="consultations"]', |
| | | treatments: '[data-stat="treatments"]', |
| | | rewards: '[data-stat="total_rewards"]' |
| | | }, |
| | | }; |
| | | |
| | | this.forms = this.container.querySelectorAll('form'); |
| | | console.log(this.forms); |
| | | this.popup = new window.jvbPopup({ |
| | | this.popup = window.jvbPopup.registerPopup({ |
| | | 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); |
| | | } |
| | | }); |
| | | |
| | | this.tabs = null; |
| | | |
| | | if (this.container.querySelector('nav.tabs')) { |
| | | this.tabs = new window.jvbTabs(this.container, {updateURL: false}); |
| | | this.tabs = window.jvbTabs.registerTab(this.container, {updateURL: false}); |
| | | } |
| | | |
| | | this.ui = window.uiFromSelectors(this.selectors, this.container); |
| | | |
| | | this.ui = window.uiFromSelectors(this.selectors); |
| | | |
| | | |
| | | |
| | | if (!this.hasCopy) { |
| | | document.querySelectorAll(this.selectors.copyBtn).forEach(btn => { |
| | | btn.remove(); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | initStore() { |
| | | if (!this.isLoggedIn()) return; |
| | | |
| | | const stores = window.jvbStore.register( |
| | | 'referrals', |
| | | [ |
| | | // Dashboard stats store |
| | | { |
| | | storeName: 'stats', |
| | | keyPath: 'user_id', |
| | | endpoint: 'referrals/stats', |
| | | TTL: 5 * 60 * 1000, |
| | | showLoading: false, |
| | | delayFetch: true, |
| | | filters: { |
| | | type: 'dashboard', |
| | | user: window.auth.getUser() |
| | | } |
| | | }, |
| | | // Referrals list store |
| | | { |
| | | storeName: 'list', |
| | | keyPath: 'id', |
| | | endpoint: 'referrals', |
| | | TTL: 10 * 60 * 1000, |
| | | showLoading: false, |
| | | delayFetch: true, |
| | | filters: { |
| | | user: window.auth.getUser(), |
| | | status: 'all', |
| | | limit: 50, |
| | | offset: 0 |
| | | } |
| | | } |
| | | ] |
| | | ); |
| | | |
| | | this.statsStore = stores.stats; |
| | | this.listStore = stores.list; |
| | | |
| | | // Subscribe to store events |
| | | if (this.statsStore) { |
| | | this.statsStore.subscribe(this.handleStatsEvent.bind(this)); |
| | | } |
| | | if (this.listStore) { |
| | | this.listStore.subscribe(this.handleListEvent.bind(this)); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | initListeners() { |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | this.inputHandler = this.handleInput.bind(this); |
| | | this.submitHandler = this.handleFormSubmit.bind(this); |
| | | this.changeHandler = this.handleChange.bind(this); |
| | | |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | 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(window.auth.getUser()); |
| | | } |
| | | |
| | | /** |
| | | * Handle DataStore stats events |
| | | */ |
| | | handleStatsEvent(event, data) { |
| | | switch(event) { |
| | | case 'data-loaded': |
| | | if (data.items && data.items.length > 0) { |
| | | this.updateStatsDisplay(); |
| | | } |
| | | break; |
| | | case 'fetch-error': |
| | | console.error('Error loading stats:', data.error); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | handleChange(e) { |
| | | if (e.target.id === 'referral-code') { |
| | | window.debouncer.schedule( |
| | | 'check-referral', |
| | | ()=> this.makeRequest('referrals/check-code', {code: e.target.value})), |
| | | 150 |
| | | /** |
| | | * Handle DataStore list events |
| | | */ |
| | | handleListEvent(event, data) { |
| | | switch(event) { |
| | | case 'data-loaded': |
| | | // Let ViewController handle main list rendering |
| | | // Only update sidebar preview if it exists |
| | | if (this.ui.recentList) { |
| | | this.renderRecentReferrals(); |
| | | } |
| | | break; |
| | | case 'fetch-error': |
| | | console.error('Error loading referrals:', data.error); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update stats display |
| | | */ |
| | | updateStatsDisplay() { |
| | | if (!this.statsStore.data.size === 0) return; |
| | | let stats = this.statsStore.data.get(parseInt(window.auth.getUser())); |
| | | const updates = { |
| | | total: stats['code_used'] || 0, |
| | | treated: stats.treatments || 0, |
| | | pending: stats.pending || 0, |
| | | rewards: '$' + parseFloat(stats['total_rewards'] || 0).toFixed(2) |
| | | }; |
| | | |
| | | Object.entries(updates).forEach(([key, value]) => { |
| | | const element = this.container.querySelector(`[data-stat="${key}"]`); |
| | | if (element) { |
| | | element.textContent = value; |
| | | } |
| | | }); |
| | | |
| | | // Also update stat cards if on dashboard |
| | | const statCards = this.container.querySelectorAll('.stats .card'); |
| | | if (statCards.length >= 4) { |
| | | statCards[0].querySelector('.stat-number').textContent = updates.code_used; |
| | | statCards[1].querySelector('.stat-number').textContent = updates.consultations; |
| | | statCards[2].querySelector('.stat-number').textContent = updates.treatments; |
| | | statCards[3].querySelector('.stat-number').textContent = updates.total_rewards; |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | handleClick(e) { |
| | | 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'); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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 (this.hasCopy) { |
| | | navigator.clipboard.writeText(text).then(() => { |
| | | button.classList.toggle('success'); |
| | | setTimeout(() => { |
| | | button.classList.remove('success'); |
| | | }, 1500); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Handle error response with field-specific feedback |
| | | */ |
| | | 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 error for specific field |
| | | */ |
| | | 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}"]`); |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Clear all form errors |
| | | */ |
| | | clearFormErrors(form) { |
| | | // Clear field-level errors |
| | | form.querySelectorAll('.field.has-error, .field.has-success').forEach(fieldWrapper => { |
| | | this.clearFieldValidation(fieldWrapper); |
| | | }); |
| | | |
| | | // 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') { |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | // ========================================== |
| | | // 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); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check for ?ref parameter in URL and pre-fill code |
| | | */ |
| | | async checkForReferral() { |
| | | const isLoggedIn = this.getUrlParameter('seeReferral'); |
| | | const refCode = this.getUrlParameter('ref'); |
| | | if (!isLoggedIn && !refCode) { |
| | | return; |
| | | } |
| | | if (!refCode) { |
| | | this.popup.openPopup(); |
| | | const refName = this.getUrlParameter('rname'); |
| | | const refEmail = this.getUrlParameter('remail'); |
| | | const seeReferral = this.getUrlParameter('seeReferral'); |
| | | |
| | | if (!refCode && !seeReferral) { |
| | | return; |
| | | } |
| | | |
| | | const codeInput = this.container.querySelector('#referral-code-input'); |
| | | // If logged in user just wants to see referral popup |
| | | if (seeReferral && !refCode) { |
| | | this.popup.openPopup(); |
| | | this.removeUrlParameter('seeReferral'); |
| | | return; |
| | | } |
| | | |
| | | const codeInput = this.container.querySelector('[name="referral_code"]'); |
| | | if (!codeInput) return; |
| | | |
| | | // Convert to uppercase |
| | |
| | | |
| | | // 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(); |
| | | // If we have token data, prefill name and email too |
| | | if (refName || refEmail) { |
| | | const nameInput = this.container.querySelector('[name="referral_name"]'); |
| | | if (nameInput) { |
| | | nameInput.value = refName; |
| | | } |
| | | |
| | | // Validate the code immediately to show referrer info |
| | | const emailInput = this.container.querySelector('[name="referral_email"]'); |
| | | if (emailInput) { |
| | | emailInput.value = refEmail; |
| | | } |
| | | } |
| | | |
| | | // Open the sidebar popup |
| | | this.popup.openPopup(); |
| | | |
| | | // 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'); |
| | | if (nameInput) { |
| | | // Focus on name input if not prefilled |
| | | const nameInput = this.container.querySelector('[name="referral_name"]'); |
| | | if (nameInput && !nameInput.value) { |
| | | 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'); |
| | | } |
| | |
| | | codeInput.readOnly = false; |
| | | } |
| | | |
| | | // Clean up URL (remove ?ref parameter) |
| | | // Clean up URL |
| | | this.removeUrlParameter('ref'); |
| | | this.removeUrlParameter('rname'); |
| | | this.removeUrlParameter('remail'); |
| | | } |
| | | |
| | | /** |
| | | * 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); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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/code`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json' |
| | | 'Content-Type': 'application/json', |
| | | 'X-WP-Nonce': window.auth.getNonce() |
| | | }, |
| | | body: JSON.stringify({ code: code }) |
| | | }); |
| | |
| | | } |
| | | |
| | | /** |
| | | * Show banner with referrer info |
| | | * Render recent referrals list |
| | | */ |
| | | 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') { |
| | | renderRecentReferrals() { |
| | | let container = this.ui.recentList; |
| | | let referrals = Array.from(this.listStore.data.values()); |
| | | if (!referrals || referrals.length === 0) { |
| | | container.innerHTML = '<p class="no-referrals">Share your code to get started!</p>'; |
| | | 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'); |
| | | if (!statsContainer) return; |
| | | |
| | | try { |
| | | const response = await fetch(`${jvbSettings.api}/referrals/stats`, { |
| | | headers: { 'X-WP-Nonce': jvbSettings.nonce } |
| | | }); |
| | | |
| | | const data = await response.json(); |
| | | if (data.success && data.stats) { |
| | | this.updateStats(data.stats); |
| | | } |
| | | } catch (error) { |
| | | console.error('Error loading 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); |
| | | } |
| | | container.innerHTML = referrals.map(ref => ` |
| | | <div class="referral-item"> |
| | | <div class="referral-info"> |
| | | <strong>${window.escapeHtml(ref.referee_name)}</strong> |
| | | <span class="status-badge">${ref.referral_status}</span> |
| | | </div> |
| | | <div class="referral-date">${window.formatTimeAgo(ref.referred_at)}</div> |
| | | </div> |
| | | `).join(''); |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | // Clear any existing errors |
| | | this.clearFormErrors(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'); |
| | | 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); |
| | | } |
| | | // Registration with referral code - goes to LoginRoutes |
| | | let data = { |
| | | name: formData.get('referral_name'), |
| | | email: formData.get('referral_email'), |
| | | referral_code: formData.get('referral_code') |
| | | }; |
| | | |
| | | const turnstileInput = form.querySelector('input[name="cf-turnstile-response"]'); |
| | | if (turnstileInput && turnstileInput.value) { |
| | | data['cf-turnstile-response'] = turnstileInput.value; |
| | | } |
| | | |
| | | 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') { |
| | | let data = { |
| | | type: 'login', |
| | | user_email: formData.get('login_email'), |
| | | context: { |
| | | redirect_to: window.location.href + '?seeReferral=1' |
| | | } |
| | | }; |
| | | const turnstileInput = form.querySelector('input[name="cf-turnstile-response"]'); |
| | | if (turnstileInput && turnstileInput.value) { |
| | | data['cf-turnstile-response'] = turnstileInput.value; |
| | | } |
| | | if (!data['user_email']) { |
| | | result.message = 'Please fill in your email'; |
| | | } else { |
| | | result = await this.makeRequest('auth/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.handleError(form, result); |
| | | } |
| | | } 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', |
| | | 'referrals/register', |
| | | 'referrals/check-code' |
| | | ].includes(endpoint)) { |
| | | return {success:false, message: 'Something went wrong (Invalid endpoint).'} |
| | | const validEndpoints = [ |
| | | 'auth/magic', |
| | | 'auth/register' |
| | | ]; |
| | | |
| | | 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}`, { |
| | | |
| | | const response = await window.auth.fetch(`${jvbSettings.api}${endpoint}`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'X-WP-Nonce': jvbSettings.nonce, |
| | | }, |
| | | body: JSON.stringify(data) |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | const errorText = await response.text(); |
| | | console.error('Error response:', response.status, errorText); |
| | | try { |
| | | return JSON.parse(errorText); |
| | | } catch { |
| | | return { success: false, message: 'Server error' }; |
| | | } |
| | | } |
| | | |
| | | return await response.json(); |
| | | } |
| | | |
| | | /** |
| | | * 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', { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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, textarea, select'); |
| | | inputs.forEach(input => input.disabled = loading); |
| | | |
| | | messageDiv.textContent = text; |
| | | messageDiv.className = 'message ' + type; |
| | | messageDiv.style.display = 'block'; |
| | | |
| | | if (type === 'error') { |
| | | setTimeout(() => { |
| | | messageDiv.style.display = 'none'; |
| | | }, 5000); |
| | | if (loading) { |
| | | this.showFormStatus(form, 'saving'); |
| | | } |
| | | } |
| | | |
| | |
| | | }); |
| | | this.container.dispatchEvent(event); |
| | | } |
| | | |
| | | |
| | | } |
| | | |
| | | |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | window.jvbReferral = new Referral(); |
| | | document.addEventListener('DOMContentLoaded', async function () { |
| | | window.auth.subscribe((event) => { |
| | | if (event === 'auth-loaded') { |
| | | window.jvbReferral = new Referral(); |
| | | } |
| | | }); |
| | | }); |
| | | |