From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh

---
 assets/js/concise/Referral.js |  472 +++++++++++++++++++++++++---------------------------------
 1 files changed, 206 insertions(+), 266 deletions(-)

diff --git a/assets/js/concise/Referral.js b/assets/js/concise/Referral.js
index d3d1a4b..7764657 100644
--- a/assets/js/concise/Referral.js
+++ b/assets/js/concise/Referral.js
@@ -15,7 +15,6 @@
 
 		this.hasCopy = navigator.clipboard && navigator.clipboard.writeText;
 		this.initElements();
-		this.storesInited = false;
 		this.initStore();
 		this.initListeners();
 		this.checkForReferral();
@@ -27,20 +26,16 @@
 			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');
-		this.popup = new window.jvbPopup({
+		this.popup = window.jvbPopup.registerPopup({
 			toggle: this.toggle,
 			popup: this.container,
 			name: 'Referral Box',
@@ -55,49 +50,19 @@
 		this.tabs = null;
 
 		if (this.container.querySelector('nav.tabs')) {
-			this.tabs = new window.jvbTabs(this.container, {updateURL: false});
+			this.tabs = window.jvbTabs.registerTab(this.container, {updateURL: false});
 		}
 
 
 		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() {
@@ -113,7 +78,7 @@
 					endpoint: 'referrals/stats',
 					TTL: 5 * 60 * 1000,
 					showLoading: false,
-					delayFetch: false,
+					delayFetch: true,
 					filters: {
 						type: 'dashboard',
 						user: window.auth.getUser()
@@ -126,7 +91,7 @@
 					endpoint: 'referrals',
 					TTL: 10 * 60 * 1000,
 					showLoading: false,
-					delayFetch: false,
+					delayFetch: true,
 					filters: {
 						user: window.auth.getUser(),
 						status: 'all',
@@ -147,27 +112,9 @@
 		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() {
 		this.clickHandler = this.handleClick.bind(this);
@@ -254,82 +201,7 @@
 		}
 	}
 
-	/**
-	 * 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) {
@@ -359,65 +231,203 @@
 		// Try clipboard API first
 		if (this.hasCopy) {
 			navigator.clipboard.writeText(text).then(() => {
-				this.showCopySuccess(button);
-			}).catch(() => {
-				// Fallback to selection
-				this.selectText(codeElement);
-				this.showCopyFallback(button);
+				button.classList.toggle('success');
+				setTimeout(() => {
+					button.classList.remove('success');
+				}, 1500);
 			});
 		}
 	}
 
+
 	/**
-	 * Select text in element
+	 * Handle error response with field-specific feedback
 	 */
-	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();
+	handleError(form, result) {
+		const { message, code, field } = result;
+
+		// If there's a specific field, highlight it
+		if (field) {
+			this.showFieldError(form, field, message);
+		} else {
+			// Show general form error using FormController pattern
+			this.showFormStatus(form, 'error', message || 'Something went wrong. Please try again.');
+		}
+
+		// Handle specific error codes
+		switch(code) {
+			case 'duplicate_email':
+				// Could add additional UI feedback
+				break;
+			case 'invalid_code':
+				// Unlock the referral code field so user can correct it
+				const codeInput = form.querySelector('[name="referral_code"]');
+				if (codeInput) {
+					codeInput.readOnly = false;
+					codeInput.focus();
+				}
+				break;
+			case 'turnstile_failed':
+				// Refresh Turnstile widget if available
+				if (window.turnstile && form.querySelector('.cf-turnstile')) {
+					window.turnstile.reset();
+				}
+				break;
 		}
 	}
 
 	/**
-	 * Show copy success feedback
+	 * Show error for specific field
 	 */
-	showCopySuccess(button) {
-		const originalHTML = button.innerHTML;
-		button.innerHTML = window.jvbIcon('check', {size: 16}) + ' Copied!';
-		button.classList.add('success');
+	showFieldError(form, fieldName, message) {
+		// Find the field wrapper (handles both direct names and referral_ prefixed names)
+		let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"]`);
+		if (!fieldWrapper) {
+			fieldWrapper = form.querySelector(`.field[data-field="referral_${fieldName}"]`);
+		}
 
-		setTimeout(() => {
-			button.innerHTML = originalHTML;
-			button.classList.remove('success');
-		}, 2000);
+		if (!fieldWrapper) {
+			// If no field wrapper found, show as general form error
+			this.showFormStatus(form, 'error', message);
+			return;
+		}
+
+		const input = fieldWrapper.querySelector('input, textarea, select');
+		const validationMessage = fieldWrapper.querySelector('.validation-message');
+		const errorIcon = fieldWrapper.querySelector('.validation-icon.error');
+		const successIcon = fieldWrapper.querySelector('.validation-icon.success');
+
+		if (!input) {
+			this.showFormStatus(form, 'error', message);
+			return;
+		}
+
+		// Apply error state (following FormController pattern)
+		fieldWrapper.classList.remove('has-success');
+		fieldWrapper.classList.add('has-error');
+		input.classList.add('error');
+		input.setAttribute('aria-invalid', 'true');
+
+		// Show error icon, hide success icon
+		if (errorIcon) errorIcon.hidden = false;
+		if (successIcon) successIcon.hidden = true;
+
+		// Show error message
+		if (validationMessage) {
+			validationMessage.textContent = message;
+			validationMessage.hidden = false;
+		}
+
+		// Focus the problematic field
+		input.focus();
+
+		// Announce to screen readers
+		this.a11y?.announce(`Error in ${fieldName}: ${message}`);
+	}
+
+	showFormStatus(form, status, message = '') {
+		const statusWrap = form.querySelector('.fstatus');
+		if (!statusWrap) {
+			console.warn('No .fstatus element found in form');
+			return;
+		}
+
+		statusWrap.hidden = false;
+		const statusElement = statusWrap.querySelector('.message');
+
+		// Clear previous state
+		statusWrap.querySelector('.icon')?.remove();
+		statusWrap.querySelector('.actions')?.remove();
+
+		// Status messages
+		const messages = {
+			'saving': 'Sending...',
+			'submitted': 'Sent successfully!',
+			'error': 'Something went wrong',
+			'checking': 'Checking code...'
+		};
+
+		// Status icons (using window.getIcon like FormController)
+		const icons = {
+			'submitted': 'check-circle',
+			'error': 'close-circle',
+			'checking': 'loading'
+		};
+
+		// Add icon if available
+		if (icons[status] && window.getIcon) {
+			const icon = window.getIcon(icons[status]);
+			if (icon) {
+				statusWrap.prepend(icon);
+			}
+		}
+
+		// Set message
+		if (statusElement) {
+			statusElement.textContent = message || messages[status] || status;
+		}
+
+		// Add loading class for pending states
+		statusWrap.classList.toggle('loading', ['saving', 'checking'].includes(status));
+
+		// Auto-hide success messages
+		if (status === 'submitted') {
+			setTimeout(() => statusWrap.hidden = true, 3000);
+		}
+
+		// Announce to screen readers
+		if (this.a11y) {
+			this.a11y.announce(message || messages[status] || status);
+		}
 	}
 
 	/**
-	 * Show fallback message
+	 * Clear all form errors
 	 */
-	showCopyFallback(button) {
-		const originalHTML = button.innerHTML;
-		button.innerHTML = '✓ Selected - Press Ctrl+C';
-		button.classList.add('selected');
+	clearFormErrors(form) {
+		// Clear field-level errors
+		form.querySelectorAll('.field.has-error, .field.has-success').forEach(fieldWrapper => {
+			this.clearFieldValidation(fieldWrapper);
+		});
 
-		setTimeout(() => {
-			button.innerHTML = originalHTML;
-			button.classList.remove('selected');
-		}, 3000);
+		// Hide form status
+		const statusWrap = form.querySelector('.fstatus');
+		if (statusWrap) {
+			statusWrap.hidden = true;
+		}
+	}
+
+	clearFieldValidation(fieldWrapper) {
+		if (!fieldWrapper) return;
+
+		const input = fieldWrapper.querySelector('input, textarea, select');
+		const validationMessage = fieldWrapper.querySelector('.validation-message');
+		const validationIcons = fieldWrapper.querySelectorAll('.validation-icon');
+
+		// Remove classes
+		fieldWrapper.classList.remove('has-error', 'has-success');
+		if (input) {
+			input.classList.remove('error');
+			input.removeAttribute('aria-invalid');
+		}
+
+		// Hide icons and messages
+		validationIcons.forEach(icon => icon.hidden = true);
+		if (validationMessage) {
+			validationMessage.hidden = true;
+			validationMessage.textContent = '';
+		}
 	}
 
 	handleInput(e) {
 		if (e.target.id === 'referral_code' || e.target.name === 'referral_code') {
 			e.target.value = e.target.value.toUpperCase();
 		}
+		// Clear field error when user types
+		const fieldWrapper = e.target.closest('.field');
+		if (fieldWrapper && fieldWrapper.classList.contains('has-error')) {
+			this.clearFieldValidation(fieldWrapper);
+		}
 	}
 
 	/**
@@ -585,63 +595,6 @@
 	}
 
 	/**
-	 * 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=${window.auth.getUser()}`, {
-				headers: { 'X-WP-Nonce': window.auth.getNonce() }
-			});
-
-			const data = await response.json();
-			if (data.success && data.stats) {
-				this.updateStats(data.stats);
-			}
-		} catch (error) {
-			console.error('Error loading stats:', error);
-		}
-	}
-
-	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
-	 */
-	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);
-		}
-	}
-
-	/**
 	 * Render recent referrals list
 	 */
 	renderRecentReferrals() {
@@ -664,22 +617,6 @@
 	}
 
 	/**
-	 * 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) {
@@ -688,6 +625,8 @@
 		const form = event.target;
 		const formData = new FormData(form);
 
+		// Clear any existing errors
+		this.clearFormErrors(form);
 		this.setFormLoading(true, form);
 
 		try {
@@ -695,32 +634,45 @@
 
 			if (form.id === 'referral-code-form') {
 				// Registration with referral code - goes to LoginRoutes
-				const data = {
+				let data = {
 					name: formData.get('referral_name'),
 					email: formData.get('referral_email'),
 					referral_code: formData.get('referral_code')
 				};
 
+				const turnstileInput = form.querySelector('input[name="cf-turnstile-response"]');
+				if (turnstileInput && turnstileInput.value) {
+					data['cf-turnstile-response'] = turnstileInput.value;
+				}
+
 				if (!data.name || !data.email || !data.referral_code) {
 					result.message = 'Please fill in all fields';
 				} else {
 					result = await this.makeRequest('auth/register', data); // UPDATED endpoint
 				}
 			} else if (form.id === 'login-form') {
-				const data = {
+				let data = {
 					type: 'login',
-					email: formData.get('login_email'),
+					user_email: formData.get('login_email'),
 					context: {
 						redirect_to: window.location.href + '?seeReferral=1'
 					}
 				};
-				result = await this.makeRequest('magic', data);
+				const turnstileInput = form.querySelector('input[name="cf-turnstile-response"]');
+				if (turnstileInput && turnstileInput.value) {
+					data['cf-turnstile-response'] = turnstileInput.value;
+				}
+				if (!data['user_email']) {
+					result.message = 'Please fill in your email';
+				} else {
+					result = await this.makeRequest('auth/magic', data);
+				}
 			}
 
 			if (result.success) {
 				this.handleSuccess(form, result);
 			} else {
-				this.showFormMessage(form, result.message || 'Something went wrong. Please try again.', 'error');
+				this.handleError(form, result);
 			}
 		} catch (error) {
 			console.error('Error submitting form:', error);
@@ -732,7 +684,7 @@
 
 	async makeRequest(endpoint, data) {
 		const validEndpoints = [
-			'magic',
+			'auth/magic',
 			'auth/register'
 		];
 
@@ -740,16 +692,11 @@
 			return { success: false, message: 'Invalid endpoint' };
 		}
 
-		const response = await fetch(`${jvbSettings.api}${endpoint}`, {
+		const response = await window.auth.fetch(`${jvbSettings.api}${endpoint}`, {
 			method: 'POST',
-			headers: {
-				'Content-Type': 'application/json',
-				'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);
@@ -814,20 +761,11 @@
 	 * Set form loading state
 	 */
 	setFormLoading(loading, form) {
-		const inputs = form.querySelectorAll('input, button');
+		const inputs = form.querySelectorAll('input, button, textarea, select');
 		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...';
-				}
-			}
+		if (loading) {
+			this.showFormStatus(form, 'saving');
 		}
 	}
 
@@ -841,6 +779,8 @@
 		});
 		this.container.dispatchEvent(event);
 	}
+
+
 }
 
 document.addEventListener('DOMContentLoaded', async function () {

--
Gitblit v1.10.0