/**
|
* 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 = '<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();
|
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 = '<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) {
|
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();
|
});
|