From 235ce5716edc2f7cbe80fdccf26eac7269587839 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 08 Jun 2026 04:38:18 +0000
Subject: [PATCH] =FavouritesManager.php and FavouritesRoutes.php fixes. Moving all logic to FavouritesManager.php. Still some left to do
---
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