| | |
| | | /** |
| | | * Referral Widget Manager |
| | | * Handles both logged-in share widget and public code validation widget |
| | | * |
| | | */ |
| | | |
| | | class Referral { |
| | |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | }); |
| | | |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | 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 |
| | |
| | | |
| | | // 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'); |
| | | } |
| | |
| | | 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); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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 }) |
| | | }); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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 } |
| | | }); |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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: { |
| | |
| | | /** |
| | | * 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'); |
| | | 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...'; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | window.jvbReferral = new Referral(); |
| | | }); |
| | | |