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