/** * Referral Widget Manager * Handles both logged-in share widget and public code validation widget */ class Referral { constructor() { this.container = document.querySelector('.jvb-referral'); if (!this.container) { return; } 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() { this.selectors = { copyBtn: '.copy-btn', checkCode: '.check-code-btn', submit: '[type=submit]', }; this.forms = this.container.querySelectorAll('form'); this.popup = new window.jvbPopup({ toggle: this.toggle, popup: this.container, name: 'Referral Box', onOpen: () => { this.bindEventListeners(true); }, onClose: () => { this.bindEventListeners(false); } }); this.tabs = null; if (this.container.querySelector('nav.tabs')) { this.tabs = new window.jvbTabs(this.container, {updateURL: false}); } this.ui = window.uiFromSelectors(this.selectors, this.container); } initListeners() { this.clickHandler = this.handleClick.bind(this); this.inputHandler = this.handleInput.bind(this); this.submitHandler = this.handleFormSubmit.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) { 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 (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' || e.target.name === 'referral_code') { e.target.value = e.target.value.toUpperCase(); } } /** * Handle code verification */ async handleCheckCode(e) { e.preventDefault(); 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 = ' 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(); return; } const codeInput = this.container.querySelector('[name="referral_code"]'); if (!codeInput) return; // Convert to uppercase const code = refCode.toUpperCase(); // Pre-fill the code input codeInput.value = code; codeInput.readOnly = true; this.popup.togglePopup(); // Validate the code immediately try { const referrer = await this.validateCodeOnly(code); if (referrer.success) { const statusDiv = codeInput.closest('form').querySelector('.code-status'); if (statusDiv) { this.showCodeStatus( statusDiv, `✓ ${referrer.referrer_name} invited you!`, 'success' ); } // Focus on name input const nameInput = this.container.querySelector('[name="referral_name"]'); if (nameInput) { nameInput.focus(); } } else { codeInput.readOnly = false; this.showMessage('This referral link is invalid. Please enter a valid code.', 'error'); } } catch (error) { console.error('Error validating code:', error); codeInput.readOnly = false; } // Clean up URL this.removeUrlParameter('ref'); } getUrlParameter(name) { const urlParams = new URLSearchParams(window.location.search); return urlParams.get(name); } removeUrlParameter(name) { const url = new URL(window.location); url.searchParams.delete(name); window.history.replaceState({}, document.title, url.toString()); } /** * Validate code without registering */ async validateCodeOnly(code) { const response = await fetch(`${jvbSettings.api}referrals/check-code`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': jvbSettings.nonce }, body: JSON.stringify({ code: code }) }); return await response.json(); } /** * 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=${jvbSettings.currentUser}`, { 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); } } /** * 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 = '

No referrals yet

'; } } catch (error) { console.error('Error loading referrals:', error); container.innerHTML = '

Failed to load referrals

'; } } /** * Render recent referrals list */ renderRecentReferrals(container, referrals) { if (!referrals || referrals.length === 0) { container.innerHTML = '

Share your code to get started!

'; return; } const html = referrals.map(ref => `
${window.escapeHtml(ref.referee_name)} ${ref.status}
${this.formatDate(ref.referred_at)}
`).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) { event.preventDefault(); const form = event.target; const formData = new FormData(form); // Disable form this.setFormLoading(true, form); try { let result = { success: false, message: '' }; if (form.id === 'referral-code-form') { 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') { 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(form, result); } else { this.showFormMessage(form, result.message || 'Something went wrong. Please try again.', 'error'); } } catch (error) { 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) { const validEndpoints = [ 'magic', 'referrals/register', 'referrals/check-code' ]; if (!validEndpoints.includes(endpoint)) { return { success: false, message: 'Invalid endpoint' }; } const response = await fetch(`${jvbSettings.api}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': jvbSettings.nonce, }, body: JSON.stringify(data) }); return await response.json(); } /** * Show success state */ handleSuccess(form, result) { // Hide form form.style.display = 'none'; // Show success message const successDiv = form.nextElementSibling; if (successDiv && successDiv.classList.contains('success-content')) { successDiv.hidden = false; // Scroll to message successDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); } // Fire custom event this.dispatchEvent('emailSent', { email: result.email }); } /** * Show message in form status area */ showFormMessage(form, text, type = 'error') { const status = form.querySelector('.status'); if (!status) return; 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); } } /** * Set form loading state */ setFormLoading(loading, form) { const inputs = form.querySelectorAll('input, button'); 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...'; } } } } /** * Dispatch custom event */ dispatchEvent(eventName, detail) { const event = new CustomEvent('referralWidget:' + eventName, { detail: detail, bubbles: true }); this.container.dispatchEvent(event); } } document.addEventListener('DOMContentLoaded', () => { window.jvbReferral = new Referral(); });