From 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Nov 2025 07:42:23 +0000
Subject: [PATCH] =Feed block mostly good! Referrals look good to go. Ready for Madi and Heidi to approve
---
assets/js/concise/Referral.js | 508 +++++++++++++++++++++++++++++++++++---------------------
1 files changed, 316 insertions(+), 192 deletions(-)
diff --git a/assets/js/concise/Referral.js b/assets/js/concise/Referral.js
index 6f2442d..9ec7702 100644
--- a/assets/js/concise/Referral.js
+++ b/assets/js/concise/Referral.js
@@ -1,7 +1,6 @@
/**
* Referral Widget Manager
* Handles both logged-in share widget and public code validation widget
- *
*/
class Referral {
@@ -12,41 +11,36 @@
}
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);
}
});
@@ -62,43 +56,170 @@
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);
+ }
}
/**
@@ -107,15 +228,17 @@
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
@@ -123,25 +246,30 @@
// 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');
}
@@ -150,21 +278,15 @@
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);
@@ -172,13 +294,14 @@
}
/**
- * 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 })
});
@@ -187,65 +310,14 @@
}
/**
- * 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 }
});
@@ -278,80 +350,129 @@
}
/**
+ * 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: {
@@ -367,23 +488,21 @@
/**
* 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', {
@@ -392,38 +511,45 @@
}
/**
- * 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...';
+ }
+ }
}
}
@@ -439,8 +565,6 @@
}
}
-
document.addEventListener('DOMContentLoaded', () => {
window.jvbReferral = new Referral();
});
-
--
Gitblit v1.10.0