From fff721dd185f5b97f7ae7a6e64189e55887ff590 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 05 Jul 2026 18:36:57 +0000
Subject: [PATCH] =Cleaning up the Square integration (still a bit more to do yet). Also majorly overhauled /rest/ files to ignore a rest request 'user' paramater, and rely on get_current_user_id() instead.
---
inc/managers/Dashboard/DashboardPage.php | 3
inc/rest/routes/NotificationsRoutes.php | 52 +-
inc/registrar/config/Integration.php | 1
inc/integrations/BlueSky.php | 12
assets/js/min/form.min.js | 2
inc/managers/Dashboard/DashboardManager.php | 120 +++++
inc/rest/routes/ContentRoutes.php | 8
assets/js/concise/Login.js | 78 +++
inc/rest/routes/FeedRoutes.php | 10
inc/rest/routes/OptionsRoutes.php | 7
inc/rest/routes/ApprovalRoutes.php | 10
inc/rest/routes/ResponseRoutes.php | 6
JVBase.php | 8
inc/managers/LoginManager.php | 13
inc/rest/routes/IntegrationsSquareRoutes.php | 49 -
inc/integrations/Square.php | 250 +++++------
inc/ui/CRUDSkeleton.php | 38 +
inc/integrations/Integrations.php | 60 --
inc/integrations/Cloudflare.php | 12
inc/integrations/Facebook.php | 12
inc/rest/routes/NewsRoutes.php | 3
inc/integrations/Instagram.php | 13
inc/ui/Navigation.php | 1
assets/js/concise/FormController.js | 72 +++
inc/integrations/Umami.php | 12
inc/rest/routes/IntegrationsHelcimRoutes.php | 9
assets/js/min/login.min.js | 2
inc/registrar/Posts.php | 2
inc/rest/routes/FavouritesRoutes.php | 32 -
inc/rest/routes/ReferralRoutes.php | 35
inc/rest/routes/ContentTermsRoutes.php | 16
inc/meta/MetaTypeManager.php | 2
inc/managers/MagicLinkManager.php | 9
inc/rest/_setup.php | 1
inc/rest/routes/UploadRoutes.php | 30
assets/js/concise/AuthManager.js | 15
assets/js/min/auth.min.js | 2
inc/registrar/Fields.php | 4
inc/integrations/GoogleMyBusiness.php | 12
inc/rest/routes/QueueRoutes.php | 25 +
inc/rest/routes/IntegrationsRoutes.php | 5
inc/rest/routes/Invitations.php | 10
inc/rest/routes/SettingsRoutes.php | 7
inc/integrations/PostMark.php | 12
inc/rest/PermissionHandler.php | 21
inc/integrations/GoogleMaps.php | 12
inc/integrations/Helcim.php | 12
inc/rest/routes/VoteRoutes.php | 15
inc/rest/routes/UserRoutes.php | 58 ++
49 files changed, 752 insertions(+), 448 deletions(-)
diff --git a/JVBase.php b/JVBase.php
index 1a3f4f7..90f903d 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -47,6 +47,7 @@
use JVBase\rest\routes\AdminRoutes;
use JVBase\rest\routes\IntegrationsRoutes;
use JVBase\base\SchemaHelper;
+use UserRoutes;
if (!defined('ABSPATH')) {
exit;
@@ -120,7 +121,8 @@
'queue' => new QueueRoutes(),
'settings' => new SettingsRoutes(),
'upload' => new UploadRoutes(),
- 'forms' => new FormRoutes()
+ 'forms' => new FormRoutes(),
+ 'user' => new UserRoutes()
];
if (Site::has('magic_link')) {
@@ -212,7 +214,7 @@
protected function setupIntegrations(): void
{
foreach(array_keys(Site::getIntegrations()) as $integration) {
- $this->integrations[$integration] = new $this->serviceMap[$integration]();
+ $this->integrations[$integration] = $this->serviceMap[$integration]::getInstance();
}
}
@@ -302,7 +304,7 @@
if (!array_key_exists($service, $this->integrations)) {
return null;
}
- return new $this->serviceMap[$service]($userID);
+ return $this->serviceMap[$service]::getInstance($userID);
}
return (array_key_exists($service, $this->integrations)) ? $this->integrations[$service] : null;
}
diff --git a/assets/js/concise/AuthManager.js b/assets/js/concise/AuthManager.js
index 7e8a435..29946d2 100644
--- a/assets/js/concise/AuthManager.js
+++ b/assets/js/concise/AuthManager.js
@@ -243,6 +243,21 @@
});
}
+ getHeader(action) {
+ switch (action) {
+ case 'dash':
+ return {
+ 'X-Action-Nonce': this.getNonce('dash')
+ };
+ case 'default':
+ return {
+ 'X-WP-Nonce': this.getNonce()
+ };
+ default:
+ return {};
+ }
+ }
+
}
// Initialize global instance
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index b92c486..ccbea31 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -490,6 +490,7 @@
e.preventDefault();
console.log('Submitting form...', this.changes.get(form.id)?.changes ?? {});
await this.handleServerSave(form, this.changes.get(form.id)?.changes ?? {});
+
}
if (form.options.showSummary) {
@@ -675,11 +676,62 @@
headers: form.options.headers??{},
});
} else {
- await window.auth.fetch(jvbSettings.api+form.options.endpoint, {
- body: changes,
- headers: form.options.headers??{},
- method: 'POST',
- });
+ try {
+ let response = await window.auth.fetch(jvbSettings.api+form.options.endpoint, {
+ body: changes,
+ headers: form.options.headers??{},
+ method: 'POST',
+ });
+
+ if (!response.ok) {
+ let errorDetails;
+ try {
+ // Attempt to parse the server's error payload as JSON
+ errorDetails = await response.json();
+ } catch {
+ // Fallback if the error response isn't JSON (e.g., HTML error page)
+ errorDetails = { message: response.statusText || 'Unknown HTTP error' };
+ }
+ this.notify('form-error', {
+ config: form,
+ data: errorDetails
+ });
+ return;
+ }
+ let result = await response.json();
+ this.notify('form-success', {
+ config: form,
+ result: result
+ });
+ } catch (error) {
+ if (error.status) {
+ this.notify('form-error', {
+ config: form,
+ data: error
+ });
+ // This is an HTTP error thrown manually (e.g., 404, 500)
+ } else if (error instanceof SyntaxError) {
+ this.notify('form-error', {
+ config: form,
+ data: {
+ status: 'error',
+ success: false,
+ message: 'Something went wrong'
+ }
+ });
+ // This happens if response.json() fails on a successful 200 OK response
+ console.error('Data Error: Server returned invalid JSON format.');
+ } else {
+ this.notify('form-error', {
+ config: form,
+ data: {
+ status: 'network_error',
+ message: 'Network error: could not connect to the server'
+ }
+ });
+ }
+ }
+
}
}
@@ -710,6 +762,9 @@
if (!Object.hasOwn(form.dataset, 'formId')) {
form.dataset.formId = window.generateID('form_');
}
+ if (Object.hasOwn(form.dataset, 'action')) {
+ options.headers = window.auth.getHeader(form.dataset.action);
+ }
const formId = form.dataset.formId;
this.addFormListeners(form);
@@ -845,7 +900,7 @@
p: 'p',
},
setup({ el, refs, manyRefs, data }) {
- const skipFields = ['sendAll', ...data.config.options.ignore??[]];
+ const skipFields = ['sendAll', ...data?.config?.options?.ignore??[]];
for (let [key, value] of Object.entries(data.changes)) {
if (skipFields.includes(key) || form.isEmptyValue(value)) continue;
@@ -899,8 +954,6 @@
}
refs.result?.remove();
- data.config.element.after(el);
- window.fade(data.config.element, false);
}
}
);
@@ -2307,6 +2360,9 @@
window.auth.subscribe(event => {
if (event === 'auth-loaded') {
window.jvbForm = new FormController();
+ document.querySelectorAll('form[data-auto]').forEach(form => {
+ window.jvbForm.registerForm(form, {});
+ });
}
});
});
diff --git a/assets/js/concise/Login.js b/assets/js/concise/Login.js
index 88528e8..1d51b30 100644
--- a/assets/js/concise/Login.js
+++ b/assets/js/concise/Login.js
@@ -14,10 +14,84 @@
for (let [id, form] of Object.entries(this.ui)) {
if (form) {
this.forms[id] = window.jvbForm.registerForm(form, {
- cache: false,
- useQueue: false,
+ cache: false,
+ showStatus: false,
+ useQueue: false,
});
+ if (id === 'magic') {
+ window.jvbForm.subscribe((event, data) => {
+
+ if (data.config === this.forms[id]) {
+ if (event === 'form-submit') {
+ form.hidden = true;
+ this.spinner = window.jvbTemplates.create('spinner', {});
+ form.parentElement.append(this.spinner);
+
+ } else if (event === 'form-success') {
+ form.hidden = true;
+ let el = window.jvbTemplates.create('formSummary', {config: form, changes: {}});
+ Array.from(el.children).forEach(ch => {
+ if (ch.tagName === 'H2') {
+ ch.textContent = 'Check your email!';
+ } else if (ch.classList.contains('message')) {
+ let children = Array.from(ch.children);
+ children[0].textContent = 'You\'ll find a magic link to log in in your email';
+ children[1].textContent = 'If you can\'t find it - check your spam folder.';
+ children[2].textContent = 'The link will expire in 15 minutes.';
+ children[3].remove();
+ } else if (ch.classList.contains('summary')) {
+ ch.remove();
+ }
+ });
+ if (this.spinner) {
+ this.spinner.remove();
+ }
+ form.parentElement.append(el);
+ } else if (event === 'form-error' ) {
+ if (this.spinner) {
+ this.spinner.remove();
+ }
+ if (Object.hasOwn(data.data, 'code') && data.data.code === 'rate_limit_exceeded') {
+
+ let el = window.jvbTemplates.create('formSummary', {config: form, changes: {}});
+ Array.from(el.children).forEach(ch => {
+ if (ch.tagName === 'H2') {
+ ch.textContent = 'Slow down there!';
+ } else if (ch.classList.contains('message')) {
+ let children = Array.from(ch.children);
+ children[0].textContent = 'You tried submitting a few too many times in a short window.';
+ children[1].textContent = 'Try again in an hour or so.';
+ children[2].remove();
+ children[3].remove();
+ } else if (ch.classList.contains('summary')) {
+ ch.remove();
+ }
+ });
+ form.parentElement.append(el);
+ } else {
+ let el = window.jvbTemplates.create('formSummary', {config: form, changes: {}});
+ Array.from(el.children).forEach(ch => {
+ if (ch.tagName === 'H2') {
+ ch.textContent = 'Something went wrong.';
+ } else if (ch.classList.contains('message')) {
+ let children = Array.from(ch.children);
+ children[0].textContent = 'We aren\'t exactly sure what.';
+ children[1].textContent = 'Try clearing your cookies and trying again. If the problem persists, let us know!';
+ children[2].remove();
+ children[3].remove();
+ } else if (ch.classList.contains('summary')) {
+ ch.remove();
+ }
+ });
+ form.parentElement.append(el);
+ }
+ }
+ }
+
+ });
+ }
+
}
}
}
diff --git a/assets/js/min/auth.min.js b/assets/js/min/auth.min.js
index 2f41ffe..bf093ef 100644
--- a/assets/js/min/auth.min.js
+++ b/assets/js/min/auth.min.js
@@ -1 +1 @@
-window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.cacheExpiry=3e5,this.init()}async init(){if(!this.isAuthenticating){this.isAuthenticating=!0;try{if("undefined"!=typeof jvbAuth)return this.setAuthData(jvbAuth),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1});await this.fetchAuth()}catch(t){this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,i={}){const e=async(s=0)=>{const n=i.body instanceof FormData,h={...!n&&{"Content-Type":"application/json"},...i.headers,"X-WP-Nonce":this.getNonce()};n||"object"!=typeof i.body||Object.hasOwn(i.body,"user")||(i.body.user=this.getUser());let a={...i,credentials:"same-origin",headers:h};n||"object"!=typeof a.body||(a.body=JSON.stringify(a.body));const r=await fetch(t,a);if((403===r.status||401===r.status)&&0===s){const t=await r.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),e(s+1)}return r};return e()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const i=await t.json();this.setAuthData(i),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){const i=this.initialized&&this.authenticated;this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{},console.log(this.nonces),i&&!this.authenticated&&(window.location.href=`/login?redirect_to=${encodeURIComponent(window.location.href)}`)}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={}}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,i){this.subscribers.forEach(e=>{try{e(t,i)}catch(t){console.error("Subscriber error:",t)}})}};
\ No newline at end of file
+window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.cacheExpiry=3e5,this.init()}async init(){if(!this.isAuthenticating){this.isAuthenticating=!0;try{if("undefined"!=typeof jvbAuth)return this.setAuthData(jvbAuth),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1});await this.fetchAuth()}catch(t){this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,e={}){const i=async(s=0)=>{const n=e.body instanceof FormData,h={...!n&&{"Content-Type":"application/json"},...e.headers,"X-WP-Nonce":this.getNonce()};n||"object"!=typeof e.body||Object.hasOwn(e.body,"user")||(e.body.user=this.getUser());let a={...e,credentials:"same-origin",headers:h};n||"object"!=typeof a.body||(a.body=JSON.stringify(a.body));const r=await fetch(t,a);if((403===r.status||401===r.status)&&0===s){const t=await r.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),i(s+1)}return r};return i()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const e=await t.json();this.setAuthData(e),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){const e=this.initialized&&this.authenticated;this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{},console.log(this.nonces),e&&!this.authenticated&&(window.location.href=`/login?redirect_to=${encodeURIComponent(window.location.href)}`)}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={}}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach(i=>{try{i(t,e)}catch(t){console.error("Subscriber error:",t)}})}getHeader(t){switch(t){case"dash":return{"X-Action-Nonce":this.getNonce("dash")};case"default":return{"X-WP-Nonce":this.getNonce()};default:return{}}}};
\ No newline at end of file
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index a5400c0..d2c6637 100644
--- a/assets/js/min/form.min.js
+++ b/assets/js/min/form.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.queue=window.jvbQueue,this.populate=window.jvbPopulate,this.changes=new Map,this.forms=new Map,this.inputs=new Map,this.repeaters=new Map,this.tagLists=new Map,this.charLimits=new Map,this.quantityFields=new Map,this.quillInstances=new Map,this.dependencies=new Map,this.subscribers=new Set,this.isRestoring=!1,this.hasListeners=!1,this.hasUploads=!1,this.summaryTemplate=!1,this.init()}init(){this.templates=window.jvbTemplates,this.defineSummaryTemplate(),this.initElements(),this.initListeners(),this.initStore(),this.initValidators(),this.initUploadSubscription()}initUploadSubscription(){window.jvbUploads.subscribe((e,t)=>{if(this.hasUploads&&"upload-received"===e){let e=this.getForm(t.field);e&&this.updateItem(`${t.field.dataset.field}_tempUpload`,t.id,e)}})}initElements(){this.inputSelectors="input, textarea, select",this.selectors={tabs:{nav:"nav.tabs",sections:".tab-content",progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},buttons:"nav.tabs button"},dependsOn:"[data-depends-on]",forms:{status:{status:".fstatus",message:".fstatus .message",icon:".fstatus .icon",actions:".fstatus .actions"},restore:{container:".restore-form",restore:'[data-action="restore"]',clear:'[data-action="clear"]'}},inputs:this.inputSelectors,fields:{field:".field",label:"label",success:".success",error:".error",message:".validation-message"},repeater:{repeater:".repeater",row:".repeater-row",handle:".drag-handle",header:".repeater-row-header",remove:".remove-row",add:".add-repeater-row",template:"template",items:".repeater-items",inputs:this.inputSelectors},tagList:{tagList:".field.tag-list",input:".row",add:".add-tag",remove:".remove-tag",label:".tag-label",items:".tag-items",item:".tag-item",inputs:this.inputSelectors,value:'input[type="hidden"]'},tag:{label:".tag-label"},number:{number:".field div.quantity",increase:"button.increase",decrease:"button.decrease",input:'input[type="number"]'},limits:{hasLimit:"[data-maxlength]",limit:".limit",current:".current"}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.blurHandler=this.handleBlur.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.quantityClick=this.handleQuantityClick.bind(this),this.repeaterClick=this.handleRepeaterClick.bind(this),this.tagListClick=this.handleTagListClick.bind(this),this.tagListInput=this.handleTagListInput.bind(this)}addFormListeners(e){e.addEventListener("click",this.clickHandler),e.addEventListener("change",this.changeHandler),e.addEventListener("input",this.inputHandler),e.addEventListener("blur",this.blurHandler),e.addEventListener("submit",this.submitHandler)}removeFormListeners(e){e.removeEventListener("click",this.clickHandler),e.removeEventListener("change",this.changeHandler),e.removeEventListener("input",this.inputHandler),e.removeEventListener("blur",this.blurHandler),e.removeEventListener("submit",this.submitHandler)}initStore(){const e=window.jvbStore.register("forms",{storeName:"forms",keyPath:"id",indexes:[{name:"src",keyPath:"src"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4});this.store=e.forms,this.store.subscribe((e,t)=>{if("data-ready"===e){let e=this.store.getFiltered().filter(e=>e.src===window.location.pathname);for(let t of e)this.showPendingNotification(t.id,t.changes)}else"operation-status"===e&&"completed"===t.status&&t.config&&this.store.delete(t.config.id)})}showPendingNotification(e,t){let s=this.forms.get(e);if(!s)return;let i=s.element;if(!i)return void console.warn(`Form element not found for: ${e}`);s.ui.restore.container.hidden=!1;const a=async(e,t)=>{this.isRestoring=!0;let i={fields:e};await this.checkStoredUploads(e,t),this.populate.populate(t,i),this.a11y.announce("Previous changes restored"),this.isRestoring=!1,s.ui.restore.container.remove()},r=async e=>{await this.checkStoredUploads(t,i,!1),await this.store.delete(e),this.a11y.announce("Previous changes discarded"),s.ui.restore.container.remove()};s.ui.restore.restore.addEventListener("click",()=>a(t,i)),s.ui.restore.clear.addEventListener("click",async()=>r(e))}async checkStoredUploads(e,t,s=!0){let i=this.forms.get(t.dataset.formId);if(!i)return;let a=[];for(let[t,s]of Object.entries(e))if(t.includes("_tempUpload")){let e=t.replace("_tempUpload","");Object.hasOwn(i.ui.uploads,e)&&(a=[...a,...s])}a.length>0&&(s?await window.jvbUploads.restoreUploads(a):await window.jvbUploads.clearUploads(a))}initValidators(){this.validators={email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-+().]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const i=t.dataset.min,a=t.dataset.max;return void 0!==i&&s<parseFloat(i)?`Value must be at least ${i}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,i=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(i&&e.length>parseInt(i))||`Must be no more than ${i} characters`}}}}validateField(e){const t=this.performValidation(e);return this.updateValidationUI(e,t),t.isValid}performValidation(e){const t=e.closest(".field"),s=this.getFieldCheckedValue(e);if(!s&&!e.required)return{isValid:!0,message:""};if(e.required)if("checkbox"===e.type){if(!e.checked)return{isValid:!1,message:"This field is required"}}else if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`);if(!Array.from(t).some(e=>e.checked))return{isValid:!1,message:"Please select an option"}}else if(!s)return{isValid:!1,message:"This field is required"};if(e.checkValidity&&!e.checkValidity())return{isValid:!1,message:e.validationMessage};if(s&&Object.hasOwn(t.dataset,"pattern")){if(!new RegExp(t.dataset.pattern).test(s))return{isValid:!1,message:t.dataset.validationMessage||"Invalid format"}}if(Object.hasOwn(t.dataset,"validate")||e.type){const i=this.validators[t.dataset.validate||e.type];if(i&&i.pattern&&!i.pattern.test(s))return{isValid:!1,message:i.message};if(i&&i.test){const e=i.test(s,t);if(!0!==e)return{isValid:!1,message:e}}}return{isValid:!0,message:""}}updateValidationUI(e,t){t.isValid?this.showSuccess(e,t.message):this.showError(e,t.message)}handleClick(e){let t=this.getForm(e.target);if(!t)return;const s=window.targetCheck(e,"[data-action]");if(s){switch(s.dataset.action){case"clear-form":this.store.delete(t.id),t.element.reset(),t.ui.status.status.hidden=!0,this.a11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":t.ui.status.status.hidden=!0;break;case"next-step":e.preventDefault(),t.tabs.ui.buttons.forEach((e,s)=>{if(e.dataset.tab===t.tabs.activeTab){let e=t.tabs.ui.buttons[s+1]??!1;e&&window.jvbTabs.switchTab(e.dataset.tab,window.jvbTabs.getConfig(e))}});break;case"prev-step":e.preventDefault(),t.tabs.ui.buttons.forEach((e,s)=>{if(e.dataset.tab===t.tabs.activeTab){let e=t.tabs.ui.buttons[s-1]??!1;e&&window.jvbTabs.switchTab(e.dataset.tab,window.jvbTabs.getConfig(e))}})}}}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getField(e.target);const s=e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');if(s){if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).forEach(e=>{this.checkFieldDependency(e,t.dataset.field)})}const e=s.dataset.field;return this.maybeUpdateCollectionDisplay(t),void window.debouncer.schedule(`collection:${e}`,()=>this.updateCollectionField(s),150)}if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).forEach(e=>{this.checkFieldDependency(e,t.dataset.field)})}let i=this.getForm(e.target);this.updateItem(t.dataset.field,this.getFieldValue(e.target),i)}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target).dataset.field;window.debouncer.cancel(`form:${t.id}:validate:${s}`),this.validateField(e.target);const i=e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');i?this.updateCollectionField(i):this.updateItem(s,this.getFieldValue(e.target),t)}handleInput(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target);if(!s)return;const i=e.target,a=s.dataset.field;this.showFormStatus(t.id,"pending"),window.debouncer.schedule(`form:${t.id}:validate:${a}`,()=>this.validateField(i),500)}async handleSubmit(e){let t=this.getForm(e.target);if(t){if(this.subscribers.size>0)if(e.preventDefault(),t.options.cache){this.cancelBackup(),await this.backup();const e=await this.store.get(t.id);this.notify("form-submit",{config:t,data:e.changes})}else this.notify("form-submit",{config:t,data:this.changes.get(t.id)?.changes??{}});if(t.options.endpoint&&!t.options.handled&&(e.preventDefault(),console.log("Submitting form...",this.changes.get(t.id)?.changes??{}),await this.handleServerSave(t,this.changes.get(t.id)?.changes??{})),t.options.showSummary){const e=await this.store.get(t.id);this.showSummary({config:t,changes:e?.changes})}}}updateItem(e,t,s){if(void 0===t)return;this.changes.has(s.id)||this.changes.set(s.id,{id:s.id,timestamp:Date.now(),src:window.location.pathname,changes:{}});let i=this.changes.get(s.id);e.includes("_tempUpload")?(Object.hasOwn(i.changes,e)||(i.changes[e]=[]),i.changes[e].push(t)):i.changes[e]=t,s.tabs&&this.updateStepProgress(s),this.changes.set(s.id,i),s.options.cache&&this.scheduleBackup(),s.options.endpoint&&!s.options.handled&&this.scheduleServerSave(s,i)}updateStepProgress(e){if(console.log("First check in updateStepProgress"),!e.tabs)return;let t=this.getCurrentStep(e);if(console.log("Current step: ",t),!t)return;t-=1;let s=this.getCurrentSection(e);if(console.log("Current section: ",s),!s)return;let i=0,a=0;if(s.childNodes.forEach(e=>{if(console.log("Child node: ",e),e.classList.contains("field")){i++;let t=this.getFieldValue(e.querySelector(this.inputSelectors));console.log("Value",t),t.length>0&&a++}}),0===i)return;let r=t+a/i,n=e.ui.tabs.sections.length;window.showProgress(e.ui.tabs.progress,r,n,`Step ${t+1} of ${n}`)}getCurrentStep(e){let t=null;return e.tabs.ui.buttons.forEach(s=>{s.dataset.tab===e.tabs.activeTab&&(t=s.dataset.step)}),t}getCurrentSection(e){console.log("Getting current section: ",e.tabs.ui.sections);let t=null;return e.tabs.ui.sections.forEach(s=>{s.dataset.tab===e.tabs.activeTab&&(t=s)}),t}scheduleBackup(){window.debouncer.schedule("form_changes",async()=>{this.changes.size>0&&await this.backup()},2e3)}cancelBackup(){window.debouncer.cancel("form_changes")}async backup(){const e=new Map;for(let[t,s]of this.changes.entries()){const i=await this.store.get(t);i?e.set(t,{...i,...s,changes:{...i.changes,...s.changes},timestamp:Date.now()}):e.set(t,s)}await this.store.saveMany(e);for(let e of this.changes.keys())this.showFormStatus(e,"autosaved");this.changes.clear()}saveCache(e){if(!this.changes.has(e))return;let t=this.changes.get(e);0!==t.size&&(this.store.save(t).then(()=>{}),this.changes.delete(e))}scheduleServerSave(e,t){window.debouncer.schedule(`form_${e.id}_server_save`,async()=>{await this.handleServerSave(e,t)},1500)}cancelServerSave(e){window.debouncer.cancel(`form_${e}_server_save`)}async handleServerSave(e,t){this.cancelServerSave(e.id),e.useQueue&&window.jvbQueue?(t.user=window.auth.getUser(),window.jvbQueue.addToQueue({endpoint:e.options.endpoint,data:t,headers:e.options.headers??{}})):await window.auth.fetch(jvbSettings.api+e.options.endpoint,{body:t,headers:e.options.headers??{},method:"POST"})}registerForm(e,t){if(t={autoUpload:!1,imageMeta:!0,delay:1500,useQueue:!0,endpoint:Object.hasOwn(e.dataset,"save")?e.dataset.save:"",showStatus:!0,showSummary:!1,handled:Object.hasOwn(e.dataset,"handled"),cache:!0,ignore:[],...t},Object.hasOwn(e.dataset,"formId")&&this.forms.has(e.dataset.formId))return;Object.hasOwn(e.dataset,"formId")||(e.dataset.formId=window.generateID("form_"));const s=e.dataset.formId;this.addFormListeners(e);const i={element:e,id:s,status:"",options:t,ui:window.uiFromSelectors(this.selectors.forms,e)};return i.ui.fields={},e.querySelectorAll("[data-field]").forEach(e=>{i.ui.fields[e.dataset.field]=e}),this.initializeFields(e,i),this.forms.set(s,i),i}clearForm(e){const t=this.forms.get(e);if(!t)return;t.unsubscribeTabs&&t.unsubscribeTabs(),t.tabs&&window.jvbTabs.removeTab(t.element),t.cache&&this.changes.has(e)&&this.saveCache(e);for(let[t,s]of this.inputs.entries())s.form===e&&this.inputs.delete(t);if(this.dependencies.forEach((t,s)=>{0===(t=t.filter(t=>t.form!==e)).length&&this.dependencies.delete(s)}),Object.hasOwn(t,"hasQuill")&&this.quillInstances.has(e)){this.quillInstances.get(e).forEach(e=>{e.disable(),e.off("text-change"),e.off("selection-change");const t=e.container.parentElement,s=t?.querySelector(".ql-toolbar");if(s&&s.remove(),e.setText(""),t&&t.classList.contains("editor-container")){const e=t.nextElementSibling;"TEXTAREA"===e?.tagName&&(e.style.display=""),t.remove()}}),this.quillInstances.delete(e)}let s={repeater:this.repeaters,tagList:this.tagLists,charLimit:this.charLimits,quantity:this.quantityFields};for(let[t,i]of Object.entries(s)){if(0===i.size)continue;let s=Array.from(i.values()).filter(t=>t.form===e);s.length>0&&s.forEach(e=>{switch(t){case"repeater":this.removeRepeaterListeners(e.element);break;case"tagList":this.removeTagListListeners(e.element);break;case"charLimit":this.removeCharacterLimitListeners(e.element);break;case"quantity":this.removeQuantityListeners(e.element)}i.has(e.id)&&i.delete(e.id)})}this.removeFormListeners(t.element),this.forms.delete(e),window.debouncer.cancel("form_changes")}defineSummaryTemplate(){this.summaryTemplate=!0;let e=this;this.templates.define("formSummary",{refs:{result:".result",h3:"h3",p:"p"},setup({el:t,refs:s,manyRefs:i,data:a}){const r=["sendAll",...a.config.options.ignore??[]];for(let[i,n]of Object.entries(a.changes)){if(r.includes(i)||e.isEmptyValue(n))continue;let a=Array.from(e.inputs.values()).find(e=>e.field?.dataset.field===i);if(!a)continue;let l=s.result.cloneNode(!0),o=l.querySelector("h3"),d=l.querySelector("p");const c=a.field?.querySelector("legend");o.textContent=c?c.textContent.replace("*","").trim():a.ui.label?.textContent.replace("*","").trim();const u=e.formatValueForSummary(n,a);u instanceof HTMLElement?d.replaceWith(u):d.textContent=u,t.append(l)}let n=a.config?.element?.querySelectorAll("[data-upload-field]");n&&n.forEach(e=>{let i=e.querySelector("h2")?.textContent??"Upload:",a=e.querySelectorAll(".item-grid.preview img"),r=s.result.cloneNode(!0);if(a){let e=s.result.cloneNode(!0),n=r.querySelector("h3"),l=r.querySelector("p");l?.remove(),n&&(n.textContent=i),a.forEach(t=>{t=t.cloneNode(!0),e.append(t)}),t.append(e)}}),s.result?.remove(),a.config.element.after(t),window.fade(a.config.element,!1)}})}initializeFields(e,t=null){const s={"[data-editor]":()=>this.checkForQuill(e,t),"div.quantity":()=>this.checkForQuantity(e),".repeater":()=>this.checkForRepeaters(e,t),".field.tag-list":()=>this.checkForTagLists(e),"[data-depends-on]":()=>this.checkForConditionalFields(e),"[data-limit]":()=>this.checkForCharacterLimits(e),"[data-uploader],[data-upload-field]":()=>this.checkForImageUploads(e,t),"nav.tabs":()=>this.checkForTabs(e,t),'[data-type="selector"]':()=>this.checkForSelectors(e)};for(const[t,i]of Object.entries(s))e.querySelector(t)&&i();Array.from(e.querySelectorAll(this.inputSelectors)).filter(e=>!e.closest(".ql-clipboard")).map(e=>{this.getItem(e,t?.id)})}checkForQuill(e,t){if(!e.querySelector("[data-editor]"))return;t&&!Object.hasOwn(t,"hasQuill")&&(t.hasQuill=!0,this.forms.set(t.id,t)),this.quillInstances.has(t.id)||this.quillInstances.set(t.id,new Set);window.jvbQuill(e).forEach(e=>{this.quillInstances.get(t.id).add(e)})}checkForQuantity(e){e.querySelector(this.selectors.number.number)&&e.querySelectorAll(this.selectors.number.number).forEach(t=>{let s={id:window.generateID("quant"),form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.number,t),element:t};t.dataset.numId=s.id,this.quantityFields.set(s.id,s),this.addQuantityListeners(t)})}addQuantityListeners(e){e.addEventListener("click",this.quantityClick)}removeQuantityListeners(e){e.removeEventListener("click",this.quantityClick)}handleQuantityClick(e){let t=this.quantityFields.get(e.target.closest("[data-num-id]")?.dataset.numId);if(!t)return;let s=0;if(t.ui.increase.contains(e.target)?s++:t.ui.decrease.contains(e.target)&&s--,0===s)return;this.getField(e.target);let i=t.ui.input.step;i=Math.max(i,1),e.ctrlKey&&e.shiftKey?i*=50:e.ctrlKey?i*=5:e.shiftKey&&(i*=10);let a=""===t.ui.input.value?0:parseFloat(t.ui.input.value);t.ui.input.value=a+i*s,a=parseFloat(t.ui.input.value),t.ui.input.min&&a<t.ui.input.min?(t.ui.input.value=t.ui.input.min,t.ui.decrease.disabled=!0):t.ui.input.max&&a>t.ui.input.max?(t.ui.input.value=t.ui.input.max,t.ui.increase.disabled=!0):(t.ui.decrease.disabled&&(t.ui.decrease.disabled=!1),t.ui.increase.disabled&&(t.ui.increase.disabled=!1))}checkForRepeaters(e){e.querySelector(this.selectors.repeater.repeater)&&e.querySelectorAll(this.selectors.repeater.repeater).forEach(t=>{let s={id:t.querySelector("template").className??window.generateID("repeater"),ui:window.uiFromSelectors(this.selectors.repeater,t),form:e.dataset.formId,element:t,field:this.getField(t),sortable:!1,rows:[]};if(!s.ui.add)return;let i=t.querySelector("template");this.templates.define(i.className,{manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach(t=>{window.prefixInput(t,`${a.repeater.dataset.field}:${r}:`,e,!1,!0)})}}),window.Sortable&&(s.sortable=new Sortable(s.ui.items,{animation:150,draggable:this.selectors.repeater.row,handle:this.selectors.repeater.handle,ghostClass:"repeater-row-ghost",chosenClass:"repeater-row-chosen",dragClass:"dragging",onEnd:()=>{this.reindexList(s.ui.items)}})),t.dataset.repeaterId=s.id,this.addRepeaterListeners(t),this.repeaters.set(s.id,s)})}addRepeaterListeners(e){e.addEventListener("click",this.repeaterClick)}removeRepeaterListeners(e){e.removeEventListener("click",this.repeaterClick)}handleRepeaterClick(e){e.target.matches(this.selectors.repeater.add)?this.addRepeaterRow(e.target.closest("[data-repeater-id]")):e.target.matches(this.selectors.repeater.remove)&&this.removeRepeaterRow(e.target.closest("[data-index]"))}addRepeaterRow(e){let t={};t.repeater=e;let s=this.repeaters.get(e.dataset.repeaterId),i=this.templates.create(e.dataset.repeaterId,t);s.rows.push({element:i,fields:Array.from(i.querySelectorAll("[data-field]"))}),this.repeaters.set(s.id,s),s.ui.items.append(i);let a=this.getForm(e);this.initializeFields(e,a),this.reindexList(s.ui.items),this.a11y.announce("Row added")}removeRepeaterRow(e){let t=e.closest("[data-repeater-id]");e.remove();let s=this.repeaters.get(t);s&&this.reindexList(s.ui.items),this.a11y.announce("Row removed")}checkForTagLists(e){e.querySelectorAll(this.selectors.tagList.tagList)?.forEach(t=>{let s={id:t.querySelector("template").className??window.generateID("tagList"),ui:window.uiFromSelectors(this.selectors.tagList,t),element:t,form:e.dataset.formId,format:t.dataset.tagFormat??"first_field"};if(!s.ui.input||!s.ui.add||!s.ui.items)return;t.dataset.tagListId=s.id,s.fieldName=t.dataset.field;let i=t.querySelector("template");this.templates.define(i.className,{refs:{label:this.selectors.tagList.label},manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach(e=>{let t=e.closest(".tag-item");window.prefixInput(e,`${a.fieldName}:${r}:`,t,!1,!0)}),t.label&&(t.label.textContent=a.label)}}),s.ui.inputs=Array.from(t.querySelectorAll(this.selectors.tagList.inputs)),s.ui.value=Array.from(t.querySelectorAll(this.selectors.tagList.value)),this.tagLists.set(s.id,s),this.addTagListListeners(t)})}addTagListListeners(e){e.addEventListener("click",this.tagListClick),e.addEventListener("keypress",this.tagListInput)}removeTagListListeners(e){e.removeEventListener("click",this.tagListClick),e.removeEventListener("keypress",this.tagListInput)}handleTagListClick(e){window.targetCheck(e,this.selectors.tagList.add)?this.addTagListItem(e.target.closest("[data-tag-list-id]")):window.targetCheck(e,this.selectors.tagList.remove)&&this.removeTagListItem(e.target.closest(this.selectors.tagList.item))}addTagListItem(e){let t=this.tagLists.get(e.dataset.tagListId);if(!t)return;let s,i={},a=!1,r=!0;for(let e of t.ui.inputs){const t=e.required||"true"===e.dataset.required,s=this.getFieldValue(e);s&&(a=!0);const n=this.validateField(e);t&&!s?(this.showError(e,"This field is required"),r=!1):n||(r=!1);const l=e.name.replace("new_","");i[l]=s}if(!r){this.a11y.announce("Please correct the errors before adding");const e=t.ui.inputs.find(e=>(e.required||"true"===e.dataset.required)&&!this.getFieldValue(e));return void(e&&e.focus())}if(!a)return this.a11y.announce("Please fill in at least one field"),void t.ui.inputs[0].focus();switch(t.format){case"first_field":s=Object.values(i)[0];break;case"all_fields":s=Object.values(i).join(", ");break;default:if(t.format.includes("{")){s=t.format;for(const[e,t]of Object.entries(i))s=s.replace(`{${e}}`,t)}else s=i[t.format]??Object.values(i)[0]}let n=this.templates.create(e.dataset.tagListId,{label:s,fieldName:t.fieldName});const l=t.ui.items?.children?.length??0;n?.querySelectorAll("input[type=hidden]")?.forEach(e=>{const s=e.dataset.field;e.name=`${t.fieldName}:${l}:${s}`,e.id=`${t.fieldName}:${l}:${s}`,e.value=i[s]||""}),t.ui.items.append(n);for(let e of t.ui.inputs)["checkbox","radio"].includes(e.type)?e.checked=!1:e.value="",this.clearValidation(e);t.ui.inputs[0]?.focus(),this.updateCollectionField(e),this.a11y.announce("Item added")}removeTagListItem(e){let t=e.closest("[data-tag-list-id]");t&&(e.remove(),this.reindexList(t),this.updateCollectionField(t),this.a11y.announce("Item removed"))}handleTagListInput(e){let t=e.target,s=t.closest("[data-tag-list-id]");if(!s)return;let i=this.tagLists.get(s.dataset.tagListId);if(i&&"Enter"===e.key)if(t===i.ui.inputs[i.ui.inputs.length-1])e.preventDefault(),this.addTagListItem(t.closest("[data-tag-list-id]"));else{e.preventDefault();let s=i.ui.inputs.indexOf(t);i.ui.inputs[s+1].focus()}}checkForConditionalFields(e){e.querySelectorAll(this.selectors.dependsOn).forEach(t=>{const s=t.dataset.dependsOn,i=t.dataset.dependsValue,a=t.dataset.dependsOperatior??"==";let r=this.forms.get(e.dataset.formId);this.dependencies.has(s)||Object.hasOwn(r.ui.fields,s)&&this.dependencies.set(s,[]);let n=this.dependencies.get(s);n&&(n.push({field:t,form:e.dataset.formId,requiredValue:i,operator:a}),this.dependencies.set(s,n)),this.checkFieldDependency(t,s)})}checkFieldDependency(e,t){const s=this.getForm(e);if(!this.dependencies.get(t))return;const i=this.getFieldValue(s.ui.fields[t]),a=this.evaluateCondition(i,e.dataset.dependsValue,e.dataset.dependsOperatior);this.toggleFieldVisibility(e,a)}evaluateCondition(e,t,s){const i=String(e||""),a=String(t||"");switch(s){case"==":default:return i===a;case"!=":return i!==a;case">":return parseFloat(i)>parseFloat(a);case"<":return parseFloat(i)<parseFloat(a);case">=":return parseFloat(i)>=parseFloat(a);case"<=":return parseFloat(i)<=parseFloat(a);case"contains":return i.includes(a);case"empty":return""===i;case"not_empty":return""!==i}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach(e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)}))}checkForCharacterLimits(e){e.querySelector(this.selectors.limits.hasLimit)&&(this.countUpdaters=this.updateCount.bind(this),e.querySelectorAll(this.selectors.limits.hasLimit).forEach(t=>{const s=this.getFieldInput(t);if(!s)return;let i=window.generateID("limit");s.dataset.charLimitId=i,s.dataset.limit=t.dataset.maxlength;let a={element:s,form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.limits,t)};a.ui.limit&&(a.ui.limit.textContent=t.dataset.maxlength),this.charLimits.set(i,a),this.addCharacterLimitListeners(s)}))}addCharacterLimitListeners(e){e.addEventListener("input",this.countUpdaters,{passive:!0})}removeCharacterLimitListeners(e){e.removeEventListener("input",this.countUpdaters,{passive:!0})}updateCount(e){let t=e.target,s=this.charLimits.get(t.dataset.charLimitId);if(!s)return;let i=t.value.length,a=t.dataset.limit;s.ui.current&&(s.ui.current.textContent=i,s.ui.current.classList.toggle("exceeded",i>=a)),i>a&&(t.value=t.value.slice(0,a))}checkForImageUploads(e,t){this.hasUploads=!0,window.jvbUploads.scanFields(e,t.options.autoUpload,t.options.imageMeta);let s=e.querySelectorAll('[data-field-type="upload"]');s&&(t.ui.uploads={},s.forEach(e=>{t.ui.uploads[e.dataset.field]=e}))}checkForTabs(e,t){window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=window.jvbTabs.registerTab(e,{preCheck:(e,t,s)=>this.validateStep(e,t,s)}),t.ui.tabs=window.uiFromSelectors(this.selectors.tabs,e),t.ui.tabs.sections=Array.from(e.querySelectorAll(this.selectors.tabs.sections)),t.ui.tabs.inputs={},t.ui.tabs.sections.forEach(e=>{t.ui.tabs.inputs[e.dataset.tab]=Array.from(e.querySelectorAll(this.inputSelectors))}),t.ui.tabs.buttons=Array.from(e.querySelectorAll(this.selectors.tabs.buttons)),t.unsubscribeTabs=window.jvbTabs.subscribe((e,s)=>{if("tab-switched"===e&&t.ui.tabs.progress){const e=t.ui.tabs.sections.filter(e=>e.dataset.tab===s.current)[0]??!1;if(!e)return;const i=e.dataset.step-1,a=t.ui.tabs.sections.length;window.showProgress(t.ui.tabs.progress,i,a,`Step ${i+1} of ${a}`)}}),this.forms.set(t.id,t))}validateStep(e,t,s){let i=!1;if(s.ui.sections&&s.ui.sections.forEach((a,r)=>{if(a===e){let e=r-1;if(e>=0){let a=s.ui.sections[e];a&&a.dataset.tab===t&&(i=!0)}}}),i)return!0;const a=e.closest("[data-form-id]")?.dataset.formId;if(!a)return!0;if(!this.forms.get(a))return!0;return Array.from(this.inputs.values()).filter(t=>t&&t.form===a&&t.section===e.dataset.tab&&!t.element.closest("[hidden]")).every(e=>!0===this.validateField(e.element))}checkForSelectors(e){window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}reindexList(e){let t=e.dataset.field||e.dataset.repeaterId||e.dataset.tagListId;if(!t){let s=e.closest("[data-field]");t=s.dataset.field||s.dataset.repeaterId||s.dataset.tagListId}Array.from(e.children).forEach((e,s)=>{e.dataset.index=`${s}`;let i=e.querySelector(".row-number");i&&(i.textContent=`#${s+1}`);let a=e.querySelector(".row-title"),r=a?a.dataset.label:null;e.querySelectorAll("input, select, textarea").forEach(i=>{if("file"===i.type)return;const n=Object.hasOwn(i.dataset,"field")?i.dataset.field:i.name.split(":").pop();n===r&&(a.textContent=window.escapeHtml(i.value)),window.prefixInput(i,`${t}:${s}:${n}`,e,!0,!0)})}),this.updateCollectionField(e)}updateCollectionField(e){const t=e.closest("[data-field]");if(!t)return;const s=t.dataset.fieldType;if(!["repeater","tag-list"].includes(s))return;const i=this.getForm(e);if(!i)return;const a=this.getFieldValue(t);this.updateItem(t.dataset.field,a,i)}maybeUpdateCollectionDisplay(e){let t=e.closest("[data-repeater-id],[data-tag-list-id]");if(t&&"repeater"===t.dataset.fieldType){let t=e.closest(".repeater-row");if(!t)return;let s=t.querySelector(".row-title");if(!s||!s.dataset.label)return;console.log(e.dataset.field.split(":").pop()),console.log(s.dataset.label),e.dataset.field.split(":").pop()===s.dataset.label&&(s.textContent=this.getFieldValue(e.querySelector(this.inputSelectors)))}}clearValidation(e){let t=this.getField(e);if(!t)return;let s=this.getItem(e);s&&(t.classList.remove("has-error","has-success"),s.ui.success&&(s.ui.success.hidden=!0),s.ui.error&&(s.ui.error.hidden=!0),s.ui.message&&(s.ui.message.hidden=!0,s.ui.message.textContent=""))}showError(e,t="Invalid field"){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-success"),s.classList.add("has-error"),s.scrollIntoView({behavior:"smooth",block:"center",inline:"nearest"}),s.querySelector(this.inputSelectors)?.focus(),i.ui.message&&(i.ui.message.hidden=!1,i.ui.message.textContent=t))}showSuccess(e,t=""){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-error"),s.classList.add("has-success"),i.ui.message&&(i.ui.message.hidden=""===t,i.ui.message.textContent=t))}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach(e=>e.remove()),e.querySelectorAll(".field-error").forEach(e=>e.classList.remove("field-error")),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const i=window.getIcon?.("check-circle");i&&(i.classList.add("success-icon"),s.prepend(i))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach(e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)})}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch(e=>{console.warn("Failed to clear form cache:",e)});const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully")}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach(e=>e.remove()),e.querySelectorAll(".field-error, .has-error").forEach(e=>{e.classList.remove("field-error","has-error")}),e.querySelectorAll(".field").forEach(e=>{this.clearValidation(e)}),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const i=window.getIcon?.("close-circle");i&&(i.classList.add("error-icon"),s.prepend(i)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}showFormStatus(e,t,s=""){let i=this.forms.get(e);i&&i.options.showStatus&&i.ui?.status?.status&&i.status!==t&&(i.status=t,i.ui.status.status.hidden=!1,i.ui.status.status.classList.toggle("loading",["uploading","saving"].includes(t)),i.ui.status.message.textContent=""===s?this.getDefaultMessage(t):s,i.ui.status.icon.className="icon icon-"+this.getDefaultIcon(t),setTimeout(()=>i.ui.status.status.hidden=!0,"submitted"===t?3e3:1e4))}getDefaultMessage(e){return{saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"}[e]??e}getDefaultIcon(e){return{autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[e]??""}showSummary(e){let t=this.templates.create("formSummary",e);e.config.element.after(t),window.fade(e.config.element,!1)}getForm(e){let t=e.closest("[data-form-id]");if(!t)return!1;let s=t.dataset.formId;if(!s)return!1;let i=this.forms.get(s);return i||!1}getField(e){return e.closest("[data-field]")}getFieldType(e){let t=this.getField(e);if(t)return t.dataset.fieldType}getFieldValue(e){let t=this.getFieldType(e),s=this.getItem(e),i=s.field?.dataset.field??!1;if(!i)return!1;switch(t){case"repeater":return this.getRepeaterValue(e,s);case"tag-list":return this.getTagListValue(e,s);case"group":return null;case"location":return this.getLocationValue(e,s);case"selector":case"upload":case"gallery":case"image":return this.getHiddenInputValue(e,s,i);case"true-false":case"toggle-text":return e.checked;case"checkbox":return e.name.endsWith("[]")?this.getCheckboxGroupValue(e,s):e.checked?e.value:"";default:return e.value}}getCheckboxGroupValue(e,t){return t.checkboxGroup||(t.checkboxGroup=t.field?.querySelectorAll(`input[type="checkbox"][name="${e.name}"]`),this.saveItem(t)),Array.from(t.checkboxGroup).filter(e=>e.checked).map(e=>e.value)}getFieldCheckedValue(e){if("checkbox"===e.type){return"true-false"===this.getFieldType(e)?e.checked:e.checked?e.value:""}if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`),s=Array.from(t).find(e=>e.checked);return s?s.value:""}return this.getFieldValue(e)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getRepeaterValue(e,t){const s=e.querySelector(".repeater-items");if(!s)return[];let i=["image_data","image-title","image-caption","image-description","image-alt-text"],a=[];return Array.from(s.children).forEach(e=>{let t={};e.querySelectorAll('input[type="hidden"]').forEach(e=>{t[e.name]=e.value}),e.querySelectorAll("[data-field]").forEach(e=>{if(!i.includes(e.dataset.field)){const s=this.getFieldInput(e);s&&(t[e.dataset.field]=this.getFieldValue(s))}}),a.push(t)}),a}getFieldInput(e){const t=e.querySelector("textarea[data-editor]");return t||e.querySelector(this.inputSelectors)}getTagListValue(e,t){t.container||(t.container=t.field?.querySelector(".tag-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach(e=>{let t=e.querySelectorAll('input[type="hidden"]'),i={};t.forEach(e=>{i[e.dataset.field]=e.value}),s.push(i)}),s}getLocationValue(e,t){t.values||(t.values=Array.from(t.field?.querySelectorAll("[data-location-field]")),this.saveItem(t));let s={};return t.values.forEach(e=>{s[e.dataset.locationField]=e.value}),s}getHiddenInputValue(e,t,s){return"INPUT"===e.tagName&&"hidden"===e.type||(e=e.querySelector('input[type="hidden"][name="'+s+'"]'))?(void 0!==t.value&&t.value===e.value||(t.value=e.value,this.saveItem(t)),t.value):null}formatValueForSummary(e,t){const s=this.getFieldType(t.element);if(this.isEmptyValue(e))return"";switch(s){case"repeater":return this.formatRepeaterForSummary(e,t);case"tag-list":return this.formatTagListForSummary(e,t);case"location":return this.formatLocationForSummary(e);case"true-false":return e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatCheckboxGroupForSummary(e,t):this.getDisplayLabel(t,e);case"selector":case"upload":case"image":case"gallery":return this.formatHiddenFieldForSummary(e,t,s);default:return"string"==typeof e?this.getDisplayLabel(t,e):"string"==typeof e&&e.includes("\n")?this.convertLineBreaks(e):e}}formatCheckboxGroupForSummary(e,t){return e.map(e=>this.getDisplayLabel(t,e)).join(", ")}convertLineBreaks(e){const t=document.createElement("span");return t.innerHTML=e.split("\n").join("<br>"),t}formatRepeaterForSummary(e,t){const s=document.createElement("div");return s.className="summary-repeater",e.forEach((e,i)=>{const a=document.createElement("div");a.className="summary-repeater-row";const r=document.createElement("strong");r.textContent=`Entry ${i+1}:`,a.appendChild(r);const n=document.createElement("ul");n.className="summary-repeater-fields";for(const[s,i]of Object.entries(e)){if(this.isEmptyValue(i))continue;const e=document.createElement("li"),a=t.field?.querySelector(`[data-field="${s}"]`),r=a?.closest(".field")?.querySelector("label")?.textContent.replace("*","").trim()||s;e.innerHTML=`<span class="field-label">${r}:</span> <span class="field-value">${i}</span>`,n.appendChild(e)}a.appendChild(n),s.appendChild(a)}),s}formatTagListForSummary(e,t){const s=document.createElement("div");s.className="summary-taglist";const i=document.createElement("ul");return i.className="summary-tags",e.forEach(e=>{const t=document.createElement("li");t.className="summary-tag";const s=Object.values(e).find(e=>!this.isEmptyValue(e))||"",a=Object.entries(e).filter(([e,t])=>!this.isEmptyValue(t));a.length>1?t.textContent=a.map(([e,t])=>t).join(", "):t.textContent=s,i.appendChild(t)}),s.appendChild(i),s}formatLocationForSummary(e){const t=[];return e.street&&t.push(e.street),e.city&&t.push(e.city),e.province&&t.push(e.province),e.postal_code&&t.push(e.postal_code),e.country&&t.push(e.country),t.length>0?t.join(", "):e.address||""}formatHiddenFieldForSummary(e,t,s){if(["upload","gallery","image"].includes(s)){const s=t.field?.querySelector("[data-upload-field]");if(s){const e=s.querySelectorAll(".item-grid.preview img");if(e.length>0){const t=document.createElement("div");return t.className="summary-uploads",e.forEach(e=>{const s=e.cloneNode(!0);s.style.maxWidth="100px",s.style.maxHeight="100px",t.appendChild(s)}),t}}return`${e.split(",").length} file(s) uploaded`}return e}getDisplayLabel(e,t){if(!e.element)return t;const s=e.element.type;if("radio"===s){const s=e.field.querySelectorAll(`input[type="radio"][name="${e.element.name}"]`),i=Array.from(s).find(e=>e.value===t);if(i){const t=i.closest("label")||e.field.querySelector(`label[for="${i.id}"]`);if(t)return t.textContent.replace("*","").trim()}}if("checkbox"===s&&"true-false"!==this.getFieldType(e.element)){const s=e.field.querySelector(`input[type="checkbox"][value="${t}"]`);if(s){const t=s.closest("label")||e.field.querySelector(`label[for="${s.id}"]`);if(t){const e=t.querySelector("span");return e?e.textContent.trim():t.textContent.replace("*","").trim()}}}return t}getItem(e,t=null){const s=Object.hasOwn(e.dataset,"ref");let i=s?e.dataset.ref:window.generateID("input");if(s||(e.dataset.ref=i),!this.inputs.has(i)){t||(t=e.closest("[data-form-id]")?.dataset.formId??!1);let s=this.getField(e);this.inputs.set(i,{id:i,element:e,form:t,field:s,section:e.closest("[data-tab]")?.dataset.tab??!1,ui:window.uiFromSelectors(this.selectors.fields,s)})}return this.inputs.get(i)}saveItem(e){this.inputs.set(e.id,e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach(s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}})}destroy(){this.forms.size>0&&(Array.from(this.forms.values()).forEach(e=>{this.removeFormListeners(e)}),this.forms.clear()),this.repeaters.size>0&&(Array.from(this.repeaters.values()).forEach(e=>{this.removeRepeaterListeners(e.element),e.sortable?.destroy()}),this.repeaters.clear()),this.quantityFields.size>0&&(Array.from(this.quantityFields.values()).forEach(e=>{this.removeQuantityListeners(e.element)}),this.quantityFields.clear()),this.tagLists.size>0&&(Array.from(this.tagLists.values()).forEach(e=>{this.removeTagListListeners(e.element)}),this.tagLists.clear()),this.charLimits.size>0&&Array.from(this.charLimits.values()).forEach(e=>{e.element.removeEventListener("input",this.countUpdaters)}),this.inputs.clear(),this.forms.clear(),this.charLimits.clear()}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.jvbForm=new e)})})})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.queue=window.jvbQueue,this.populate=window.jvbPopulate,this.changes=new Map,this.forms=new Map,this.inputs=new Map,this.repeaters=new Map,this.tagLists=new Map,this.charLimits=new Map,this.quantityFields=new Map,this.quillInstances=new Map,this.dependencies=new Map,this.subscribers=new Set,this.isRestoring=!1,this.hasListeners=!1,this.hasUploads=!1,this.summaryTemplate=!1,this.init()}init(){this.templates=window.jvbTemplates,this.defineSummaryTemplate(),this.initElements(),this.initListeners(),this.initStore(),this.initValidators(),this.initUploadSubscription()}initUploadSubscription(){window.jvbUploads.subscribe((e,t)=>{if(this.hasUploads&&"upload-received"===e){let e=this.getForm(t.field);e&&this.updateItem(`${t.field.dataset.field}_tempUpload`,t.id,e)}})}initElements(){this.inputSelectors="input, textarea, select",this.selectors={tabs:{nav:"nav.tabs",sections:".tab-content",progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},buttons:"nav.tabs button"},dependsOn:"[data-depends-on]",forms:{status:{status:".fstatus",message:".fstatus .message",icon:".fstatus .icon",actions:".fstatus .actions"},restore:{container:".restore-form",restore:'[data-action="restore"]',clear:'[data-action="clear"]'}},inputs:this.inputSelectors,fields:{field:".field",label:"label",success:".success",error:".error",message:".validation-message"},repeater:{repeater:".repeater",row:".repeater-row",handle:".drag-handle",header:".repeater-row-header",remove:".remove-row",add:".add-repeater-row",template:"template",items:".repeater-items",inputs:this.inputSelectors},tagList:{tagList:".field.tag-list",input:".row",add:".add-tag",remove:".remove-tag",label:".tag-label",items:".tag-items",item:".tag-item",inputs:this.inputSelectors,value:'input[type="hidden"]'},tag:{label:".tag-label"},number:{number:".field div.quantity",increase:"button.increase",decrease:"button.decrease",input:'input[type="number"]'},limits:{hasLimit:"[data-maxlength]",limit:".limit",current:".current"}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.blurHandler=this.handleBlur.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.quantityClick=this.handleQuantityClick.bind(this),this.repeaterClick=this.handleRepeaterClick.bind(this),this.tagListClick=this.handleTagListClick.bind(this),this.tagListInput=this.handleTagListInput.bind(this)}addFormListeners(e){e.addEventListener("click",this.clickHandler),e.addEventListener("change",this.changeHandler),e.addEventListener("input",this.inputHandler),e.addEventListener("blur",this.blurHandler),e.addEventListener("submit",this.submitHandler)}removeFormListeners(e){e.removeEventListener("click",this.clickHandler),e.removeEventListener("change",this.changeHandler),e.removeEventListener("input",this.inputHandler),e.removeEventListener("blur",this.blurHandler),e.removeEventListener("submit",this.submitHandler)}initStore(){const e=window.jvbStore.register("forms",{storeName:"forms",keyPath:"id",indexes:[{name:"src",keyPath:"src"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4});this.store=e.forms,this.store.subscribe((e,t)=>{if("data-ready"===e){let e=this.store.getFiltered().filter(e=>e.src===window.location.pathname);for(let t of e)this.showPendingNotification(t.id,t.changes)}else"operation-status"===e&&"completed"===t.status&&t.config&&this.store.delete(t.config.id)})}showPendingNotification(e,t){let s=this.forms.get(e);if(!s)return;let i=s.element;if(!i)return void console.warn(`Form element not found for: ${e}`);s.ui.restore.container.hidden=!1;const a=async(e,t)=>{this.isRestoring=!0;let i={fields:e};await this.checkStoredUploads(e,t),this.populate.populate(t,i),this.a11y.announce("Previous changes restored"),this.isRestoring=!1,s.ui.restore.container.remove()},r=async e=>{await this.checkStoredUploads(t,i,!1),await this.store.delete(e),this.a11y.announce("Previous changes discarded"),s.ui.restore.container.remove()};s.ui.restore.restore.addEventListener("click",()=>a(t,i)),s.ui.restore.clear.addEventListener("click",async()=>r(e))}async checkStoredUploads(e,t,s=!0){let i=this.forms.get(t.dataset.formId);if(!i)return;let a=[];for(let[t,s]of Object.entries(e))if(t.includes("_tempUpload")){let e=t.replace("_tempUpload","");Object.hasOwn(i.ui.uploads,e)&&(a=[...a,...s])}a.length>0&&(s?await window.jvbUploads.restoreUploads(a):await window.jvbUploads.clearUploads(a))}initValidators(){this.validators={email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-+().]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const i=t.dataset.min,a=t.dataset.max;return void 0!==i&&s<parseFloat(i)?`Value must be at least ${i}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,i=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(i&&e.length>parseInt(i))||`Must be no more than ${i} characters`}}}}validateField(e){const t=this.performValidation(e);return this.updateValidationUI(e,t),t.isValid}performValidation(e){const t=e.closest(".field"),s=this.getFieldCheckedValue(e);if(!s&&!e.required)return{isValid:!0,message:""};if(e.required)if("checkbox"===e.type){if(!e.checked)return{isValid:!1,message:"This field is required"}}else if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`);if(!Array.from(t).some(e=>e.checked))return{isValid:!1,message:"Please select an option"}}else if(!s)return{isValid:!1,message:"This field is required"};if(e.checkValidity&&!e.checkValidity())return{isValid:!1,message:e.validationMessage};if(s&&Object.hasOwn(t.dataset,"pattern")){if(!new RegExp(t.dataset.pattern).test(s))return{isValid:!1,message:t.dataset.validationMessage||"Invalid format"}}if(Object.hasOwn(t.dataset,"validate")||e.type){const i=this.validators[t.dataset.validate||e.type];if(i&&i.pattern&&!i.pattern.test(s))return{isValid:!1,message:i.message};if(i&&i.test){const e=i.test(s,t);if(!0!==e)return{isValid:!1,message:e}}}return{isValid:!0,message:""}}updateValidationUI(e,t){t.isValid?this.showSuccess(e,t.message):this.showError(e,t.message)}handleClick(e){let t=this.getForm(e.target);if(!t)return;const s=window.targetCheck(e,"[data-action]");if(s){switch(s.dataset.action){case"clear-form":this.store.delete(t.id),t.element.reset(),t.ui.status.status.hidden=!0,this.a11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":t.ui.status.status.hidden=!0;break;case"next-step":e.preventDefault(),t.tabs.ui.buttons.forEach((e,s)=>{if(e.dataset.tab===t.tabs.activeTab){let e=t.tabs.ui.buttons[s+1]??!1;e&&window.jvbTabs.switchTab(e.dataset.tab,window.jvbTabs.getConfig(e))}});break;case"prev-step":e.preventDefault(),t.tabs.ui.buttons.forEach((e,s)=>{if(e.dataset.tab===t.tabs.activeTab){let e=t.tabs.ui.buttons[s-1]??!1;e&&window.jvbTabs.switchTab(e.dataset.tab,window.jvbTabs.getConfig(e))}})}}}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getField(e.target);const s=e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');if(s){if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).forEach(e=>{this.checkFieldDependency(e,t.dataset.field)})}const e=s.dataset.field;return this.maybeUpdateCollectionDisplay(t),void window.debouncer.schedule(`collection:${e}`,()=>this.updateCollectionField(s),150)}if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).forEach(e=>{this.checkFieldDependency(e,t.dataset.field)})}let i=this.getForm(e.target);this.updateItem(t.dataset.field,this.getFieldValue(e.target),i)}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target).dataset.field;window.debouncer.cancel(`form:${t.id}:validate:${s}`),this.validateField(e.target);const i=e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');i?this.updateCollectionField(i):this.updateItem(s,this.getFieldValue(e.target),t)}handleInput(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target);if(!s)return;const i=e.target,a=s.dataset.field;this.showFormStatus(t.id,"pending"),window.debouncer.schedule(`form:${t.id}:validate:${a}`,()=>this.validateField(i),500)}async handleSubmit(e){let t=this.getForm(e.target);if(t){if(this.subscribers.size>0)if(e.preventDefault(),t.options.cache){this.cancelBackup(),await this.backup();const e=await this.store.get(t.id);this.notify("form-submit",{config:t,data:e.changes})}else this.notify("form-submit",{config:t,data:this.changes.get(t.id)?.changes??{}});if(t.options.endpoint&&!t.options.handled&&(e.preventDefault(),console.log("Submitting form...",this.changes.get(t.id)?.changes??{}),await this.handleServerSave(t,this.changes.get(t.id)?.changes??{})),t.options.showSummary){const e=await this.store.get(t.id);this.showSummary({config:t,changes:e?.changes})}}}updateItem(e,t,s){if(void 0===t)return;this.changes.has(s.id)||this.changes.set(s.id,{id:s.id,timestamp:Date.now(),src:window.location.pathname,changes:{}});let i=this.changes.get(s.id);e.includes("_tempUpload")?(Object.hasOwn(i.changes,e)||(i.changes[e]=[]),i.changes[e].push(t)):i.changes[e]=t,s.tabs&&this.updateStepProgress(s),this.changes.set(s.id,i),s.options.cache&&this.scheduleBackup(),s.options.endpoint&&!s.options.handled&&this.scheduleServerSave(s,i)}updateStepProgress(e){if(console.log("First check in updateStepProgress"),!e.tabs)return;let t=this.getCurrentStep(e);if(console.log("Current step: ",t),!t)return;t-=1;let s=this.getCurrentSection(e);if(console.log("Current section: ",s),!s)return;let i=0,a=0;if(s.childNodes.forEach(e=>{if(console.log("Child node: ",e),e.classList.contains("field")){i++;let t=this.getFieldValue(e.querySelector(this.inputSelectors));console.log("Value",t),t.length>0&&a++}}),0===i)return;let r=t+a/i,n=e.ui.tabs.sections.length;window.showProgress(e.ui.tabs.progress,r,n,`Step ${t+1} of ${n}`)}getCurrentStep(e){let t=null;return e.tabs.ui.buttons.forEach(s=>{s.dataset.tab===e.tabs.activeTab&&(t=s.dataset.step)}),t}getCurrentSection(e){console.log("Getting current section: ",e.tabs.ui.sections);let t=null;return e.tabs.ui.sections.forEach(s=>{s.dataset.tab===e.tabs.activeTab&&(t=s)}),t}scheduleBackup(){window.debouncer.schedule("form_changes",async()=>{this.changes.size>0&&await this.backup()},2e3)}cancelBackup(){window.debouncer.cancel("form_changes")}async backup(){const e=new Map;for(let[t,s]of this.changes.entries()){const i=await this.store.get(t);i?e.set(t,{...i,...s,changes:{...i.changes,...s.changes},timestamp:Date.now()}):e.set(t,s)}await this.store.saveMany(e);for(let e of this.changes.keys())this.showFormStatus(e,"autosaved");this.changes.clear()}saveCache(e){if(!this.changes.has(e))return;let t=this.changes.get(e);0!==t.size&&(this.store.save(t).then(()=>{}),this.changes.delete(e))}scheduleServerSave(e,t){window.debouncer.schedule(`form_${e.id}_server_save`,async()=>{await this.handleServerSave(e,t)},1500)}cancelServerSave(e){window.debouncer.cancel(`form_${e}_server_save`)}async handleServerSave(e,t){if(this.cancelServerSave(e.id),e.useQueue&&window.jvbQueue)t.user=window.auth.getUser(),window.jvbQueue.addToQueue({endpoint:e.options.endpoint,data:t,headers:e.options.headers??{}});else try{let s=await window.auth.fetch(jvbSettings.api+e.options.endpoint,{body:t,headers:e.options.headers??{},method:"POST"});if(!s.ok){let t;try{t=await s.json()}catch{t={message:s.statusText||"Unknown HTTP error"}}return void this.notify("form-error",{config:e,data:t})}let i=await s.json();this.notify("form-success",{config:e,result:i})}catch(t){t.status?this.notify("form-error",{config:e,data:t}):t instanceof SyntaxError?(this.notify("form-error",{config:e,data:{status:"error",success:!1,message:"Something went wrong"}}),console.error("Data Error: Server returned invalid JSON format.")):this.notify("form-error",{config:e,data:{status:"network_error",message:"Network error: could not connect to the server"}})}}registerForm(e,t){if(t={autoUpload:!1,imageMeta:!0,delay:1500,useQueue:!0,endpoint:Object.hasOwn(e.dataset,"save")?e.dataset.save:"",showStatus:!0,showSummary:!1,handled:Object.hasOwn(e.dataset,"handled"),cache:!0,ignore:[],...t},Object.hasOwn(e.dataset,"formId")&&this.forms.has(e.dataset.formId))return;Object.hasOwn(e.dataset,"formId")||(e.dataset.formId=window.generateID("form_")),Object.hasOwn(e.dataset,"action")&&(t.headers=window.auth.getHeader(e.dataset.action));const s=e.dataset.formId;this.addFormListeners(e);const i={element:e,id:s,status:"",options:t,ui:window.uiFromSelectors(this.selectors.forms,e)};return i.ui.fields={},e.querySelectorAll("[data-field]").forEach(e=>{i.ui.fields[e.dataset.field]=e}),this.initializeFields(e,i),this.forms.set(s,i),i}clearForm(e){const t=this.forms.get(e);if(!t)return;t.unsubscribeTabs&&t.unsubscribeTabs(),t.tabs&&window.jvbTabs.removeTab(t.element),t.cache&&this.changes.has(e)&&this.saveCache(e);for(let[t,s]of this.inputs.entries())s.form===e&&this.inputs.delete(t);if(this.dependencies.forEach((t,s)=>{0===(t=t.filter(t=>t.form!==e)).length&&this.dependencies.delete(s)}),Object.hasOwn(t,"hasQuill")&&this.quillInstances.has(e)){this.quillInstances.get(e).forEach(e=>{e.disable(),e.off("text-change"),e.off("selection-change");const t=e.container.parentElement,s=t?.querySelector(".ql-toolbar");if(s&&s.remove(),e.setText(""),t&&t.classList.contains("editor-container")){const e=t.nextElementSibling;"TEXTAREA"===e?.tagName&&(e.style.display=""),t.remove()}}),this.quillInstances.delete(e)}let s={repeater:this.repeaters,tagList:this.tagLists,charLimit:this.charLimits,quantity:this.quantityFields};for(let[t,i]of Object.entries(s)){if(0===i.size)continue;let s=Array.from(i.values()).filter(t=>t.form===e);s.length>0&&s.forEach(e=>{switch(t){case"repeater":this.removeRepeaterListeners(e.element);break;case"tagList":this.removeTagListListeners(e.element);break;case"charLimit":this.removeCharacterLimitListeners(e.element);break;case"quantity":this.removeQuantityListeners(e.element)}i.has(e.id)&&i.delete(e.id)})}this.removeFormListeners(t.element),this.forms.delete(e),window.debouncer.cancel("form_changes")}defineSummaryTemplate(){this.summaryTemplate=!0;let e=this;this.templates.define("formSummary",{refs:{result:".result",h3:"h3",p:"p"},setup({el:t,refs:s,manyRefs:i,data:a}){const r=["sendAll",...a?.config?.options?.ignore??[]];for(let[i,n]of Object.entries(a.changes)){if(r.includes(i)||e.isEmptyValue(n))continue;let a=Array.from(e.inputs.values()).find(e=>e.field?.dataset.field===i);if(!a)continue;let o=s.result.cloneNode(!0),l=o.querySelector("h3"),d=o.querySelector("p");const c=a.field?.querySelector("legend");l.textContent=c?c.textContent.replace("*","").trim():a.ui.label?.textContent.replace("*","").trim();const u=e.formatValueForSummary(n,a);u instanceof HTMLElement?d.replaceWith(u):d.textContent=u,t.append(o)}let n=a.config?.element?.querySelectorAll("[data-upload-field]");n&&n.forEach(e=>{let i=e.querySelector("h2")?.textContent??"Upload:",a=e.querySelectorAll(".item-grid.preview img"),r=s.result.cloneNode(!0);if(a){let e=s.result.cloneNode(!0),n=r.querySelector("h3"),o=r.querySelector("p");o?.remove(),n&&(n.textContent=i),a.forEach(t=>{t=t.cloneNode(!0),e.append(t)}),t.append(e)}}),s.result?.remove()}})}initializeFields(e,t=null){const s={"[data-editor]":()=>this.checkForQuill(e,t),"div.quantity":()=>this.checkForQuantity(e),".repeater":()=>this.checkForRepeaters(e,t),".field.tag-list":()=>this.checkForTagLists(e),"[data-depends-on]":()=>this.checkForConditionalFields(e),"[data-limit]":()=>this.checkForCharacterLimits(e),"[data-uploader],[data-upload-field]":()=>this.checkForImageUploads(e,t),"nav.tabs":()=>this.checkForTabs(e,t),'[data-type="selector"]':()=>this.checkForSelectors(e)};for(const[t,i]of Object.entries(s))e.querySelector(t)&&i();Array.from(e.querySelectorAll(this.inputSelectors)).filter(e=>!e.closest(".ql-clipboard")).map(e=>{this.getItem(e,t?.id)})}checkForQuill(e,t){if(!e.querySelector("[data-editor]"))return;t&&!Object.hasOwn(t,"hasQuill")&&(t.hasQuill=!0,this.forms.set(t.id,t)),this.quillInstances.has(t.id)||this.quillInstances.set(t.id,new Set);window.jvbQuill(e).forEach(e=>{this.quillInstances.get(t.id).add(e)})}checkForQuantity(e){e.querySelector(this.selectors.number.number)&&e.querySelectorAll(this.selectors.number.number).forEach(t=>{let s={id:window.generateID("quant"),form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.number,t),element:t};t.dataset.numId=s.id,this.quantityFields.set(s.id,s),this.addQuantityListeners(t)})}addQuantityListeners(e){e.addEventListener("click",this.quantityClick)}removeQuantityListeners(e){e.removeEventListener("click",this.quantityClick)}handleQuantityClick(e){let t=this.quantityFields.get(e.target.closest("[data-num-id]")?.dataset.numId);if(!t)return;let s=0;if(t.ui.increase.contains(e.target)?s++:t.ui.decrease.contains(e.target)&&s--,0===s)return;this.getField(e.target);let i=t.ui.input.step;i=Math.max(i,1),e.ctrlKey&&e.shiftKey?i*=50:e.ctrlKey?i*=5:e.shiftKey&&(i*=10);let a=""===t.ui.input.value?0:parseFloat(t.ui.input.value);t.ui.input.value=a+i*s,a=parseFloat(t.ui.input.value),t.ui.input.min&&a<t.ui.input.min?(t.ui.input.value=t.ui.input.min,t.ui.decrease.disabled=!0):t.ui.input.max&&a>t.ui.input.max?(t.ui.input.value=t.ui.input.max,t.ui.increase.disabled=!0):(t.ui.decrease.disabled&&(t.ui.decrease.disabled=!1),t.ui.increase.disabled&&(t.ui.increase.disabled=!1))}checkForRepeaters(e){e.querySelector(this.selectors.repeater.repeater)&&e.querySelectorAll(this.selectors.repeater.repeater).forEach(t=>{let s={id:t.querySelector("template").className??window.generateID("repeater"),ui:window.uiFromSelectors(this.selectors.repeater,t),form:e.dataset.formId,element:t,field:this.getField(t),sortable:!1,rows:[]};if(!s.ui.add)return;let i=t.querySelector("template");this.templates.define(i.className,{manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach(t=>{window.prefixInput(t,`${a.repeater.dataset.field}:${r}:`,e,!1,!0)})}}),window.Sortable&&(s.sortable=new Sortable(s.ui.items,{animation:150,draggable:this.selectors.repeater.row,handle:this.selectors.repeater.handle,ghostClass:"repeater-row-ghost",chosenClass:"repeater-row-chosen",dragClass:"dragging",onEnd:()=>{this.reindexList(s.ui.items)}})),t.dataset.repeaterId=s.id,this.addRepeaterListeners(t),this.repeaters.set(s.id,s)})}addRepeaterListeners(e){e.addEventListener("click",this.repeaterClick)}removeRepeaterListeners(e){e.removeEventListener("click",this.repeaterClick)}handleRepeaterClick(e){e.target.matches(this.selectors.repeater.add)?this.addRepeaterRow(e.target.closest("[data-repeater-id]")):e.target.matches(this.selectors.repeater.remove)&&this.removeRepeaterRow(e.target.closest("[data-index]"))}addRepeaterRow(e){let t={};t.repeater=e;let s=this.repeaters.get(e.dataset.repeaterId),i=this.templates.create(e.dataset.repeaterId,t);s.rows.push({element:i,fields:Array.from(i.querySelectorAll("[data-field]"))}),this.repeaters.set(s.id,s),s.ui.items.append(i);let a=this.getForm(e);this.initializeFields(e,a),this.reindexList(s.ui.items),this.a11y.announce("Row added")}removeRepeaterRow(e){let t=e.closest("[data-repeater-id]");e.remove();let s=this.repeaters.get(t);s&&this.reindexList(s.ui.items),this.a11y.announce("Row removed")}checkForTagLists(e){e.querySelectorAll(this.selectors.tagList.tagList)?.forEach(t=>{let s={id:t.querySelector("template").className??window.generateID("tagList"),ui:window.uiFromSelectors(this.selectors.tagList,t),element:t,form:e.dataset.formId,format:t.dataset.tagFormat??"first_field"};if(!s.ui.input||!s.ui.add||!s.ui.items)return;t.dataset.tagListId=s.id,s.fieldName=t.dataset.field;let i=t.querySelector("template");this.templates.define(i.className,{refs:{label:this.selectors.tagList.label},manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach(e=>{let t=e.closest(".tag-item");window.prefixInput(e,`${a.fieldName}:${r}:`,t,!1,!0)}),t.label&&(t.label.textContent=a.label)}}),s.ui.inputs=Array.from(t.querySelectorAll(this.selectors.tagList.inputs)),s.ui.value=Array.from(t.querySelectorAll(this.selectors.tagList.value)),this.tagLists.set(s.id,s),this.addTagListListeners(t)})}addTagListListeners(e){e.addEventListener("click",this.tagListClick),e.addEventListener("keypress",this.tagListInput)}removeTagListListeners(e){e.removeEventListener("click",this.tagListClick),e.removeEventListener("keypress",this.tagListInput)}handleTagListClick(e){window.targetCheck(e,this.selectors.tagList.add)?this.addTagListItem(e.target.closest("[data-tag-list-id]")):window.targetCheck(e,this.selectors.tagList.remove)&&this.removeTagListItem(e.target.closest(this.selectors.tagList.item))}addTagListItem(e){let t=this.tagLists.get(e.dataset.tagListId);if(!t)return;let s,i={},a=!1,r=!0;for(let e of t.ui.inputs){const t=e.required||"true"===e.dataset.required,s=this.getFieldValue(e);s&&(a=!0);const n=this.validateField(e);t&&!s?(this.showError(e,"This field is required"),r=!1):n||(r=!1);const o=e.name.replace("new_","");i[o]=s}if(!r){this.a11y.announce("Please correct the errors before adding");const e=t.ui.inputs.find(e=>(e.required||"true"===e.dataset.required)&&!this.getFieldValue(e));return void(e&&e.focus())}if(!a)return this.a11y.announce("Please fill in at least one field"),void t.ui.inputs[0].focus();switch(t.format){case"first_field":s=Object.values(i)[0];break;case"all_fields":s=Object.values(i).join(", ");break;default:if(t.format.includes("{")){s=t.format;for(const[e,t]of Object.entries(i))s=s.replace(`{${e}}`,t)}else s=i[t.format]??Object.values(i)[0]}let n=this.templates.create(e.dataset.tagListId,{label:s,fieldName:t.fieldName});const o=t.ui.items?.children?.length??0;n?.querySelectorAll("input[type=hidden]")?.forEach(e=>{const s=e.dataset.field;e.name=`${t.fieldName}:${o}:${s}`,e.id=`${t.fieldName}:${o}:${s}`,e.value=i[s]||""}),t.ui.items.append(n);for(let e of t.ui.inputs)["checkbox","radio"].includes(e.type)?e.checked=!1:e.value="",this.clearValidation(e);t.ui.inputs[0]?.focus(),this.updateCollectionField(e),this.a11y.announce("Item added")}removeTagListItem(e){let t=e.closest("[data-tag-list-id]");t&&(e.remove(),this.reindexList(t),this.updateCollectionField(t),this.a11y.announce("Item removed"))}handleTagListInput(e){let t=e.target,s=t.closest("[data-tag-list-id]");if(!s)return;let i=this.tagLists.get(s.dataset.tagListId);if(i&&"Enter"===e.key)if(t===i.ui.inputs[i.ui.inputs.length-1])e.preventDefault(),this.addTagListItem(t.closest("[data-tag-list-id]"));else{e.preventDefault();let s=i.ui.inputs.indexOf(t);i.ui.inputs[s+1].focus()}}checkForConditionalFields(e){e.querySelectorAll(this.selectors.dependsOn).forEach(t=>{const s=t.dataset.dependsOn,i=t.dataset.dependsValue,a=t.dataset.dependsOperatior??"==";let r=this.forms.get(e.dataset.formId);this.dependencies.has(s)||Object.hasOwn(r.ui.fields,s)&&this.dependencies.set(s,[]);let n=this.dependencies.get(s);n&&(n.push({field:t,form:e.dataset.formId,requiredValue:i,operator:a}),this.dependencies.set(s,n)),this.checkFieldDependency(t,s)})}checkFieldDependency(e,t){const s=this.getForm(e);if(!this.dependencies.get(t))return;const i=this.getFieldValue(s.ui.fields[t]),a=this.evaluateCondition(i,e.dataset.dependsValue,e.dataset.dependsOperatior);this.toggleFieldVisibility(e,a)}evaluateCondition(e,t,s){const i=String(e||""),a=String(t||"");switch(s){case"==":default:return i===a;case"!=":return i!==a;case">":return parseFloat(i)>parseFloat(a);case"<":return parseFloat(i)<parseFloat(a);case">=":return parseFloat(i)>=parseFloat(a);case"<=":return parseFloat(i)<=parseFloat(a);case"contains":return i.includes(a);case"empty":return""===i;case"not_empty":return""!==i}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach(e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)}))}checkForCharacterLimits(e){e.querySelector(this.selectors.limits.hasLimit)&&(this.countUpdaters=this.updateCount.bind(this),e.querySelectorAll(this.selectors.limits.hasLimit).forEach(t=>{const s=this.getFieldInput(t);if(!s)return;let i=window.generateID("limit");s.dataset.charLimitId=i,s.dataset.limit=t.dataset.maxlength;let a={element:s,form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.limits,t)};a.ui.limit&&(a.ui.limit.textContent=t.dataset.maxlength),this.charLimits.set(i,a),this.addCharacterLimitListeners(s)}))}addCharacterLimitListeners(e){e.addEventListener("input",this.countUpdaters,{passive:!0})}removeCharacterLimitListeners(e){e.removeEventListener("input",this.countUpdaters,{passive:!0})}updateCount(e){let t=e.target,s=this.charLimits.get(t.dataset.charLimitId);if(!s)return;let i=t.value.length,a=t.dataset.limit;s.ui.current&&(s.ui.current.textContent=i,s.ui.current.classList.toggle("exceeded",i>=a)),i>a&&(t.value=t.value.slice(0,a))}checkForImageUploads(e,t){this.hasUploads=!0,window.jvbUploads.scanFields(e,t.options.autoUpload,t.options.imageMeta);let s=e.querySelectorAll('[data-field-type="upload"]');s&&(t.ui.uploads={},s.forEach(e=>{t.ui.uploads[e.dataset.field]=e}))}checkForTabs(e,t){window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=window.jvbTabs.registerTab(e,{preCheck:(e,t,s)=>this.validateStep(e,t,s)}),t.ui.tabs=window.uiFromSelectors(this.selectors.tabs,e),t.ui.tabs.sections=Array.from(e.querySelectorAll(this.selectors.tabs.sections)),t.ui.tabs.inputs={},t.ui.tabs.sections.forEach(e=>{t.ui.tabs.inputs[e.dataset.tab]=Array.from(e.querySelectorAll(this.inputSelectors))}),t.ui.tabs.buttons=Array.from(e.querySelectorAll(this.selectors.tabs.buttons)),t.unsubscribeTabs=window.jvbTabs.subscribe((e,s)=>{if("tab-switched"===e&&t.ui.tabs.progress){const e=t.ui.tabs.sections.filter(e=>e.dataset.tab===s.current)[0]??!1;if(!e)return;const i=e.dataset.step-1,a=t.ui.tabs.sections.length;window.showProgress(t.ui.tabs.progress,i,a,`Step ${i+1} of ${a}`)}}),this.forms.set(t.id,t))}validateStep(e,t,s){let i=!1;if(s.ui.sections&&s.ui.sections.forEach((a,r)=>{if(a===e){let e=r-1;if(e>=0){let a=s.ui.sections[e];a&&a.dataset.tab===t&&(i=!0)}}}),i)return!0;const a=e.closest("[data-form-id]")?.dataset.formId;if(!a)return!0;if(!this.forms.get(a))return!0;return Array.from(this.inputs.values()).filter(t=>t&&t.form===a&&t.section===e.dataset.tab&&!t.element.closest("[hidden]")).every(e=>!0===this.validateField(e.element))}checkForSelectors(e){window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}reindexList(e){let t=e.dataset.field||e.dataset.repeaterId||e.dataset.tagListId;if(!t){let s=e.closest("[data-field]");t=s.dataset.field||s.dataset.repeaterId||s.dataset.tagListId}Array.from(e.children).forEach((e,s)=>{e.dataset.index=`${s}`;let i=e.querySelector(".row-number");i&&(i.textContent=`#${s+1}`);let a=e.querySelector(".row-title"),r=a?a.dataset.label:null;e.querySelectorAll("input, select, textarea").forEach(i=>{if("file"===i.type)return;const n=Object.hasOwn(i.dataset,"field")?i.dataset.field:i.name.split(":").pop();n===r&&(a.textContent=window.escapeHtml(i.value)),window.prefixInput(i,`${t}:${s}:${n}`,e,!0,!0)})}),this.updateCollectionField(e)}updateCollectionField(e){const t=e.closest("[data-field]");if(!t)return;const s=t.dataset.fieldType;if(!["repeater","tag-list"].includes(s))return;const i=this.getForm(e);if(!i)return;const a=this.getFieldValue(t);this.updateItem(t.dataset.field,a,i)}maybeUpdateCollectionDisplay(e){let t=e.closest("[data-repeater-id],[data-tag-list-id]");if(t&&"repeater"===t.dataset.fieldType){let t=e.closest(".repeater-row");if(!t)return;let s=t.querySelector(".row-title");if(!s||!s.dataset.label)return;console.log(e.dataset.field.split(":").pop()),console.log(s.dataset.label),e.dataset.field.split(":").pop()===s.dataset.label&&(s.textContent=this.getFieldValue(e.querySelector(this.inputSelectors)))}}clearValidation(e){let t=this.getField(e);if(!t)return;let s=this.getItem(e);s&&(t.classList.remove("has-error","has-success"),s.ui.success&&(s.ui.success.hidden=!0),s.ui.error&&(s.ui.error.hidden=!0),s.ui.message&&(s.ui.message.hidden=!0,s.ui.message.textContent=""))}showError(e,t="Invalid field"){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-success"),s.classList.add("has-error"),s.scrollIntoView({behavior:"smooth",block:"center",inline:"nearest"}),s.querySelector(this.inputSelectors)?.focus(),i.ui.message&&(i.ui.message.hidden=!1,i.ui.message.textContent=t))}showSuccess(e,t=""){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-error"),s.classList.add("has-success"),i.ui.message&&(i.ui.message.hidden=""===t,i.ui.message.textContent=t))}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach(e=>e.remove()),e.querySelectorAll(".field-error").forEach(e=>e.classList.remove("field-error")),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const i=window.getIcon?.("check-circle");i&&(i.classList.add("success-icon"),s.prepend(i))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach(e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)})}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch(e=>{console.warn("Failed to clear form cache:",e)});const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully")}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach(e=>e.remove()),e.querySelectorAll(".field-error, .has-error").forEach(e=>{e.classList.remove("field-error","has-error")}),e.querySelectorAll(".field").forEach(e=>{this.clearValidation(e)}),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const i=window.getIcon?.("close-circle");i&&(i.classList.add("error-icon"),s.prepend(i)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}showFormStatus(e,t,s=""){let i=this.forms.get(e);i&&i.options.showStatus&&i.ui?.status?.status&&i.status!==t&&(i.status=t,i.ui.status.status.hidden=!1,i.ui.status.status.classList.toggle("loading",["uploading","saving"].includes(t)),i.ui.status.message.textContent=""===s?this.getDefaultMessage(t):s,i.ui.status.icon.className="icon icon-"+this.getDefaultIcon(t),setTimeout(()=>i.ui.status.status.hidden=!0,"submitted"===t?3e3:1e4))}getDefaultMessage(e){return{saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"}[e]??e}getDefaultIcon(e){return{autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[e]??""}showSummary(e){let t=this.templates.create("formSummary",e);e.config.element.after(t),window.fade(e.config.element,!1)}getForm(e){let t=e.closest("[data-form-id]");if(!t)return!1;let s=t.dataset.formId;if(!s)return!1;let i=this.forms.get(s);return i||!1}getField(e){return e.closest("[data-field]")}getFieldType(e){let t=this.getField(e);if(t)return t.dataset.fieldType}getFieldValue(e){let t=this.getFieldType(e),s=this.getItem(e),i=s.field?.dataset.field??!1;if(!i)return!1;switch(t){case"repeater":return this.getRepeaterValue(e,s);case"tag-list":return this.getTagListValue(e,s);case"group":return null;case"location":return this.getLocationValue(e,s);case"selector":case"upload":case"gallery":case"image":return this.getHiddenInputValue(e,s,i);case"true-false":case"toggle-text":return e.checked;case"checkbox":return e.name.endsWith("[]")?this.getCheckboxGroupValue(e,s):e.checked?e.value:"";default:return e.value}}getCheckboxGroupValue(e,t){return t.checkboxGroup||(t.checkboxGroup=t.field?.querySelectorAll(`input[type="checkbox"][name="${e.name}"]`),this.saveItem(t)),Array.from(t.checkboxGroup).filter(e=>e.checked).map(e=>e.value)}getFieldCheckedValue(e){if("checkbox"===e.type){return"true-false"===this.getFieldType(e)?e.checked:e.checked?e.value:""}if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`),s=Array.from(t).find(e=>e.checked);return s?s.value:""}return this.getFieldValue(e)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getRepeaterValue(e,t){const s=e.querySelector(".repeater-items");if(!s)return[];let i=["image_data","image-title","image-caption","image-description","image-alt-text"],a=[];return Array.from(s.children).forEach(e=>{let t={};e.querySelectorAll('input[type="hidden"]').forEach(e=>{t[e.name]=e.value}),e.querySelectorAll("[data-field]").forEach(e=>{if(!i.includes(e.dataset.field)){const s=this.getFieldInput(e);s&&(t[e.dataset.field]=this.getFieldValue(s))}}),a.push(t)}),a}getFieldInput(e){const t=e.querySelector("textarea[data-editor]");return t||e.querySelector(this.inputSelectors)}getTagListValue(e,t){t.container||(t.container=t.field?.querySelector(".tag-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach(e=>{let t=e.querySelectorAll('input[type="hidden"]'),i={};t.forEach(e=>{i[e.dataset.field]=e.value}),s.push(i)}),s}getLocationValue(e,t){t.values||(t.values=Array.from(t.field?.querySelectorAll("[data-location-field]")),this.saveItem(t));let s={};return t.values.forEach(e=>{s[e.dataset.locationField]=e.value}),s}getHiddenInputValue(e,t,s){return"INPUT"===e.tagName&&"hidden"===e.type||(e=e.querySelector('input[type="hidden"][name="'+s+'"]'))?(void 0!==t.value&&t.value===e.value||(t.value=e.value,this.saveItem(t)),t.value):null}formatValueForSummary(e,t){const s=this.getFieldType(t.element);if(this.isEmptyValue(e))return"";switch(s){case"repeater":return this.formatRepeaterForSummary(e,t);case"tag-list":return this.formatTagListForSummary(e,t);case"location":return this.formatLocationForSummary(e);case"true-false":return e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatCheckboxGroupForSummary(e,t):this.getDisplayLabel(t,e);case"selector":case"upload":case"image":case"gallery":return this.formatHiddenFieldForSummary(e,t,s);default:return"string"==typeof e?this.getDisplayLabel(t,e):"string"==typeof e&&e.includes("\n")?this.convertLineBreaks(e):e}}formatCheckboxGroupForSummary(e,t){return e.map(e=>this.getDisplayLabel(t,e)).join(", ")}convertLineBreaks(e){const t=document.createElement("span");return t.innerHTML=e.split("\n").join("<br>"),t}formatRepeaterForSummary(e,t){const s=document.createElement("div");return s.className="summary-repeater",e.forEach((e,i)=>{const a=document.createElement("div");a.className="summary-repeater-row";const r=document.createElement("strong");r.textContent=`Entry ${i+1}:`,a.appendChild(r);const n=document.createElement("ul");n.className="summary-repeater-fields";for(const[s,i]of Object.entries(e)){if(this.isEmptyValue(i))continue;const e=document.createElement("li"),a=t.field?.querySelector(`[data-field="${s}"]`),r=a?.closest(".field")?.querySelector("label")?.textContent.replace("*","").trim()||s;e.innerHTML=`<span class="field-label">${r}:</span> <span class="field-value">${i}</span>`,n.appendChild(e)}a.appendChild(n),s.appendChild(a)}),s}formatTagListForSummary(e,t){const s=document.createElement("div");s.className="summary-taglist";const i=document.createElement("ul");return i.className="summary-tags",e.forEach(e=>{const t=document.createElement("li");t.className="summary-tag";const s=Object.values(e).find(e=>!this.isEmptyValue(e))||"",a=Object.entries(e).filter(([e,t])=>!this.isEmptyValue(t));a.length>1?t.textContent=a.map(([e,t])=>t).join(", "):t.textContent=s,i.appendChild(t)}),s.appendChild(i),s}formatLocationForSummary(e){const t=[];return e.street&&t.push(e.street),e.city&&t.push(e.city),e.province&&t.push(e.province),e.postal_code&&t.push(e.postal_code),e.country&&t.push(e.country),t.length>0?t.join(", "):e.address||""}formatHiddenFieldForSummary(e,t,s){if(["upload","gallery","image"].includes(s)){const s=t.field?.querySelector("[data-upload-field]");if(s){const e=s.querySelectorAll(".item-grid.preview img");if(e.length>0){const t=document.createElement("div");return t.className="summary-uploads",e.forEach(e=>{const s=e.cloneNode(!0);s.style.maxWidth="100px",s.style.maxHeight="100px",t.appendChild(s)}),t}}return`${e.split(",").length} file(s) uploaded`}return e}getDisplayLabel(e,t){if(!e.element)return t;const s=e.element.type;if("radio"===s){const s=e.field.querySelectorAll(`input[type="radio"][name="${e.element.name}"]`),i=Array.from(s).find(e=>e.value===t);if(i){const t=i.closest("label")||e.field.querySelector(`label[for="${i.id}"]`);if(t)return t.textContent.replace("*","").trim()}}if("checkbox"===s&&"true-false"!==this.getFieldType(e.element)){const s=e.field.querySelector(`input[type="checkbox"][value="${t}"]`);if(s){const t=s.closest("label")||e.field.querySelector(`label[for="${s.id}"]`);if(t){const e=t.querySelector("span");return e?e.textContent.trim():t.textContent.replace("*","").trim()}}}return t}getItem(e,t=null){const s=Object.hasOwn(e.dataset,"ref");let i=s?e.dataset.ref:window.generateID("input");if(s||(e.dataset.ref=i),!this.inputs.has(i)){t||(t=e.closest("[data-form-id]")?.dataset.formId??!1);let s=this.getField(e);this.inputs.set(i,{id:i,element:e,form:t,field:s,section:e.closest("[data-tab]")?.dataset.tab??!1,ui:window.uiFromSelectors(this.selectors.fields,s)})}return this.inputs.get(i)}saveItem(e){this.inputs.set(e.id,e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach(s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}})}destroy(){this.forms.size>0&&(Array.from(this.forms.values()).forEach(e=>{this.removeFormListeners(e)}),this.forms.clear()),this.repeaters.size>0&&(Array.from(this.repeaters.values()).forEach(e=>{this.removeRepeaterListeners(e.element),e.sortable?.destroy()}),this.repeaters.clear()),this.quantityFields.size>0&&(Array.from(this.quantityFields.values()).forEach(e=>{this.removeQuantityListeners(e.element)}),this.quantityFields.clear()),this.tagLists.size>0&&(Array.from(this.tagLists.values()).forEach(e=>{this.removeTagListListeners(e.element)}),this.tagLists.clear()),this.charLimits.size>0&&Array.from(this.charLimits.values()).forEach(e=>{e.element.removeEventListener("input",this.countUpdaters)}),this.inputs.clear(),this.forms.clear(),this.charLimits.clear()}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.jvbForm=new e,document.querySelectorAll("form[data-auto]").forEach(e=>{window.jvbForm.registerForm(e,{})}))})})})();
\ No newline at end of file
diff --git a/assets/js/min/login.min.js b/assets/js/min/login.min.js
index 71ce697..cf9a9e5 100644
--- a/assets/js/min/login.min.js
+++ b/assets/js/min/login.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.initElements()}initElements(){this.selectors={magic:"form#magic-link-form",login:"form#loginform"},this.ui=window.uiFromSelectors(this.selectors),this.forms={};for(let[e,i]of Object.entries(this.ui))i&&(this.forms[e]=window.jvbForm.registerForm(i,{cache:!1,useQueue:!1}))}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(i=>{"auth-loaded"===i&&(window.login=new e)})})})();
\ No newline at end of file
+(()=>{class e{constructor(){this.initElements()}initElements(){this.selectors={magic:"form#magic-link-form",login:"form#loginform"},this.ui=window.uiFromSelectors(this.selectors),this.forms={};for(let[e,t]of Object.entries(this.ui))t&&(this.forms[e]=window.jvbForm.registerForm(t,{cache:!1,showStatus:!1,useQueue:!1}),"magic"===e&&window.jvbForm.subscribe((n,i)=>{if(i.config===this.forms[e])if("form-submit"===n)t.hidden=!0,this.spinner=window.jvbTemplates.create("spinner",{}),t.parentElement.append(this.spinner);else if("form-success"===n){t.hidden=!0;let e=window.jvbTemplates.create("formSummary",{config:t,changes:{}});Array.from(e.children).forEach(e=>{if("H2"===e.tagName)e.textContent="Check your email!";else if(e.classList.contains("message")){let t=Array.from(e.children);t[0].textContent="You'll find a magic link to log in in your email",t[1].textContent="If you can't find it - check your spam folder.",t[2].textContent="The link will expire in 15 minutes.",t[3].remove()}else e.classList.contains("summary")&&e.remove()}),this.spinner&&this.spinner.remove(),t.parentElement.append(e)}else if("form-error"===n)if(this.spinner&&this.spinner.remove(),Object.hasOwn(i.data,"code")&&"rate_limit_exceeded"===i.data.code){let e=window.jvbTemplates.create("formSummary",{config:t,changes:{}});Array.from(e.children).forEach(e=>{if("H2"===e.tagName)e.textContent="Slow down there!";else if(e.classList.contains("message")){let t=Array.from(e.children);t[0].textContent="You tried submitting a few too many times in a short window.",t[1].textContent="Try again in an hour or so.",t[2].remove(),t[3].remove()}else e.classList.contains("summary")&&e.remove()}),t.parentElement.append(e)}else{let e=window.jvbTemplates.create("formSummary",{config:t,changes:{}});Array.from(e.children).forEach(e=>{if("H2"===e.tagName)e.textContent="Something went wrong.";else if(e.classList.contains("message")){let t=Array.from(e.children);t[0].textContent="We aren't exactly sure what.",t[1].textContent="Try clearing your cookies and trying again. If the problem persists, let us know!",t[2].remove(),t[3].remove()}else e.classList.contains("summary")&&e.remove()}),t.parentElement.append(e)}}))}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.login=new e)})})})();
\ No newline at end of file
diff --git a/inc/integrations/BlueSky.php b/inc/integrations/BlueSky.php
index f3af70f..2691efe 100644
--- a/inc/integrations/BlueSky.php
+++ b/inc/integrations/BlueSky.php
@@ -15,7 +15,17 @@
protected ?string $access_token = null;
protected ?string $did = null;
- public function __construct(?int $user_id = null) {
+ protected static array $instances = [];
+ public static function getInstance(?int $userID = null):self
+ {
+ $key = is_null($userID) ? 'base' : $userID;
+ if (!array_key_exists($key, self::$instances)) {
+ self::$instances[$key] = new self($userID);
+ }
+ return self::$instances[$key];
+ }
+
+ protected function __construct(?int $user_id = null) {
$this->service_name = 'bluesky';
$this->title = 'BlueSky';
$this->icon = 'fediverse-logo';
diff --git a/inc/integrations/Cloudflare.php b/inc/integrations/Cloudflare.php
index 9d86ad7..ed37f69 100644
--- a/inc/integrations/Cloudflare.php
+++ b/inc/integrations/Cloudflare.php
@@ -13,7 +13,17 @@
private string $theme = 'light';
private string $size = 'normal';
- public function __construct()
+ protected static array $instances = [];
+ public static function getInstance(?int $userID = null):self
+ {
+ $key = is_null($userID) ? 'base' : $userID;
+ if (!array_key_exists($key, self::$instances)) {
+ self::$instances[$key] = new self($userID);
+ }
+ return self::$instances[$key];
+ }
+
+ protected function __construct()
{
$this->service_name = 'cloudflare';
$this->title = 'Cloudflare Turnstile';
diff --git a/inc/integrations/Facebook.php b/inc/integrations/Facebook.php
index e7d54e9..f30ee8f 100644
--- a/inc/integrations/Facebook.php
+++ b/inc/integrations/Facebook.php
@@ -42,7 +42,17 @@
'milestone' => 'milestones'
];
- public function __construct(?int $userID = null)
+ protected static array $instances = [];
+ public static function getInstance(?int $userID = null):self
+ {
+ $key = is_null($userID) ? 'base' : $userID;
+ if (!array_key_exists($key, self::$instances)) {
+ self::$instances[$key] = new self($userID);
+ }
+ return self::$instances[$key];
+ }
+
+ protected function __construct(?int $userID = null)
{
$this->service_name = 'facebook';
$this->title = 'Facebook';
diff --git a/inc/integrations/GoogleMaps.php b/inc/integrations/GoogleMaps.php
index 31a3d36..545e323 100644
--- a/inc/integrations/GoogleMaps.php
+++ b/inc/integrations/GoogleMaps.php
@@ -25,7 +25,17 @@
private const DEFAULT_ZOOM = 11; // City-wide view
private const DEFAULT_MAP_ID = '873aed4ca260b4203e45b6e3';
- public function __construct()
+ protected static self $instance;
+ public static function getInstance():self
+ {
+
+ if (!isset(self::$instance)) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ protected function __construct()
{
$this->service_name = 'maps';
$this->title = 'Google Maps';
diff --git a/inc/integrations/GoogleMyBusiness.php b/inc/integrations/GoogleMyBusiness.php
index 5893851..b802142 100644
--- a/inc/integrations/GoogleMyBusiness.php
+++ b/inc/integrations/GoogleMyBusiness.php
@@ -22,7 +22,17 @@
private ?string $client_secret = null;
private ?string $account_id = null;
- public function __construct(?int $userID = null)
+ protected static array $instances = [];
+ public static function getInstance(?int $userID = null):self
+ {
+ $key = is_null($userID) ? 'base' : $userID;
+ if (!array_key_exists($key, self::$instances)) {
+ self::$instances[$key] = new self($userID);
+ }
+ return self::$instances[$key];
+ }
+
+ protected function __construct(?int $userID = null)
{
$this->service_name = 'gmb';
$this->title = 'Google My Business';
diff --git a/inc/integrations/Helcim.php b/inc/integrations/Helcim.php
index c4c4d18..7f2556d 100644
--- a/inc/integrations/Helcim.php
+++ b/inc/integrations/Helcim.php
@@ -40,7 +40,17 @@
'per_hour' => 1000
];
- public function __construct(?int $userID = null)
+ protected static array $instances = [];
+ public static function getInstance(?int $userID = null):self
+ {
+ $key = is_null($userID) ? 'base' : $userID;
+ if (!array_key_exists($key, self::$instances)) {
+ self::$instances[$key] = new self($userID);
+ }
+ return self::$instances[$key];
+ }
+
+ protected function __construct(?int $userID = null)
{
$this->title = 'Helcim';
$this->icon = 'credit-card';
diff --git a/inc/integrations/Instagram.php b/inc/integrations/Instagram.php
index b2d34d7..77abcf0 100644
--- a/inc/integrations/Instagram.php
+++ b/inc/integrations/Instagram.php
@@ -32,7 +32,18 @@
private const GRAPH_API_BASE = 'https://graph.facebook.com/';
private const INSTAGRAM_BASE = 'https://graph.instagram.com/';
- public function __construct(?int $userID = null)
+ protected static array $instances = [];
+ public static function getInstance(?int $userID = null):self
+ {
+ $key = is_null($userID) ? 'base' : $userID;
+ if (!array_key_exists($key, self::$instances)) {
+ self::$instances[$key] = new self($userID);
+ }
+
+ return self::$instances[$key];
+ }
+
+ protected function __construct(?int $userID = null)
{
$this->service_name = 'instagram';
$this->title = 'Instagram';
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 7666081..761e6a0 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -177,7 +177,7 @@
protected bool $is_healthy = true;
protected bool $handleWebhooks = false;
- public function __construct(?int $userID = null)
+ protected function __construct(?int $userID = null)
{
$this->cacheName = $this->cacheName ?: $this->service_name;
$this->userID = $userID;
@@ -209,6 +209,13 @@
}
add_filter('jvbShouldRenderMeta', [$this, 'checkRenderField'], 10, 4);
+
+
+ }
+
+ protected function addFilters():bool
+ {
+ return is_null($this->userID);
}
protected function setContentTypes():void
@@ -1424,51 +1431,6 @@
}
- /**
- * Switch user context
- */
- public function switchUser(int $user_id): void
- {
- if ($this->userID === $user_id) {
- return;
- }
-
- // Clean up current context
- $this->cleanup();
-
- // Switch context
- $this->userID = $user_id;
- $this->credentials = [];
- $this->resetTokenRefreshFlag(); // ADD THIS LINE
-
- $this->ensureInitialized();
- }
-
- public function getAsUser(int $user_id) {
- return new $this($user_id);
- }
-
- /**
- * Clean up resources
- */
- protected function cleanup(): void
- {
- // Clear sensitive data
- $this->credentials = [];
-
- // Clear request history
- $this->request_history = [];
- }
-
- /**
- * Destructor - ensure cleanup
- */
- public function __destruct()
- {
- $this->cleanup();
- }
-
-
/***************************************************************
ERROR HANDLING
***************************************************************/
@@ -2718,16 +2680,16 @@
public static function title():string
{
- return (new static())->getTitle();
+ return static::getInstance()->getTitle();
}
public static function icon():string
{
- return (new static())->getIcon();
+ return static::getInstance()->getIcon();
}
public static function hasExtraOptions():bool
{
- return (new static())::$hasExtraOptions;
+ return static::getInstance()::$hasExtraOptions;
}
/*********************************************************************
diff --git a/inc/integrations/PostMark.php b/inc/integrations/PostMark.php
index 7ac31e6..a06f65f 100644
--- a/inc/integrations/PostMark.php
+++ b/inc/integrations/PostMark.php
@@ -22,10 +22,20 @@
protected bool $track_open;
protected bool $track_links;
protected ?string $lastMessageId = null;
+
+ protected static array $instances = [];
+ public static function getInstance(?int $userID = null):self
+ {
+ $key = is_null($userID) ? 'base' : $userID;
+ if (!array_key_exists($key, self::$instances)) {
+ self::$instances[$key] = new static($userID);
+ }
+ return self::$instances[$key];
+ }
/**
* Constructor
*/
- public function __construct(?int $userID = null)
+ protected function __construct(?int $userID = null)
{
$this->service_name = 'postmark';
$this->title = 'PostMark';
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index d9d4046..5e1a831 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -7,10 +7,12 @@
use JVBase\registrar\Fields;
use JVBase\registrar\Posts;
use JVBase\registrar\Registrar;
+use JVBase\ui\CRUDSkeleton;
use WP_Error;
use JVBase\ui\Checkout;
use JVBase\managers\queue\TypeConfig;
use JVBase\managers\queue\executors\IntegrationExecutor;
+use WP_Query;
if (!defined('ABSPATH')) {
exit;
@@ -83,7 +85,18 @@
protected string $locationId = '';
protected array $locations = [];
- public function __construct(?int $userID = null)
+ protected static array $instances = [];
+
+ public static function getInstance(?int $userID = null):self
+ {
+ $key = is_null($userID) ? 'base' : $userID;
+ if (!array_key_exists($key, self::$instances)) {
+ self::$instances[$key] = new static($userID);
+ }
+ return self::$instances[$key];
+ }
+
+ protected function __construct(?int $userID = null)
{
// Display properties
$this->title = 'Square';
@@ -196,7 +209,10 @@
'sync_to_square' => 'Sync Site to Square',
]
);
+
add_action('init', [$this, 'registerSquarePostTypes'], 5);
+ add_action('init', [$this, 'addDashboardPages'], 10);
+ add_action(BASE.'dashboard_page_orders', [$this, 'renderDashPage']);
}
/**
@@ -256,7 +272,7 @@
'type' => 'number',
'label' => 'Total Amount (cents)',
],
- 'post_status' => [
+ 'square_payment_status' => [
'type' => 'select',
'label' => 'Order Status',
'options' => [
@@ -294,6 +310,7 @@
'customer_phone' => [
'type' => 'phone',
'label' => 'Customer Phone',
+ 'section'=> 'your-account'
],
'special_instructions' => [
'type' => 'textarea',
@@ -1016,19 +1033,6 @@
if (!is_wp_error($response)) {
$this->processBatchSyncResponse($response, $map, $success, $errors);
- $square_id = $response['objects'][0]['id'];
- update_post_meta($postID, BASE . '_square_catalog_id', $square_id);
- update_post_meta($postID, BASE . '_square_sync_status', 'synced');
- update_post_meta($postID, BASE . '_square_last_sync', current_time('mysql'));
-
- // Save variation IDs
- if (!empty($response['objects'][0]['item_data']['variations'])) {
- foreach ($response['objects'][0]['item_data']['variations'] as $index => $variation) {
- update_post_meta($postID, BASE . '_square_variation_' . $index . '_id', $variation['id']);
- }
- }
-
- $success[] = $postID;
} else {
// Handle batch request failure
$error_message = 'Batch sync failed';
@@ -1145,6 +1149,7 @@
* @param string|null $square_image_id Previously uploaded Square image ID
* @return array|WP_Error Catalog object or error
*/
+ //TODO: Get to work with Registrar settings
protected function buildCatalogObject(int $postID, ?string $square_image_id = null): array|WP_Error
{
$post = get_post($postID);
@@ -1474,6 +1479,7 @@
/**
* Get valid fields for Square product type
*/
+ //TODO: This feels redundant now, with how we've defined fields in getAdditionalFields
private function getValidFieldsForProductType(string $product_type): array
{
$fields = ['name', 'description_html', 'sku', 'price', 'image_ids', 'category_id'];
@@ -1517,6 +1523,7 @@
/**
* Handle customer authentication during checkout
*/
+ //TODO: Is this necessary?
public function handleCustomerAuth($data):WP_Error|array
{
$email = sanitize_email($data['email'] ?? '');
@@ -1567,7 +1574,8 @@
'message' => 'Email found. Would you like to create an account to save your order history?'
];
}
- } else {
+ }
+
// Check Square for customer
$response = $this->postRequest('customers/search', [
'filter' => [
@@ -1590,7 +1598,6 @@
'message' => 'New customer'
];
}
- }
}
private function createCustomerAccount(string $email):WP_Error|array
@@ -1831,9 +1838,6 @@
/**
* Handle order status webhook
*/
- /**
- * Handle order status webhook - NOW UPDATES POST TYPE
- */
private function handleOrderWebhook(array $data): bool
{
$order_id = $data['object']['order']['id'] ?? '';
@@ -1845,7 +1849,7 @@
}
// Find the WP post for this order
- $wp_order_id = get_option(BASE . 'square_order_map_' . $order_id);
+ $wp_order_id = $this->getOrderPost($order_id);
if ($wp_order_id) {
// Update the post meta
@@ -1872,10 +1876,6 @@
do_action(BASE . 'square_order_ready', $wp_order_id, $order_id);
}
}
-
- // Also update transient cache for quick status checks
- set_transient(BASE . 'square_order_' . $order_id, $state, HOUR_IN_SECONDS);
-
// Trigger action for other integrations
do_action(BASE . 'square_order_updated', $order_id, $state, $data);
@@ -1980,7 +1980,7 @@
'application_id' => $this->credentials['client_id'] ?? '',
'location_id' => $this->locationId,
'environment' => $this->environment,
- 'currency' => get_option(BASE . 'currency', 'CAD'),
+ 'currency' => $this->getCurrency(),
'is_logged_in' => is_user_logged_in(),
'user_email' => is_user_logged_in() ? wp_get_current_user()->user_email : '',
]);
@@ -1993,7 +1993,7 @@
/**
* Get or create Square customer
*/
- private function getOrCreateSquareCustomer(array $customer_info): ?string
+ public function getOrCreateSquareCustomer(array $customer_info): ?string
{
if (empty($customer_info['email'])) {
return null;
@@ -2026,92 +2026,6 @@
return null;
}
- /**
- * Save order reference for status tracking
- */
- public function saveOrderReference($data): array
- {
- $order_id = sanitize_text_field($data['order_id'] ?? '');
- $payment_id = sanitize_text_field($data['payment_id'] ?? '');
-
- if (!$order_id) {
- return ['success' => false, 'message' => 'Invalid order data'];
- }
-
- // Save to user if logged in
- if (is_user_logged_in()) {
- $user_id = get_current_user_id();
- $orders = get_user_meta($user_id, BASE . '_square_orders', true) ?: [];
- $orders[] = [
- 'order_id' => $order_id,
- 'payment_id' => $payment_id,
- 'date' => current_time('mysql'),
- 'customer' => $data['customer'] ?? []
- ];
-
- // Keep last 50 orders
- if (count($orders) > 50) {
- $orders = array_slice($orders, -50);
- }
-
- update_user_meta($user_id, BASE . '_square_orders', $orders);
- }
-
- return [
- 'success' => true,
- 'order_id' => $order_id,
- 'message' => 'Order saved'
- ];
- }
- /**
- * Save order to user meta
- */
- private function saveOrderToUser(int $user_id, string $order_id): void
- {
- $orders = get_user_meta($user_id, BASE . '_square_orders', true) ?: [];
- $orders[] = [
- 'order_id' => $order_id,
- 'date' => current_time('mysql')
- ];
-
- // Keep only last 50 orders
- if (count($orders) > 50) {
- $orders = array_slice($orders, -50);
- }
-
- update_user_meta($user_id, BASE . '_square_orders', $orders);
- }
-
- /**
- * Get order status (for customer feedback)
- */
- public function getOrderStatus($data): WP_Error|array
- {
- $order_id = sanitize_text_field($data['order_id'] ?? '');
-
- if (!$order_id) {
- return new WP_Error('error', 'Order ID required');
- }
-
- // Fetch from Square
- $response = $this->getRequest('v2/orders/' . $order_id);
-
- if (is_wp_error($response)) {
- return new WP_Error('error', 'Could not fetch order status');
- }
-
- $order = $response['order'] ?? [];
- $status_data = [
- 'state' => $order['state'] ?? 'UNKNOWN',
- 'fulfillment_eta' => $order['fulfillments'][0]['pickup_details']['pickup_at'] ?? null
- ];
-
- return [
- 'success' => true,
- 'status' => $status_data['state'],
- 'eta' => $status_data['fulfillment_eta']
- ];
- }
/**
* Process delete from Square
@@ -2426,8 +2340,12 @@
return false;
}
- // Update cached payment status
- set_transient(BASE . 'square_payment_' . $payment_id, $status, HOUR_IN_SECONDS);
+ if ($order_id) {
+ $order = $this->getOrderPost($order_id);
+ if ($order) {
+ Meta::forPost($order)->set('square_payment_status', $status);
+ }
+ }
// Trigger action for other integrations
do_action(BASE . 'square_payment_updated', $payment_id, $status, $order_id, $data);
@@ -2755,24 +2673,25 @@
'type' => 'text',
'label' => 'Square Customer ID',
'hidden'=> true,
+ 'section'=> 'your-account'
],
'address_line_1' => [
'type' => 'text',
'label' => 'Address Line 1',
'hint' => 'ex: 6551 111 St NW',
'required' => true,
- 'section' => 'about'
+ 'section' => 'address'
],
'address_line_2' => [
'type' => 'text',
'label' => 'Address Line 2',
'hint' => 'ex: Unit 2',
- 'section' => 'about'
+ 'section' => 'address'
],
'city' => [
'type' => 'text',
'label'=> 'City',
- 'section' => 'about',
+ 'section' => 'address',
'required' => true,
],
'state' => [
@@ -2780,15 +2699,20 @@
'label' => 'Province',
'hint' => 'The two-character code, example: AB',
'default'=> 'AB',
- 'section' => 'about',
+ 'section' => 'address',
'required' => true,
],
+ 'zip_code' => [
+ 'type' => 'text',
+ 'label' => 'Postal Code',
+ 'section'=> 'address'
+ ],
'countryCode' => [
'type' => 'text',
'label' => 'Country Code',
'hint' => 'The tw-character country code, example: CA',
'default' => 'CA',
- 'section' => 'about',
+ 'section' => 'address',
'required' => true,
]
];
@@ -3375,7 +3299,7 @@
}
}
- private function createSquareOrder(array $items, ?string $customer_id, array $data): array|WP_Error
+ public function createSquareOrder(array $items, ?string $customer_id, array $data): array|WP_Error
{
// Build line items for Square
$line_items = [];
@@ -3432,7 +3356,7 @@
return $this->postRequest('orders', $order_data);
}
- private function createSquarePayment(
+ public function createSquarePayment(
string $source_id,
string $idempotency_key,
int $amount_cents,
@@ -3463,7 +3387,7 @@
return $this->postRequest('payments', $payment_data);
}
- private function saveOrderToWordPress(array $order_data): int
+ public function saveOrderToWordPress(array $order_data): int
{
// Extract customer info
$customer_email = $order_data['customer']['email'] ?? '';
@@ -3484,7 +3408,7 @@
// Create order post
$order_post_id = wp_insert_post([
- 'post_type' => BASE . '_square_orders',
+ 'post_type' => BASE.$this->orderPostType,
'post_title' => 'Order #' . $order_data['square_order_id'],
'post_status' => 'publish',
'post_author' => $user_id // Associate with user if logged in
@@ -3518,9 +3442,6 @@
'updated_at' => current_time('mysql')
]);
- // Index by Square order ID for quick webhook lookups
- update_option(BASE . 'square_order_map_' . $order_data['square_order_id'], $order_post_id);
-
return $order_post_id;
}
@@ -3575,17 +3496,10 @@
public function checkOrderStatus(string $order_id): ?string
{
- // Check transient cache first
- $cached = get_transient(BASE . 'square_order_' . $order_id);
- if ($cached) {
- return $cached;
- }
-
// Fetch from Square
$response = $this->getRequest('orders/' . $order_id);
if (!is_wp_error($response)) {
$state = $response['order']['state'] ?? null;
- set_transient(BASE . 'square_order_' . $order_id, $state, HOUR_IN_SECONDS);
return $state;
}
@@ -3661,4 +3575,72 @@
return $result;
}
+
+ public function addDashboardPages():void
+ {
+ $page = JVB()->dashboard()->addPage('Your Orders', 'orders', 'receipt');
+ $page->setScripts(['jvb-crud']);
+ }
+ public function renderDashPage():void
+ {
+ $instance = $this->determineInstance();
+
+ $crud = new CRUDSkeleton();
+ $crud->icon('receipt');
+ $crud->title('Your Orders','Here you can see your past orders, and reorder from there if you\'d like');
+ $crud->content($this->orderPostType, 'Order', 'Orders');
+ $crud->addSearch();
+ $crud->addCapabilities(['view']);
+ $crud->setEmptyState(sprintf(
+ '<div class="empty-state">
+ <h3>%sNothing here%s</h3>
+ <p>It doesn\'t look like you have any orders yet.</p>
+ <p>Head on over to <a href="%s">our menu</a> and make your first order!</p>
+ </div>',
+ jvbDashIcon($crud->getIcon()),
+ jvbDashIcon($crud->getIcon()),
+ get_post_type_archive_link(BASE.'menu_item')
+ ));
+
+
+ $crud->render();
+ }
+ protected function determineInstance():self
+ {
+ if (current_user_can('manage_options')) {
+ return self::$instances['base'];
+ }
+ return self::getInstance(get_current_user_id());
+ }
+
+ public function getOrderPost(string $order_id):int|false
+ {
+ $posts = new WP_Query([
+ 'post_type' => BASE.$this->orderPostType,
+ 'posts_per_page' => 1,
+ 'meta_key' => BASE.'square_order_id',
+ 'meta_value' => $order_id,
+ 'fields' => 'ids',
+ ]);
+ wp_reset_postdata();
+ return $posts->have_posts() ? $posts[0] : false;
+ }
+ public function getOrderHistory(int $user_id):array
+ {
+ $posts = new WP_Query([
+ 'post_type' => BASE.$this->orderPostType,
+ 'posts_per_page' => 25,
+ 'author' => $user_id,
+ 'orderby' => 'date',
+ 'order' => 'desc',
+ 'fields' => 'ids',
+ ]);
+ wp_reset_postdata();
+ return array_map(function ($post) {
+ $fields = Meta::forPost($post);
+ $fields['wp_order_id'] = $post;
+ return $fields;
+ }, $posts->posts);
+
+ }
}
diff --git a/inc/integrations/Umami.php b/inc/integrations/Umami.php
index 048e784..4afb1d5 100644
--- a/inc/integrations/Umami.php
+++ b/inc/integrations/Umami.php
@@ -35,7 +35,17 @@
private string $events_table;
private string $metrics_table;
- public function __construct(?int $userID = null)
+ protected static array $instances = [];
+ public static function getInstance(?int $userID = null):self
+ {
+ $key = is_null($userID) ? 'base' : $userID;
+ if (!array_key_exists($key, self::$instances)) {
+ self::$instances[$key] = new static($userID);
+ }
+ return self::$instances[$key];
+ }
+
+ protected function __construct(?int $userID = null)
{
$this->service_name = 'umami';
$this->title = 'Umami.js';
diff --git a/inc/managers/Dashboard/DashboardManager.php b/inc/managers/Dashboard/DashboardManager.php
index 3ef40f4..2a09547 100644
--- a/inc/managers/Dashboard/DashboardManager.php
+++ b/inc/managers/Dashboard/DashboardManager.php
@@ -5,8 +5,15 @@
use JVBase\forms\TaxonomySelector;
use JVBase\base\Site;
use JVBase\managers\Cache;
-use JVBase\managers\IconsManager;use JVBase\managers\RoleManager;use JVBase\meta\Form;use JVBase\meta\Meta;use JVBase\registrar\Fields;use JVBase\registrar\Registrar;
+use JVBase\managers\IconsManager;
+use JVBase\managers\LoginManager;
+use JVBase\managers\RoleManager;
+use JVBase\meta\Form;
+use JVBase\meta\Meta;
+use JVBase\registrar\Fields;
+use JVBase\registrar\Registrar;
use JVBase\ui\Navigation;
+use JVBase\ui\Tabs;
if (!defined('ABSPATH')) {
exit;
@@ -37,6 +44,7 @@
add_action(BASE.'dashboard_page_dash', [$this, 'renderMain'], 10);
}
add_action(BASE.'dashboard_page_account', [$this, 'renderAccount'], 10);
+ add_action(BASE.'dashboard_page_reset_password', [$this, 'resetPasswordPage'], 10);
}
protected function registerDashboard():void
@@ -84,14 +92,17 @@
return;
}
$page = $this->getCurrentPage();
+ if (!$page) {
+ $this->redirectToDashboard();
+ }
if (!in_array($page, $this->pages)) {
- error_log('Looking for page: '.$page. ' in: '.print_r($this->pages, true));
- error_log('[DashboardManager]::handleRedirect could not find page for '.$page);
+ error_log('Looking for page: '.$page->getSlug(). ' in: '.print_r($this->pages, true));
+ error_log('[DashboardManager]::handleRedirect could not find page for '.$page->getTitle());
return;
}
$permission = $page->getPermission();
- if (!empty($permission) && !current_user_can($permission)) {
- error_log('[DashboardManager]::handleRedirect User cannot manage '.$page);
+ if (!empty($permission) && !$this->handlePermission($page)) {
+ error_log('[DashboardManager]::handleRedirect User cannot manage '.$page->getTitle());
$this->redirectToDashboard();
}
}
@@ -216,12 +227,16 @@
// uasort($pages, fn($a, $b) => $a->getOrder() <=> $b->getOrder());
uasort($all, fn($a, $b) => $a->getOrder() <=> $b->getOrder());
+
foreach ($all as $slug => $item) {
if (is_a($item, Section::class)) {
$this->buildMenuSection($item, $menu);
} else {
$menuItem = $menu->addItem($item->getTitle(), jvbDashIcon($item->getIcon()));
$menuItem->url($item->getURL());
+ if ($this->getCurrentPage() === $item) {
+ $menuItem->current();
+ }
}
}
return $menu->render();
@@ -239,6 +254,10 @@
$main = $this->getMainPage($section->getSlug());
if ($main && $section->getIsLink()) {
$page->url($main->getURL());
+
+ if ($this->getCurrentPage() === $page) {
+ $page->current();
+ }
}
$submenu = $page->submenu();
@@ -248,6 +267,10 @@
} elseif ($slug !== $section->getSlug()) {
$menuItem = $submenu->addItem($item->getTitle(), jvbDashIcon($item->getIcon()));
$menuItem->url($item->getURL());
+
+ if ($this->getCurrentPage() === $item) {
+ $menuItem->current();
+ }
}
}
@@ -394,6 +417,7 @@
}
$account = $this->addPage('Account', 'account', 'user-circle');
+ $account->setScripts(['jvb-form','jvb-tabs']);
$account->setSection('account');
$this->sections['account'] = new Section('Account', 'account', 'user-circle');
@@ -403,6 +427,7 @@
$password = $this->addPage('Reset Password', 'reset-password', 'password', $account->getID());
$password->setOrder(2);
+ $password->setScripts(['jvb-form', 'jvb-tabs']);
$password->setSection('account');
}
@@ -489,6 +514,7 @@
$page = $this->addPage('Integrations', 'integrations', 'plugs-connected');
$page->setSection('settings');
$page->setOrder($order);
+ $page->setPermission('user_has_integrations');
$page->setScripts(['jvb-integrations']);
$parent = $page->getID();
@@ -599,9 +625,29 @@
protected function setCurrentUserPages():void
{
$this->currentUserPages = array_filter($this->pages, function($page) {
- return current_user_can('manage_options') || empty($page->getPermission()) || current_user_can($page->getPermission());
+ return current_user_can('manage_options') || empty($page->getPermission()) || $this->handlePermission($page);
});
}
+ protected function handlePermission($page):bool
+ {
+ return match($page->getPermission()) {
+ 'user_has_integrations' => $this->handleIntegrationPermission(),
+ default => current_user_can($page->getPermission())
+ };
+ }
+ protected function handleIntegrationPermission():bool
+ {
+ $user = wp_get_current_user();
+ $role = jvbUserRole($user->ID);
+ if (current_user_can('manage_options')) {
+ return true;
+ }
+ $registrar = Registrar::getInstance($role);
+ if (!$registrar) {
+ return false;
+ }
+ return $registrar->hasAnyIntegrations();
+ }
#[NoReturn]protected function redirectToLogin():void
{
@@ -669,26 +715,70 @@
public function renderAccount():void
{
-
$user = get_userdata(get_current_user_id());
$role = jvbUserRole($user->ID);
$registrar = Registrar::getInstance($role);
-
if ($registrar) {
$meta = Meta::forUser($user->ID);
$fields = $registrar->getFields();
+ $sections = $registrar->getSections();
+ $tabs = new Tabs();
+ foreach ($sections as $slug => $config) {
+ $tab = $tabs->addTab($slug);
+ $tab->title($config['title']);
+ if (!empty($config['description'])) {
+ $tab->description($config['description']);
+ }
+ if (!empty($config['icon'])) {
+ $tab->icon($config['icon']);
+ }
+ $content = implode('', array_map(function ($f) use ($meta) {
+ return Form::renderFrom($meta,$f);
+ },$config['fields']));
+ $tab->content($content);
+ }
+ echo '<form data-save="user" data-auto data-action="dash">'.jvbFormStatus().$tabs->render().'<button type="submit">'.jvbDashIcon('floppy-disk').'Save</button></form>';
} else {
$fields = Fields::getUserFields();
$meta = new Meta($user->ID, 'user', $fields);
+
+ echo Form::renderFormFrom($meta, 'user', [
+ 'heading' => 'Your Account',
+ 'description' => [
+ 'You can set your information here.',
+ 'To reset your password, check the side menu under "Account"'
+ ],
+ 'submit' => true
+ ]);
}
- echo Form::renderFormFrom($meta, 'user', [
- 'heading' => 'Your Account',
- 'description' => [
- 'You can set your information here.',
- 'To reset your password, check the side menu under "Account"'
- ]
- ]);
+ $script = 'document.addEventListener(\'DOMContentLoaded\', async function () {
+
+ window.auth.subscribe(event => {
+ if (event === \'auth-loaded\') {
+ const form = document.querySelector(\'form[data-save="user"]\');
+ if (!form || !window.jvbForm) return;
+
+ window.jvbForm.registerForm(form);
+ }
+ });
+ });';
+// wp_add_inline_script('jvb-form', $script);
+ }
+
+ public function resetPasswordPage():void
+ {
+ ?>
+ <h1>Reset Your Password</h1>
+ <p>If you'd like to reset your password, you can do so here.</p>
+ <?php
+ if (Site::has('magic_link')) {
+ echo '<p>Alternatively, you can always login using the magic link, which sets a temporary password for 15 minutes.</p>';
+ }
+ ?>
+ <?= LoginManager::getInstance()->renderLoginForm('resetpass'); ?>
+
+ <?php
}
}
diff --git a/inc/managers/Dashboard/DashboardPage.php b/inc/managers/Dashboard/DashboardPage.php
index c8c7964..2096a87 100644
--- a/inc/managers/Dashboard/DashboardPage.php
+++ b/inc/managers/Dashboard/DashboardPage.php
@@ -163,8 +163,9 @@
if (empty($out)) {
$out = sprintf(
- '<h1>%s</h1><p>It doesn\'t look like this page is configured yet.</p>',
+ '<h1>%s</h1><p>It doesn\'t look like this page is configured yet: %s.</p>',
$this->title,
+ BASE.'dashboard_page_'.$check
);
}
return $out;
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index e971e6c..af45d49 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -222,12 +222,14 @@
'subtype' => 'password',
'label' => __('New Password', 'jvb'),
'required' => true,
+ 'autocomplete'=> 'new-password'
],
'pass2' => [
'type' => 'text',
'subtype' => 'password',
'label' => __('Confirm Password', 'jvb'),
'required' => true,
+ 'autocomplete'=> 'new-password'
],
];
break;
@@ -656,10 +658,19 @@
$additionalInputs,
$fields,
$turnstile,
- $this->labels['submit']??'Login',
+ $this->labels['submit']??$this->determineSubmit($action),
$magicLink
);
}
+ protected function determineSubmit(string $action):string
+ {
+ return match($action) {
+ 'rp','resetpass' => 'Reset Password',
+ 'register' => 'Register',
+ 'lostpassword' => 'Send me a reset link',
+ default => 'Login',
+ };
+ }
protected function renderHeader():void
{
?>
diff --git a/inc/managers/MagicLinkManager.php b/inc/managers/MagicLinkManager.php
index 8d642f5..30c11f4 100644
--- a/inc/managers/MagicLinkManager.php
+++ b/inc/managers/MagicLinkManager.php
@@ -85,6 +85,7 @@
protected function generateToken(string $email, string $type, array $data = []): string
{
$token = wp_generate_password(32, false);
+ error_log('Generated Token: '.$token);
$token_data = array_merge([
'email' => $email,
@@ -96,9 +97,12 @@
if ($type === self::TYPE_REFERRAL) {
$this->referral_cache->set($token, $token_data);
} else {
+ error_log('Setting to $this->cache');
$this->cache->set($token, $token_data);
}
+ error_log('Generated token: '.print_r($token_data, true));
+
return $token;
}
@@ -107,8 +111,10 @@
*/
public function verifyToken(string $token, string $email): array|WP_Error
{
+ error_log('Verifying token: '.$token);
// Try regular cache first, then referral cache
$token_data = $this->cache->get($token);
+ error_log('Got token data from cache: '.print_r($token_data, true));
if (!$token_data) {
$token_data = $this->referral_cache->get($token);
@@ -302,7 +308,6 @@
if (!isset($_GET['magic_token']) || !isset($_GET['action']) || !isset($_GET['email'])) {
return;
}
-
$action = sanitize_text_field($_GET['action']);
$token = sanitize_text_field($_GET['magic_token']);
$email = sanitize_email(rawurldecode($_GET['email']));
@@ -315,7 +320,6 @@
if (is_wp_error($token_data)) {
$this->cleanURL();
- return;
}
switch ($action) {
@@ -358,6 +362,7 @@
$user = get_user_by('ID', $token_data['user_id']);
if (!$user) {
+ error_log('No user found: '.print_r($user, true));
wp_die('Invalid user');
}
diff --git a/inc/meta/MetaTypeManager.php b/inc/meta/MetaTypeManager.php
index e0da68f..a3001e8 100644
--- a/inc/meta/MetaTypeManager.php
+++ b/inc/meta/MetaTypeManager.php
@@ -52,7 +52,7 @@
],
'phone' => [
'type' => 'string',
- 'sanitize' => 'sanitizeTelephone',
+ 'sanitize' => 'sanitizePhone',
'default' => '',
],
'url' => [
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index 9f59f3d..c8a2d95 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -190,18 +190,22 @@
'first_name' => [
'type' => 'text',
'label' => 'First Name',
+ 'section'=> 'your-account'
],
'last_name' => [
'type' => 'text',
'label' => 'Last Name',
+ 'section'=> 'your-account'
],
'user_email' => [
'type' => 'email',
'label' => 'Your Email',
+ 'section'=> 'your-account'
],
'display_name' => [
'type' => 'text',
'label' => 'Display Name',
+ 'section'=> 'your-account'
],
// 'website' => [
// 'type' => 'url',
diff --git a/inc/registrar/Posts.php b/inc/registrar/Posts.php
index 3f0f3fe..de0f72a 100644
--- a/inc/registrar/Posts.php
+++ b/inc/registrar/Posts.php
@@ -158,7 +158,7 @@
* An array of taxonomy identifiers that will be registered for the post type. Taxonomies can be registered later with register_taxonomy() or register_taxonomy_for_object_type() .
* @var array
*/
- public array $taxonomies;
+ public array $taxonomies = [];
/**
* Whether there should be post type archives, or if a string, the archive slug to use.
* Will generate the proper rewrite rules if $rewrite is enabled. Default false.
diff --git a/inc/registrar/config/Integration.php b/inc/registrar/config/Integration.php
index 53d71a0..cab3a4c 100644
--- a/inc/registrar/config/Integration.php
+++ b/inc/registrar/config/Integration.php
@@ -58,6 +58,7 @@
error_log('[Integration]::setContentType Service is not setup. '.$this->service_name);
return $this;
}
+
$allowed = $connection->getAllowedContent();
if (!in_array($content, $allowed)) {
error_log($this->service_name.' Connection does not support this content: '.$content);
diff --git a/inc/rest/PermissionHandler.php b/inc/rest/PermissionHandler.php
index eb87fd0..4c41989 100644
--- a/inc/rest/PermissionHandler.php
+++ b/inc/rest/PermissionHandler.php
@@ -30,23 +30,6 @@
);
}
- $requestedUserId = $request->get_param('user');
-
- // No user param specified - allow (controller will handle)
- if (empty($requestedUserId)) {
- return true;
- }
-
- $currentUserId = get_current_user_id();
-
- if ((int) $requestedUserId !== $currentUserId) {
- return new WP_Error(
- 'forbidden',
- 'You can only access your own resources',
- ['status' => 403]
- );
- }
-
return true;
}
@@ -348,8 +331,8 @@
*/
public static function verifyActionNonce(WP_REST_Request $request, string $actionPrefix, string $header = 'X-Action-Nonce'): bool|WP_Error
{
- $userId = absint($request->get_param('user'));
- if ($userId === 0) {
+ $userId = get_current_user_id();
+ if (!$userId) {
return false;
}
diff --git a/inc/rest/_setup.php b/inc/rest/_setup.php
index b66a8a8..6a8dcf0 100644
--- a/inc/rest/_setup.php
+++ b/inc/rest/_setup.php
@@ -78,3 +78,4 @@
require(JVB_DIR .'/inc/rest/routes/FormRoutes.php');
require(JVB_DIR .'/inc/rest/routes/IntegrationsRoutes.php');
require(JVB_DIR .'/inc/rest/routes/LoginRoutes.php');
+require(JVB_DIR .'/inc/rest/routes/UserRoutes.php');
diff --git a/inc/rest/routes/ApprovalRoutes.php b/inc/rest/routes/ApprovalRoutes.php
index b35e485..b4129a7 100644
--- a/inc/rest/routes/ApprovalRoutes.php
+++ b/inc/rest/routes/ApprovalRoutes.php
@@ -55,7 +55,6 @@
Route::for('approvals')
->get([$this, 'getApprovals'])
->args([
- 'user' => 'integer|required',
'type' => 'string',
'status' => 'string|enum:pending,approved,rejected,expired',
])
@@ -63,7 +62,6 @@
->rateLimit(30)
->post([$this, 'handleAction'])
->args([
- 'user' => 'integer|required',
'request_id' => 'integer|required',
'action' => 'string|required|enum:approve,reject',
'type' => 'string|required',
@@ -84,7 +82,7 @@
{
$data = $request->get_params();
$request_id = absint($data['request_id']);
- $user_id = absint($data['user']);
+ $user_id = get_current_user_id();
$action = sanitize_text_field($data['action']);
$type = sanitize_text_field($data['type']);
$notes = sanitize_text_field($data['notes'] ?? '');
@@ -208,14 +206,10 @@
public function getApprovals(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
+ $user_id = get_current_user_id();
$type = sanitize_text_field($request->get_param('type') ?? 'all');
$status = sanitize_text_field($request->get_param('status') ?? 'pending');
- if (!$this->checkUser($user_id)) {
- return $this->unauthorized();
- }
-
$cacheKey = compact('user_id', 'type', 'status');
$result = $this->cache->remember($cacheKey, function() use ($type, $status) {
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index b5beed6..9375fa2 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -88,12 +88,11 @@
'dateFrom' => 'string',
'dateTo' => 'string',
])
- ->rateLimit(20)
+ ->rateLimit(30)
->post([$this, 'postContent'])
->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']]))
- ->rateLimit(30)
+ ->rateLimit()
->args([
- 'user' => 'int|required',
'posts' => 'required',
'content' => 'string',
])
@@ -163,7 +162,7 @@
public function postContent(WP_REST_Request $request): WP_REST_Response
{
$data = $request->get_params();
- $user_id = $data['user'];
+ $user_id = get_current_user_id();
if (!array_key_exists('posts', $data) || !is_array($data['posts'])) {
return Response::success(['message'=>'No posts found in request']);
@@ -172,7 +171,6 @@
$count = count($data['posts']);
$operationId = $data['id'];
- unset($data['user']);
unset($data['id']);
error_log('[CONTENT]:'.print_r($data, true));
diff --git a/inc/rest/routes/ContentTermsRoutes.php b/inc/rest/routes/ContentTermsRoutes.php
index 37c9d03..08fda0d 100644
--- a/inc/rest/routes/ContentTermsRoutes.php
+++ b/inc/rest/routes/ContentTermsRoutes.php
@@ -197,7 +197,7 @@
*/
public function checkTermPermission(WP_REST_Request $request): bool
{
- $userID = $request->get_param('user') ?? get_current_user_id();
+ $userID = get_current_user_id();
$termID = (int)$request->get_param('term_id');
if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
@@ -210,10 +210,10 @@
public function checkOwnerPermission(WP_REST_Request $request): bool
{
- $userID = $request->get_param('user') ?? get_current_user_id();
+ $userID = get_current_user_id();
$termID = (int)$request->get_param('term_id');
- if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
+ if (!term_exists($termID, jvbCheckBase($this->taxonomy))) {
return false;
}
@@ -227,7 +227,7 @@
public function updateSettings(WP_REST_Request $request): WP_REST_Response
{
$termID = (int)$request->get_param('term_id');
- $userID = $request->get_param('user');
+ $userID = get_current_user_id();
$data = $request->get_params();
unset($data['user'], $data['term_id']);
@@ -301,14 +301,10 @@
public function manageMember(WP_REST_Request $request): WP_REST_Response
{
$action = $request->get_param('action');
- $userID = $request->get_param('user');
+ $userID = get_current_user_id();
$termID = (int)$request->get_param('term_id');
$targetUserID = (int)$request->get_param('target_user');
- if (!$this->checkUser($targetUserID)) {
- return $this->error('Invalid target user');
- }
-
// Queue the operation
$op = JVB()->queue()->add(
"{$this->taxonomy}_member_{$action}",
@@ -367,7 +363,7 @@
public function handleRequest(WP_REST_Request $request): WP_REST_Response
{
$action = $request->get_param('action');
- $userID = $request->get_param('user');
+ $userID = get_current_user_id();
$termID = (int)$request->get_param('term_id');
return match($action) {
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index 953e4db..a9d767d 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -54,7 +54,6 @@
Route::for('favourites')
->get([$this, 'getFavourites'])
->args([
- 'user' => 'integer|required',
'type' => 'string',
'include_all' => 'boolean',
])
@@ -62,7 +61,6 @@
->rateLimit(30)
->post([$this, 'handleFavourite'])
->args([
- 'user' => 'integer|required',
'action' => 'string|required|enum:add,remove,toggle,batch,note',
'type' => 'string',
'target_id' => 'integer',
@@ -81,7 +79,6 @@
->rateLimit(30)
->post([$this, 'handleList'])
->args([
- 'user' => 'integer|required',
'id' => 'string|required',
'action' => 'string|required|enum:create,update,delete,share,unshare,add_items,remove_items',
'list_id' => 'integer',
@@ -95,7 +92,6 @@
// Favourite counts
Route::for('favourites/counts')
->get([$this, 'getFavouriteCounts'])
- ->args(['user' => 'integer|required'])
->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
->register();
}
@@ -105,9 +101,9 @@
*/
public function getFavourites(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
+ $user_id = get_current_user_id();
- if (!$this->userCheck($user_id)) {
+ if (!$user_id) {
return $this->unauthorized();
}
@@ -132,11 +128,8 @@
public function handleFavourite(WP_REST_Request $request): WP_REST_Response
{
$params = $request->get_params();
- $user_id = absint($params['user']??0);
+ $user_id = get_current_user_id();
- if (!$this->userCheck($user_id)) {
- return $this->unauthorized();
- }
$action = strtolower(sanitize_text_field($params['action']));
$action = in_array($action, ['add', 'remove']) ? $action : false;
if (!$action) {
@@ -171,11 +164,8 @@
public function getLists(WP_REST_Request $request): WP_REST_Response
{
$params = $request->get_params();
- $user_id = absint($params['user']);
+ $user_id = get_current_user_id();
- if (!$this->userCheck($user_id)) {
- return $this->unauthorized();
- }
$args = $this->buildParams($request);
$args['per_page'] = 20;
@@ -209,11 +199,8 @@
public function getFavouriteCounts(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
+ $user_id = get_current_user_id();
- if (!$this->userCheck($user_id)) {
- return $this->unauthorized();
- }
$counts = JVB()->favourites()->getFavouriteCounts($user_id);
@@ -528,8 +515,11 @@
protected function buildParams(WP_REST_Request $request): array
{
$data = $request->get_params();
-
- $where = ['user_id' => absint($data['user'])];
+ $userID = get_current_user_id();
+ if(!$userID) {
+ return [];
+ }
+ $where = ['user_id' => $userID];
if (!empty($data['content']) && $data['content'] !== 'all') {
$where['type'] = BASE . $data['content'];
}
@@ -1652,7 +1642,7 @@
*/
public function handleList(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
+ $user_id = get_current_user_id();
$operation_id = sanitize_text_field($request->get_param('id'));
$action = sanitize_text_field($request->get_param('action'));
diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index f4e0ab1..b0fcbe2 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -70,7 +70,6 @@
'context' => 'string',
'contextId' => 'string',
'favourites' => 'boolean',
- 'user' => 'integer',
'highlight' => 'string',
])
->auth('public')
@@ -89,7 +88,6 @@
'context' => 'string',
'contextId' => 'string',
'favourites' => 'boolean',
- 'user' => 'integer',
'highlight' => 'string',
])
->auth('public')
@@ -543,11 +541,15 @@
*/
protected function applyFavouritesFilter(array $args, array $data): array
{
- if (empty($data['favourites']) || empty($data['user'])) {
+ if (empty($data['favourites'])) {
return $args;
}
- $user_id = (int)$data['user'];
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ //Shouldn't happen, but whatever
+ return $args;
+ }
$content = jvbNoBase($args['post_type']);
// Get user's favourites for this content type
diff --git a/inc/rest/routes/IntegrationsHelcimRoutes.php b/inc/rest/routes/IntegrationsHelcimRoutes.php
index 6476e16..b626ecb 100644
--- a/inc/rest/routes/IntegrationsHelcimRoutes.php
+++ b/inc/rest/routes/IntegrationsHelcimRoutes.php
@@ -55,7 +55,10 @@
public function handleInitializeCheckout(WP_REST_Request $request): WP_REST_Response
{
$data = $request->get_json_params();
- $user_id = absint($data['user'] ?? get_current_user_id());
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
if (empty($data['amount'])) {
return $this->validationError(['message' => 'Amount is required']);
@@ -88,7 +91,7 @@
*/
public function getInvoices(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user') ?? get_current_user_id());
+ $user_id = get_current_user_id();
if (!$user_id) {
return $this->validationError(['message' => 'Not logged in']);
@@ -135,7 +138,7 @@
*/
public function getSavedCards(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user') ?? get_current_user_id());
+ $user_id = get_current_user_id();
if (!$user_id) {
return $this->validationError(['message' => 'Not logged in']);
diff --git a/inc/rest/routes/IntegrationsRoutes.php b/inc/rest/routes/IntegrationsRoutes.php
index ce909bf..4bfb20b 100644
--- a/inc/rest/routes/IntegrationsRoutes.php
+++ b/inc/rest/routes/IntegrationsRoutes.php
@@ -53,7 +53,10 @@
$action = $request->get_param('action');
- $theUserID = (user_can($request->get_param('user'), 'manage_options')) ? null : $request->get_param('user');
+ $theUserID = current_user_can('manage_options') ? null : get_current_user_id();
+ if ($theUserID === false) {
+ return $this->validationError(['message' => 'Not logged in']);
+ }
$integration = JVB()->connect($service, $theUserID);
if (!$integration) {
diff --git a/inc/rest/routes/IntegrationsSquareRoutes.php b/inc/rest/routes/IntegrationsSquareRoutes.php
index ea338e1..f60dc34 100644
--- a/inc/rest/routes/IntegrationsSquareRoutes.php
+++ b/inc/rest/routes/IntegrationsSquareRoutes.php
@@ -2,6 +2,7 @@
namespace JVBase\rest\routes;
+use JVBase\integrations\Square;
use JVBase\meta\Meta;
use JVBase\rest\Rest;
use Exception;
@@ -78,6 +79,7 @@
}
try {
+ /** @var Square $square */
$square = JVB()->connect('square');
// Step 1: Get or create Square customer
$customer_id = $square->getOrCreateSquareCustomer($data['customer']);
@@ -97,7 +99,7 @@
// Step 3: Create Payment in Square
$payment_response = $square->createSquarePayment(
$data['source_id'],
- $data['idempotency_key'],
+ $idempotency_key,
$data['amount'],
$order_id,
$customer_id
@@ -148,12 +150,11 @@
{
$data = $request->get_params();
error_log('Getting Saved Cards: '.print_r($data, true));
- $user_id = absint($data['user']??0);
- if ($user_id === 0) {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
return $this->success(['cards' => []]);
}
- $square = JVB()->connect('square');
// Get Square customer ID for this user
$square_customer_id = get_user_meta($user_id, BASE . '_square_customer_id', true);
@@ -162,58 +163,30 @@
return $this->success(['cards' => []]);
}
- // Fetch cards from Square (2025-compliant - separate endpoint)
- $cards_response = $square->getRequest('cards?customer_id=' . $square_customer_id);
-
- if (is_wp_error($cards_response)) {
- return $this->error('Failed to fetch cards');
- }
-
- return $this->success(['cards' => $cards_response['cards']??[]]);
+ return $this->success(['cards' => Square::getInstance()->getUserCards( $square_customer_id)]);
}
public function getOrderHistory(WP_REST_Request $request):WP_REST_Response
{
- $data = $request->get_params();
- $user_id = absint($data['user']??0);
- if ($user_id === 0) {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
return $this->validationError(['message' => 'Not logged in']);
}
- // Get orders from custom post type
- $orders = get_posts([
- 'post_type' => BASE . '_sq_orders',
- 'author' => $user_id,
- 'posts_per_page' => 50,
- 'orderby' => 'date',
- 'order' => 'DESC'
- ]);
-
- $order_data = [];
- foreach ($orders as $order) {
- $meta = Meta::forPost($order->ID);
- $fields = $meta->getAll(['square_order_id', 'status', 'amount', 'items', 'created_at', 'pickup_time']);
- $order_data[] = array_merge([
- 'wp_order_id' => $order->ID,
- ], $fields);
- }
-
- return $this->success(['orders' => $order_data]);
+ return $this->success(['orders' => Square::getInstance()->getOrderHistory($user_id)]);
}
public function getOrderStatus(WP_REST_Request $request):WP_REST_Response
{
$order_id = $request->get_param('order_id');
-
- // Find WP post by Square order ID
- $wp_order_id = get_option(BASE . 'square_order_map_' . $order_id);
+ $wp_order_id = Square::getInstance()->getOrderPost($order_id);
if (!$wp_order_id) {
return $this->error('Order not found');
}
$meta = Meta::forPost($wp_order_id);
- $fields = $meta->getAll(['status', 'fulfillment_status', 'pickup_time', 'items']);
+ $fields = $meta->getAll(['square_payment_status', 'fulfillment_status', 'pickup_time', 'items']);
return $this->success(['order' => $fields]);
}
diff --git a/inc/rest/routes/Invitations.php b/inc/rest/routes/Invitations.php
index 1839f44..6322115 100644
--- a/inc/rest/routes/Invitations.php
+++ b/inc/rest/routes/Invitations.php
@@ -43,7 +43,6 @@
Route::for('invitations')
->get([$this, 'getInvitations'])
->args([
- 'user' => 'int|required',
'to_term' => 'int',
'taxonomy' => 'string',
'status' => 'string|enum:all,pending,accepted,rejected,expired,revoked|default:all',
@@ -54,7 +53,6 @@
->post([$this, 'createInvitationRequest'])
->args([
- 'user' => 'int|required',
'action' => 'string|enum:create,revoke,refresh|default:create',
'invites' => 'array',
'invitation_id' => 'int'
@@ -69,12 +67,12 @@
*/
public function getInvitations(WP_REST_Request $request): WP_REST_Response
{
- $userID = $request->get_param('user');
+ $userID = get_current_user_id();
$termID = $request->get_param('to_term');
$taxonomy = $request->get_param('taxonomy');
// Validate user
- if (get_current_user_id() !== $userID) {
+ if (!$userID) {
return $this->unauthorized('Invalid user');
}
@@ -185,11 +183,11 @@
*/
public function createInvitationRequest(WP_REST_Request $request): WP_REST_Response
{
- $userID = $request->get_param('user');
+ $userID = get_current_user_id();
$action = $request->get_param('action');
// Validate user
- if (get_current_user_id() !== $userID) {
+ if (!$userID) {
return $this->unauthorized('Invalid user');
}
diff --git a/inc/rest/routes/NewsRoutes.php b/inc/rest/routes/NewsRoutes.php
index 6c25297..485e389 100644
--- a/inc/rest/routes/NewsRoutes.php
+++ b/inc/rest/routes/NewsRoutes.php
@@ -52,7 +52,6 @@
->rateLimit(20)
->post([$this, 'handleNewsOperation'])
->args([
- 'user' => 'integer|required',
'id' => 'string|required',
'post_title' => 'string|required',
'post_excerpt' => 'string',
@@ -80,7 +79,7 @@
{
$queue = JVB()->queue();
$data = $request->get_params();
- $user = absint($data['user']);
+ $user = get_current_user_id();
$operationID = sanitize_text_field($data['id']);
unset($data['user']);
diff --git a/inc/rest/routes/NotificationsRoutes.php b/inc/rest/routes/NotificationsRoutes.php
index eaf3e7f..b3c2e05 100644
--- a/inc/rest/routes/NotificationsRoutes.php
+++ b/inc/rest/routes/NotificationsRoutes.php
@@ -146,7 +146,6 @@
Route::for('notifications')
->get([$this, 'getNotifications'])
->args([
- 'user' => 'integer|required',
'type' => 'string',
'status' => 'string|enum:unread,read,actioned,dismissed',
'limit' => 'integer|default:20|min:1|max:100',
@@ -160,7 +159,6 @@
Route::for('notifications/read')
->post([$this, 'markRead'])
->args([
- 'user' => 'integer|required',
'notification_id' => 'integer|required',
])
->auth('user')
@@ -171,7 +169,6 @@
Route::for('notifications/read-all')
->post([$this, 'markAllRead'])
->args([
- 'user' => 'integer|required',
'type' => 'string',
])
->auth('user')
@@ -182,7 +179,6 @@
Route::for('notifications/action')
->post([$this, 'markActioned'])
->args([
- 'user' => 'integer|required',
'notification_id' => 'integer|required',
])
->auth('user')
@@ -193,7 +189,6 @@
Route::for('notifications/dismiss')
->post([$this, 'markDismissed'])
->args([
- 'user' => 'integer|required',
'notification_id' => 'integer|required',
])
->auth('user')
@@ -204,7 +199,6 @@
Route::for('notifications/count')
->get([$this, 'getUnreadCount'])
->args([
- 'user' => 'integer|required',
'type' => 'string',
])
->auth('user')
@@ -221,16 +215,15 @@
*/
public function getNotifications(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
$type = sanitize_text_field($request->get_param('type') ?? '');
$status = sanitize_text_field($request->get_param('status') ?? '');
$limit = absint($request->get_param('limit'));
$offset = absint($request->get_param('offset'));
- if (!$this->checkUser($user_id)) {
- return $this->unauthorized();
- }
-
$cacheKey = compact('user_id', 'type', 'status', 'limit', 'offset');
$result = $this->cache->remember($cacheKey, function() use ($user_id, $type, $status, $limit, $offset) {
@@ -265,12 +258,11 @@
*/
public function getUnreadCount(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
- $type = sanitize_text_field($request->get_param('type') ?? '');
-
- if (!$this->checkUser($user_id)) {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
return $this->unauthorized();
}
+ $type = sanitize_text_field($request->get_param('type') ?? '');
$cacheKey = compact('user_id', 'type');
@@ -647,12 +639,11 @@
*/
public function markRead(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
- $notification_id = absint($request->get_param('notification_id'));
-
- if (!$this->checkUser($user_id)) {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
return $this->unauthorized();
}
+ $notification_id = absint($request->get_param('notification_id'));
try {
$result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
@@ -695,12 +686,11 @@
*/
public function markAllRead(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
- $type = sanitize_text_field($request->get_param('type') ?? '');
-
- if (!$this->checkUser($user_id)) {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
return $this->unauthorized();
}
+ $type = sanitize_text_field($request->get_param('type') ?? '');
try {
$where = ['owner_id' => $user_id, 'status' => 'unread'];
@@ -730,12 +720,11 @@
*/
public function markActioned(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
- $notification_id = absint($request->get_param('notification_id'));
-
- if (!$this->checkUser($user_id)) {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
return $this->unauthorized();
}
+ $notification_id = absint($request->get_param('notification_id'));
try {
$result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
@@ -779,12 +768,11 @@
*/
public function markDismissed(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
- $notification_id = absint($request->get_param('notification_id'));
-
- if (!$this->checkUser($user_id)) {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
return $this->unauthorized();
}
+ $notification_id = absint($request->get_param('notification_id'));
try {
$result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
@@ -1318,7 +1306,7 @@
'status' => in_array($params['status'] ?? '', ['all', 'unread', 'expired'])
? $params['status']
: 'unread',
- 'user_id' => absint($params['user'] ?? get_current_user_id()),
+ 'user_id' => get_current_user_id(),
'page' => absint($params['page'] ?? 1),
'type' => in_array($params['type'] ?? '', array_keys($this->manager->notification_types ?? []))
? $params['type']
diff --git a/inc/rest/routes/OptionsRoutes.php b/inc/rest/routes/OptionsRoutes.php
index de1f75a..0ba6861 100644
--- a/inc/rest/routes/OptionsRoutes.php
+++ b/inc/rest/routes/OptionsRoutes.php
@@ -32,7 +32,6 @@
->auth(PermissionHandler::combine(['user', ['actionNonce' => 'dash-']]))
->rateLimit(3)
->args([
- 'user' => 'int|required',
'id' => 'string|required',
])
->register();
@@ -41,11 +40,11 @@
public function saveOptions(WP_REST_Request $request):WP_REST_Response
{
$data = $request->get_params();
- $user = $data['user'];
- if ($user && !user_can($user, 'manage_options')) {
+ $user = get_current_user_id();
+ if ($user && !current_user_can( 'manage_options')) {
return $this->error('User Cannot modify options');
}
- unset($data['user']);
+
$operationID = $data['id'];
unset($data['id']);
diff --git a/inc/rest/routes/QueueRoutes.php b/inc/rest/routes/QueueRoutes.php
index c1017be..e7fd1d7 100644
--- a/inc/rest/routes/QueueRoutes.php
+++ b/inc/rest/routes/QueueRoutes.php
@@ -87,7 +87,10 @@
public function getQueue(WP_REST_Request $request): WP_REST_Response
{
$params = $request->get_params();
- $user_id = absint($params['user']);
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
$this->cache = Cache::for('queue')->user();
$status = sanitize_text_field($params['status']);
$ids = !empty($params['ids'])
@@ -148,7 +151,10 @@
: array_map('trim', array_map('sanitize_text_field', explode(',', $data['ids'])));
$action = sanitize_text_field($data['action'] ?? '');
- $user_id = absint($data['user']);
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
$this->cache = Cache::for($user_id.'_queue');
@@ -182,7 +188,10 @@
public function pollQueue(WP_REST_Request $request): WP_REST_Response
{
- $userId = $request->get_param('user');
+ $userId = get_current_user_id();
+ if (!$userId) {
+ return $this->unauthorized();
+ }
$this->cache = Cache::for($userId.'_queue');
$since = $request->get_param('since');
$ids = $request->get_param('ids');
@@ -221,7 +230,10 @@
public function getOperationErrors(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
$this->cache = Cache::for($user_id.'_queue');
$operations = JVB()->queue()->getUserOperations($user_id, [
'state' => 'completed',
@@ -247,7 +259,10 @@
public function getOperation(WP_REST_Request $request): WP_REST_Response
{
$id = $request->get_param('id');
- $userId = $request->get_param('user');
+ $userId = get_current_user_id();
+ if (!$userId) {
+ return $this->unauthorized();
+ }
$this->cache = Cache::for($userId.'_queue');
$op = JVB()->queue()->get($id);
diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
index a1e3f3e..e977513 100644
--- a/inc/rest/routes/ReferralRoutes.php
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -35,7 +35,6 @@
Route::for('referrals')
->get([$this, 'getReferrals'])
->args([
- 'user' => 'integer',
'status' => 'string|enum:all,pending,consulted,treated,unused,registered,completed|default:all',
'date_start' => 'string',
'date_end' => 'string',
@@ -55,7 +54,6 @@
// Referral code endpoint
Route::for('referrals/code')
->get([$this, 'getCode'])
- ->args(['user' => 'integer'])
->auth('user')
->rateLimit(30)
->post([$this, 'validateCode'])
@@ -67,7 +65,6 @@
// Stats endpoint
Route::for('referrals/stats')
->get([$this, 'getStats'])
- ->args(['user' => 'integer'])
->auth('user')
->rateLimit(30)
->register();
@@ -102,15 +99,14 @@
*/
public function getReferrals(WP_REST_Request $request): WP_REST_Response
{
- $user_id = $request->get_param('user');
+ $user_id = get_current_user_id();
- // Determine scope: admin without user param gets all referrals
if (!$user_id) {
- $current_user_id = get_current_user_id();
- if (current_user_can('manage_options')) {
- return $this->getAllReferrals($request);
- }
- $user_id = $current_user_id;
+ return $this->unauthorized();
+ }
+
+ if (current_user_can('manage_options')) {
+ return $this->getAllReferrals($request);
}
// Build cache key
@@ -164,13 +160,12 @@
*/
protected function actionInvite(WP_REST_Request $request): WP_REST_Response
{
- $user = absint($request->get_param('user'));
- if (!$user || !get_userdata($user)) {
- return $this->error('Invalid user', 'invalid_user', 400);
+ $user = get_current_user_id();
+ if (!$user) {
+ return $this->unauthorized();
}
//Additional check to not send too many emails in an hour
- $user = absint($request->get_param('user'));
$transient_key = "referral_invite_limit_{$user}";
$recent_invites = get_transient($transient_key) ?: 0;
@@ -298,11 +293,10 @@
*/
public function getCode(WP_REST_Request $request): WP_REST_Response
{
- $user_id = $request->get_param('user') ?? get_current_user_id();
+ $user_id = get_current_user_id();
- // Check permission
- if ($user_id != get_current_user_id() && !current_user_can('manage_options')) {
- return $this->forbidden('Unauthorized');
+ if (!$user_id) {
+ return $this->unauthorized();
}
$code = JVB()->referrals()->getUserReferralCode($user_id);
@@ -353,7 +347,10 @@
*/
public function getStats(WP_REST_Request $request): WP_REST_Response
{
- $user_id = $request->get_param('user') ?? get_current_user_id();
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
$cache_key = "stats_{$user_id}";
// Check 304 Not Modified
diff --git a/inc/rest/routes/ResponseRoutes.php b/inc/rest/routes/ResponseRoutes.php
index 00c15a3..e84c254 100644
--- a/inc/rest/routes/ResponseRoutes.php
+++ b/inc/rest/routes/ResponseRoutes.php
@@ -97,10 +97,10 @@
public function handleResponseActions(WP_REST_Request $request): WP_REST_Response
{
$data = $request->get_params();
- $user_id = (int) ($data['user'] ?? 0);
+ $user_id = get_current_user_id();
- if (!$this->userCheck($user_id)) {
- return $this->unauthorized('User verification failed');
+ if (!$user_id) {
+ return $this->unauthorized();
}
$operation_id = $data['id'] ?? uniqid('response_');
diff --git a/inc/rest/routes/SettingsRoutes.php b/inc/rest/routes/SettingsRoutes.php
index b5d4926..66d1068 100644
--- a/inc/rest/routes/SettingsRoutes.php
+++ b/inc/rest/routes/SettingsRoutes.php
@@ -51,10 +51,9 @@
{
$data = $request->get_params();
- error_log('User: '.print_r($data['user'], true));
- error_log('Settings routes data: '.print_r($data, true));
- $user_id = absint($data['user']??0);
- if ($user_id === 0) {
+
+ $user_id = get_current_user_id();
+ if (!$user_id) {
return $this->unauthorized();
}
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 5222e4d..a0e8afa 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -105,7 +105,6 @@
->args([
'id' => 'string|required',
'content' => 'string|required',
- 'user' => 'int|required'
])
->register();
@@ -114,7 +113,6 @@
->auth(PermissionHandler::combine(['nonce']))
->rateLimit(30)
->args([
- 'user' => 'int|required',
'items' => 'array|required',
'id' => 'string'
])
@@ -174,15 +172,13 @@
break;
// User ID
case 'user':
- if ($this->userCheck($value)) {
- $args['user'] = (int) $value;
- if (!array_key_exists('post_id', $args) &&
- !array_key_exists('post_id', $data) &&
- !array_key_exists('term_id', $data) &&
- !array_key_exists('item_id', $data)) {
- $args['post_id'] = (int)get_user_meta((int) $value, BASE.'profile_link', true);
- }
+ if (!array_key_exists('post_id', $args) &&
+ !array_key_exists('post_id', $data) &&
+ !array_key_exists('term_id', $data) &&
+ !array_key_exists('item_id', $data)) {
+ $args['post_id'] = (int)get_user_meta((int) $value, BASE.'profile_link', true);
}
+
break;
// Operation ID
case 'id':
@@ -261,13 +257,13 @@
case 'field_key':
case 'field_type':
case 'subtype':
- case 'item_id':
case 'context':
if (is_string($value)) {
$args[$key] = sanitize_text_field($value);
}
break;
}
+ $args['user'] = get_current_user_id();
}
return $args;
}
@@ -286,8 +282,8 @@
$files = $request->get_file_params();
$args = $this->buildUploadArgs($request);
-
- if (!$args['user']) {
+ $userID = get_current_user_id();
+ if (!$userID) {
return $this->unauthorized();
}
if (!$args['content']) {
@@ -417,7 +413,7 @@
error_log('With ID: '.print_r($args['upload'], true));
$queuedProcessing = JVB()->queue()->queueOperation(
$operation_type,
- $args['user'],
+ get_current_user_id(),
array_merge(
['secured_files' => $secured_data['files']],
$args
@@ -441,7 +437,7 @@
if (!$queuedProcessing['updated_existing']) {
JVB()->queue()->queueOperation(
'attach_upload_to_content',
- $args['user'],
+ get_current_user_id(),
$args,
[
'priority' => 'high',
@@ -454,7 +450,7 @@
JVB()->queue()->queueOperation(
'temporary_cleanup',
- $args['user'],
+ get_current_user_id(),
[
'files' => $secured_data['files'],
'context' => $args,
@@ -1159,7 +1155,7 @@
$files = $request->get_file_params();
$args = $this->buildUploadArgs($request);
- if (!array_key_exists('user', $args) || $args['user'] === 0){
+ if (!$args['user']) {
return $this->unauthorized();
}
if (!array_key_exists('content', $args) || empty($args['content'])) {
diff --git a/inc/rest/routes/UserRoutes.php b/inc/rest/routes/UserRoutes.php
new file mode 100644
index 0000000..b3f3cb1
--- /dev/null
+++ b/inc/rest/routes/UserRoutes.php
@@ -0,0 +1,58 @@
+<?php
+
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use JVBase\rest\PermissionHandler;
+use JVBase\rest\Rest;
+use JVBase\rest\Route;
+use JVBase\base\Site;
+
+if (!defined('ABSPATH')) {
+ exit;
+}
+
+/**
+ * User Routes
+ */
+class UserRoutes extends Rest
+{
+ public function __construct()
+ {
+ $this->cacheName = 'user';
+ $this->cacheTtl = WEEK_IN_SECONDS;
+
+ parent::__construct();
+
+ }
+
+ public function registerRoutes(): void
+ {
+ // Standard login
+ Route::for('user')
+ ->post([$this, 'handleUpdate'])
+ ->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']]))
+ ->rateLimit()
+ ->register();
+ }
+
+ public function handleUpdate(WP_REST_Request $request):WP_REST_Response
+ {
+ $data = $request->get_params();
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
+ if (!array_key_exists('changes', $data) || empty($data['changes'])) {
+ return $this->success();
+ }
+
+ error_log('User route data: '.print_r($data, true));
+ try {
+ $meta = Meta::forUser($user_id);
+ $meta->setAll($data['changes']);
+ return $this->success();
+ } catch (Exception $e) {
+ return $this->error($e->getMessage());
+ }
+ }
+}
diff --git a/inc/rest/routes/VoteRoutes.php b/inc/rest/routes/VoteRoutes.php
index c0298ec..528cc2d 100644
--- a/inc/rest/routes/VoteRoutes.php
+++ b/inc/rest/routes/VoteRoutes.php
@@ -34,7 +34,6 @@
Route::for('vote')
->post([$this, 'handleVote'])
->args([
- 'user' => 'integer|required',
'id' => 'string|required',
'item_id' => 'integer|required',
'content' => 'string|required',
@@ -43,9 +42,6 @@
->auth('user')
->rateLimit(10)
->get([$this, 'getVotes'])
- ->args([
- 'user' => 'integer',
- ])
->auth('user')
->rateLimit(30)
->register();
@@ -70,9 +66,9 @@
return Response::validationError(['message' => __('Invalid item or vote attempt', 'jvb')]);
}
- $user = absint($request->get_param('user'));
- if (!$this->userCheck($user)) {
- return Response::validationError(['message' => __('User doesn\'t match. Bot?', 'jvb')]);
+ $user = get_current_user_id();
+ if (!$user) {
+ return $this->unauthorized();
}
$type = $registrar->getType()??false;
@@ -103,7 +99,10 @@
*/
public function getVotes(WP_REST_Request $request): WP_REST_Response
{
- $user = absint($request->get_param('user') ?? get_current_user_id());
+ $user = get_current_user_id();
+ if (!$user) {
+ return $this->unauthorized();
+ }
$cache = $this->cache->get($user);
if ($cache) {
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index b231217..87eec03 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -38,6 +38,7 @@
protected string $singular = '';
protected string $plural = '';
protected string $icon;
+ protected string $emptyState = '';
// Capabilities
protected array $caps = [];
@@ -145,6 +146,15 @@
return $this;
}
+ public function icon(string $icon):void
+ {
+ $this->icon = $icon;
+ }
+
+ public function getIcon():string
+ {
+ return $this->icon;
+ }
/**
* Set content type information
*/
@@ -533,7 +543,7 @@
* Render the CRUD interface
*/
public function render(): void {
- $config = $this->build();
+
$classes = array_merge(['dashboard-page', $this->dataType], $this->additionalClasses);
// ob_start();
@@ -1102,15 +1112,23 @@
}
protected function renderEmptyState():string
{
- ob_start();
- ?>
- <div class="empty-state">
- <h3><?=jvbDashIcon($this->icon)?>Nothing here<?=jvbDashIcon($this->icon)?></h3>
- <p>It doesn't look like you have any <?=$this->plural ?> yet.</p>
- <p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbDashIcon('plus-square')?>" button to add one at a time.</small></p>
- </div>
- <?php
- return ob_get_clean();
+
+ return empty($this->emptyState) ? sprintf(
+ '<div class="empty-state">
+ <h3>%sNothing here%s</h3>
+ <p>It doesn\'t look like you have any %s yet.</p>
+ <p><small><i>Add many by uploading images above.</i>, or click the "%s" button to add one at a time.</small></p>
+ </div>',
+ jvbDashIcon($this->icon),
+ jvbDashIcon($this->icon),
+ $this->plural,
+ jvbDashIcon('plus-square')
+ ) : $this->emptyState;
+ }
+
+ public function setEmptyState(string $state):void
+ {
+ $this->emptyState = $state;
}
protected function renderGalleryPreviewTemplate():void
diff --git a/inc/ui/Navigation.php b/inc/ui/Navigation.php
index 2fc120d..d075776 100644
--- a/inc/ui/Navigation.php
+++ b/inc/ui/Navigation.php
@@ -354,6 +354,7 @@
$this->current = $current;
if ($current) {
$this->addClass('current');
+ $this->attribute('aria-current', 'page');
}
return $this;
}
--
Gitblit v1.10.0