From 3aada9949d51024a92a8b5c6cb70d12f9c3cac16 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 21 Dec 2025 19:59:48 +0000
Subject: [PATCH] =auth refactored via rest, referral system set up for Jane, some javascript consolidation
---
assets/js/concise/Referral.js | 408 +++++++++++++++++++++++++++++++++++++++++++++++++---------
1 files changed, 345 insertions(+), 63 deletions(-)
diff --git a/assets/js/concise/Referral.js b/assets/js/concise/Referral.js
index 9ec7702..d3d1a4b 100644
--- a/assets/js/concise/Referral.js
+++ b/assets/js/concise/Referral.js
@@ -5,7 +5,7 @@
class Referral {
constructor() {
- this.container = document.querySelector('.jvb-referral');
+ this.container = document.querySelector('aside.referral');
if (!this.container) {
return;
}
@@ -13,15 +13,12 @@
this.a11y = window.jvbA11y;
this.toggle = document.querySelector('button[data-action="toggle-referral"]');
+ this.hasCopy = navigator.clipboard && navigator.clipboard.writeText;
this.initElements();
+ this.storesInited = false;
+ this.initStore();
this.initListeners();
this.checkForReferral();
-
- // Load additional data for logged-in users
- if (this.isLoggedIn()) {
- this.loadStats();
- this.loadRecentReferrals();
- }
}
initElements() {
@@ -29,6 +26,17 @@
copyBtn: '.copy-btn',
checkCode: '.check-code-btn',
submit: '[type=submit]',
+ recentList: '.recent-referrals-list',
+ invite: 'form.invite',
+ adminList: '.items-list.referral',
+ dash: '.replace .referral-dashboard',
+ stats: {
+ codeUsed: '[data-stat="code_used"]',
+ consultations: '[data-stat="consultations"]',
+ treatments: '[data-stat="treatments"]',
+ rewards: '[data-stat="total_rewards"]'
+ },
+ list: '.referrals-list'
};
this.forms = this.container.querySelectorAll('form');
@@ -45,11 +53,120 @@
});
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);
+
+ this.ui = window.uiFromSelectors(this.selectors);
+
+ this.dashTabs = null;
+ if (this.ui.dash) {
+ this.dashTabs = new window.jvbTabs(this.ui.dash);
+ }
+
+ if (!this.hasCopy) {
+ document.querySelectorAll(this.selectors.copyBtn).forEach(btn => {
+ btn.remove();
+ });
+ }
+ this.formController = null;
+
+ if (this.ui.invite) {
+ this.formController = new window.jvbForm();
+ this.formController.registerForm(
+ this.ui.invite,
+ {
+ autosave: true,
+ endpoint: 'referrals',
+ formStatus: false,
+ }
+ );
+
+ this.formController.subscribe((event, data) => {
+ if (event === 'form-submit') {
+ data = data.fullData;
+ data.action = 'invite';
+ window.jvbQueue.addToQueue(
+ {
+ endpoint: 'referrals',
+ data: data,
+ title: 'Submitting invitations',
+ }
+ );
+ }
+ });
+ }
+ }
+
+ 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: false,
+ filters: {
+ type: 'dashboard',
+ user: window.auth.getUser()
+ }
+ },
+ // Referrals list store
+ {
+ storeName: 'list',
+ keyPath: 'id',
+ endpoint: 'referrals',
+ TTL: 10 * 60 * 1000,
+ showLoading: false,
+ delayFetch: false,
+ 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));
+ }
+
+ if (this.ui.dash) {
+ this.initViewController();
+ }
+ }
+
+ initViewController() {
+ if (!this.listStore || !this.ui.adminList) return;
+
+ this.view = new window.jvbViews(this.ui.adminList, this.listStore);
+ this.view.subscribe((event, data) => {
+ switch(event) {
+ case 'item-action':
+ this.handleItemAction(data);
+ break;
+ case 'bulk-action':
+ this.handleBulkAction(data);
+ break;
+ }
+ });
}
initListeners() {
@@ -70,9 +187,151 @@
}
isLoggedIn() {
- return Boolean(jvbSettings.currentUser);
+ 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;
+ }
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ /**
+ * Handle item actions (remove, resend)
+ */
+ handleItemAction(data) {
+ const { action, itemId } = data;
+
+ switch(action) {
+ case 'remove':
+ this.removeReferral(itemId);
+ break;
+ case 'resend':
+ this.resendInvite(itemId);
+ break;
+ }
+ }
+
+ /**
+ * Remove referral from list
+ */
+ async removeReferral(id) {
+ if (!confirm('Remove this referral from your list?')) return;
+
+ try {
+ const response = await fetch(`${jvbSettings.api}referrals`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': window.auth.getNonce()
+ },
+ body: JSON.stringify({
+ action: 'remove',
+ referral_id: id
+ })
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ // Refresh DataStore
+ if (this.listStore) this.listStore.fetch();
+ if (this.statsStore) this.statsStore.fetch();
+ this.a11y?.announce('Referral removed');
+ }
+ } catch (error) {
+ console.error('Error removing referral:', error);
+ }
+ }
+
+ /**
+ * Resend invite email
+ */
+ async resendInvite(id) {
+ try {
+ const response = await fetch(`${jvbSettings.api}referrals`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': window.auth.getNonce()
+ },
+ body: JSON.stringify({
+ action: 'resend',
+ referral_id: id
+ })
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ this.a11y?.announce('Invitation resent');
+ } else {
+ alert(result.message || 'Cannot resend yet. Wait 7 days between invites.');
+ }
+ } catch (error) {
+ console.error('Error resending invite:', error);
+ }
+ }
+
+
handleClick(e) {
const target = e.target.closest('.copy-btn, .check-code-btn, .attn');
if (!target) return;
@@ -98,7 +357,7 @@
const text = codeElement.textContent.trim();
// Try clipboard API first
- if (navigator.clipboard && navigator.clipboard.writeText) {
+ if (this.hasCopy) {
navigator.clipboard.writeText(text).then(() => {
this.showCopySuccess(button);
}).catch(() => {
@@ -106,10 +365,6 @@
this.selectText(codeElement);
this.showCopyFallback(button);
});
- } else {
- // Fallback to selection
- this.selectText(codeElement);
- this.showCopyFallback(button);
}
}
@@ -226,15 +481,19 @@
* Check for ?ref parameter in URL and pre-fill code
*/
async checkForReferral() {
- const isLoggedIn = this.getUrlParameter('seeReferral');
const refCode = this.getUrlParameter('ref');
+ const refName = this.getUrlParameter('rname');
+ const refEmail = this.getUrlParameter('remail');
+ const seeReferral = this.getUrlParameter('seeReferral');
- if (!isLoggedIn && !refCode) {
+ if (!refCode && !seeReferral) {
return;
}
- if (!refCode) {
+ // If logged in user just wants to see referral popup
+ if (seeReferral && !refCode) {
this.popup.openPopup();
+ this.removeUrlParameter('seeReferral');
return;
}
@@ -248,7 +507,21 @@
codeInput.value = code;
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;
+ }
+
+ 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 {
@@ -264,9 +537,9 @@
);
}
- // Focus on name input
+ // Focus on name input if not prefilled
const nameInput = this.container.querySelector('[name="referral_name"]');
- if (nameInput) {
+ if (nameInput && !nameInput.value) {
nameInput.focus();
}
} else {
@@ -280,6 +553,8 @@
// Clean up URL
this.removeUrlParameter('ref');
+ this.removeUrlParameter('rname');
+ this.removeUrlParameter('remail');
}
getUrlParameter(name) {
@@ -297,11 +572,11 @@
* 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',
- 'X-WP-Nonce': jvbSettings.nonce
+ 'X-WP-Nonce': window.auth.getNonce()
},
body: JSON.stringify({ code: code })
});
@@ -317,8 +592,8 @@
if (!statsContainer) return;
try {
- const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${jvbSettings.currentUser}`, {
- headers: { 'X-WP-Nonce': jvbSettings.nonce }
+ const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${window.auth.getUser()}`, {
+ headers: { 'X-WP-Nonce': window.auth.getNonce() }
});
const data = await response.json();
@@ -330,6 +605,23 @@
}
}
+ async loadSidebarStats() {
+ try {
+ const response = await fetch(
+ `${jvbSettings.api}referrals/stats?user=${window.auth.getUser()}&type=quick`,
+ { headers: { 'X-WP-Nonce': window.auth.getNonce() } }
+ );
+
+ const data = await response.json();
+ if (data.success && data.stats) {
+ this.updateSidebarStats(data.stats);
+ }
+ } catch (error) {
+ console.error('Error loading sidebar stats:', error);
+ }
+ }
+
+
/**
* Update stats display
*/
@@ -350,49 +642,25 @@
}
/**
- * 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) {
+ 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;
}
- const html = referrals.map(ref => `
+ 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.status}">${ref.status}</span>
+ <span class="status-badge">${ref.referral_status}</span>
</div>
- <div class="referral-date">${this.formatDate(ref.referred_at)}</div>
+ <div class="referral-date">${window.formatTimeAgo(ref.referred_at)}</div>
</div>
`).join('');
-
- container.innerHTML = html;
}
/**
@@ -420,23 +688,23 @@
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') {
+ // Registration with referral code - goes to LoginRoutes
const data = {
name: formData.get('referral_name'),
email: formData.get('referral_email'),
- code: formData.get('referral_code')
+ referral_code: formData.get('referral_code')
};
- if (!data.name || !data.email || !data.code) {
+ if (!data.name || !data.email || !data.referral_code) {
result.message = 'Please fill in all fields';
} else {
- result = await this.makeRequest('referrals/register', data);
+ result = await this.makeRequest('auth/register', data); // UPDATED endpoint
}
} else if (form.id === 'login-form') {
const data = {
@@ -465,8 +733,7 @@
async makeRequest(endpoint, data) {
const validEndpoints = [
'magic',
- 'referrals/register',
- 'referrals/check-code'
+ 'auth/register'
];
if (!validEndpoints.includes(endpoint)) {
@@ -477,11 +744,22 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'X-WP-Nonce': jvbSettings.nonce,
+ 'X-WP-Nonce': window.auth.getNonce(),
},
body: JSON.stringify(data)
});
+ // Add error handling to see the actual response
+ 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();
}
@@ -565,6 +843,10 @@
}
}
-document.addEventListener('DOMContentLoaded', () => {
- window.jvbReferral = new Referral();
+document.addEventListener('DOMContentLoaded', async function () {
+ window.auth.subscribe((event) => {
+ if (event === 'auth-loaded') {
+ window.jvbReferral = new Referral();
+ }
+ });
});
--
Gitblit v1.10.0