=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.
1 files added
48 files modified
| | |
| | | use JVBase\rest\routes\AdminRoutes; |
| | | use JVBase\rest\routes\IntegrationsRoutes; |
| | | use JVBase\base\SchemaHelper; |
| | | use UserRoutes; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | 'queue' => new QueueRoutes(), |
| | | 'settings' => new SettingsRoutes(), |
| | | 'upload' => new UploadRoutes(), |
| | | 'forms' => new FormRoutes() |
| | | 'forms' => new FormRoutes(), |
| | | 'user' => new UserRoutes() |
| | | ]; |
| | | |
| | | if (Site::has('magic_link')) { |
| | |
| | | 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(); |
| | | } |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | |
| | | }); |
| | | } |
| | | |
| | | 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 |
| | |
| | | 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) { |
| | |
| | | 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' |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | refs.result?.remove(); |
| | | data.config.element.after(el); |
| | | window.fade(data.config.element, false); |
| | | } |
| | | } |
| | | ); |
| | |
| | | window.auth.subscribe(event => { |
| | | if (event === 'auth-loaded') { |
| | | window.jvbForm = new FormController(); |
| | | document.querySelectorAll('form[data-auto]').forEach(form => { |
| | | window.jvbForm.registerForm(form, {}); |
| | | }); |
| | | } |
| | | }); |
| | | }); |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | | }); |
| | | } |
| | | |
| | | } |
| | | } |
| | | } |
| | |
| | | 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)}})}}; |
| | | 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{}}}}; |
| | |
| | | (()=>{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)})})})(); |
| | | (()=>{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,{})}))})})})(); |
| | |
| | | (()=>{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)})})})(); |
| | | (()=>{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)})})})(); |
| | |
| | | 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'; |
| | |
| | | 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'; |
| | |
| | | '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'; |
| | |
| | | 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'; |
| | |
| | | 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'; |
| | |
| | | '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'; |
| | |
| | | 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'; |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | add_filter('jvbShouldRenderMeta', [$this, 'checkRenderField'], 10, 4); |
| | | |
| | | |
| | | } |
| | | |
| | | protected function addFilters():bool |
| | | { |
| | | return is_null($this->userID); |
| | | } |
| | | |
| | | protected function setContentTypes():void |
| | |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 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 |
| | | ***************************************************************/ |
| | |
| | | |
| | | 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; |
| | | } |
| | | |
| | | /********************************************************************* |
| | |
| | | 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'; |
| | |
| | | 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; |
| | |
| | | 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'; |
| | |
| | | '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']); |
| | | } |
| | | |
| | | /** |
| | |
| | | 'type' => 'number', |
| | | 'label' => 'Total Amount (cents)', |
| | | ], |
| | | 'post_status' => [ |
| | | 'square_payment_status' => [ |
| | | 'type' => 'select', |
| | | 'label' => 'Order Status', |
| | | 'options' => [ |
| | |
| | | 'customer_phone' => [ |
| | | 'type' => 'phone', |
| | | 'label' => 'Customer Phone', |
| | | 'section'=> 'your-account' |
| | | ], |
| | | 'special_instructions' => [ |
| | | 'type' => 'textarea', |
| | |
| | | |
| | | 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'; |
| | |
| | | * @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); |
| | |
| | | /** |
| | | * 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']; |
| | |
| | | /** |
| | | * Handle customer authentication during checkout |
| | | */ |
| | | //TODO: Is this necessary? |
| | | public function handleCustomerAuth($data):WP_Error|array |
| | | { |
| | | $email = sanitize_email($data['email'] ?? ''); |
| | |
| | | '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' => [ |
| | |
| | | 'message' => 'New customer' |
| | | ]; |
| | | } |
| | | } |
| | | } |
| | | |
| | | private function createCustomerAccount(string $email):WP_Error|array |
| | |
| | | /** |
| | | * Handle order status webhook |
| | | */ |
| | | /** |
| | | * Handle order status webhook - NOW UPDATES POST TYPE |
| | | */ |
| | | private function handleOrderWebhook(array $data): bool |
| | | { |
| | | $order_id = $data['object']['order']['id'] ?? ''; |
| | |
| | | } |
| | | |
| | | // 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 |
| | |
| | | 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); |
| | | |
| | |
| | | '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 : '', |
| | | ]); |
| | |
| | | /** |
| | | * 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; |
| | |
| | | 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 |
| | |
| | | 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); |
| | |
| | | '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' => [ |
| | |
| | | '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, |
| | | ] |
| | | ]; |
| | |
| | | } |
| | | } |
| | | |
| | | 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 = []; |
| | |
| | | return $this->postRequest('orders', $order_data); |
| | | } |
| | | |
| | | private function createSquarePayment( |
| | | public function createSquarePayment( |
| | | string $source_id, |
| | | string $idempotency_key, |
| | | int $amount_cents, |
| | |
| | | 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'] ?? ''; |
| | |
| | | |
| | | // 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 |
| | |
| | | '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; |
| | | } |
| | | |
| | |
| | | |
| | | 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; |
| | | } |
| | | |
| | |
| | | |
| | | 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); |
| | | |
| | | } |
| | | } |
| | |
| | | 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'; |
| | |
| | | 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; |
| | |
| | | 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 |
| | |
| | | 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(); |
| | | } |
| | | } |
| | |
| | | // 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(); |
| | |
| | | $main = $this->getMainPage($section->getSlug()); |
| | | if ($main && $section->getIsLink()) { |
| | | $page->url($main->getURL()); |
| | | |
| | | if ($this->getCurrentPage() === $page) { |
| | | $page->current(); |
| | | } |
| | | } |
| | | $submenu = $page->submenu(); |
| | | |
| | |
| | | } elseif ($slug !== $section->getSlug()) { |
| | | $menuItem = $submenu->addItem($item->getTitle(), jvbDashIcon($item->getIcon())); |
| | | $menuItem->url($item->getURL()); |
| | | |
| | | if ($this->getCurrentPage() === $item) { |
| | | $menuItem->current(); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | $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'); |
| | |
| | | |
| | | $password = $this->addPage('Reset Password', 'reset-password', 'password', $account->getID()); |
| | | $password->setOrder(2); |
| | | $password->setScripts(['jvb-form', 'jvb-tabs']); |
| | | $password->setSection('account'); |
| | | |
| | | } |
| | |
| | | $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(); |
| | | |
| | |
| | | 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 |
| | | { |
| | |
| | | |
| | | 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 |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | '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; |
| | |
| | | $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 |
| | | { |
| | | ?> |
| | |
| | | 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, |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | */ |
| | | 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); |
| | |
| | | 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'])); |
| | |
| | | |
| | | if (is_wp_error($token_data)) { |
| | | $this->cleanURL(); |
| | | return; |
| | | } |
| | | |
| | | switch ($action) { |
| | |
| | | $user = get_user_by('ID', $token_data['user_id']); |
| | | |
| | | if (!$user) { |
| | | error_log('No user found: '.print_r($user, true)); |
| | | wp_die('Invalid user'); |
| | | } |
| | | |
| | |
| | | ], |
| | | 'phone' => [ |
| | | 'type' => 'string', |
| | | 'sanitize' => 'sanitizeTelephone', |
| | | 'sanitize' => 'sanitizePhone', |
| | | 'default' => '', |
| | | ], |
| | | 'url' => [ |
| | |
| | | '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', |
| | |
| | | * 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. |
| | |
| | | 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); |
| | |
| | | ); |
| | | } |
| | | |
| | | $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; |
| | | } |
| | | |
| | |
| | | */ |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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'); |
| | |
| | | Route::for('approvals') |
| | | ->get([$this, 'getApprovals']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'type' => 'string', |
| | | 'status' => 'string|enum:pending,approved,rejected,expired', |
| | | ]) |
| | |
| | | ->rateLimit(30) |
| | | ->post([$this, 'handleAction']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'request_id' => 'integer|required', |
| | | 'action' => 'string|required|enum:approve,reject', |
| | | 'type' => 'string|required', |
| | |
| | | { |
| | | $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'] ?? ''); |
| | |
| | | |
| | | 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) { |
| | |
| | | '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', |
| | | ]) |
| | |
| | | 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']); |
| | |
| | | |
| | | $count = count($data['posts']); |
| | | $operationId = $data['id']; |
| | | unset($data['user']); |
| | | unset($data['id']); |
| | | |
| | | error_log('[CONTENT]:'.print_r($data, true)); |
| | |
| | | */ |
| | | 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))) { |
| | |
| | | |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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']); |
| | |
| | | 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}", |
| | |
| | | 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) { |
| | |
| | | Route::for('favourites') |
| | | ->get([$this, 'getFavourites']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'type' => 'string', |
| | | 'include_all' => 'boolean', |
| | | ]) |
| | |
| | | ->rateLimit(30) |
| | | ->post([$this, 'handleFavourite']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'action' => 'string|required|enum:add,remove,toggle,batch,note', |
| | | 'type' => 'string', |
| | | 'target_id' => 'integer', |
| | |
| | | ->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', |
| | |
| | | // Favourite counts |
| | | Route::for('favourites/counts') |
| | | ->get([$this, 'getFavouriteCounts']) |
| | | ->args(['user' => 'integer|required']) |
| | | ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']])) |
| | | ->register(); |
| | | } |
| | |
| | | */ |
| | | 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(); |
| | | } |
| | | |
| | |
| | | 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) { |
| | |
| | | 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; |
| | |
| | | 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); |
| | | |
| | |
| | | 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']; |
| | | } |
| | |
| | | */ |
| | | 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')); |
| | | |
| | |
| | | 'context' => 'string', |
| | | 'contextId' => 'string', |
| | | 'favourites' => 'boolean', |
| | | 'user' => 'integer', |
| | | 'highlight' => 'string', |
| | | ]) |
| | | ->auth('public') |
| | |
| | | 'context' => 'string', |
| | | 'contextId' => 'string', |
| | | 'favourites' => 'boolean', |
| | | 'user' => 'integer', |
| | | 'highlight' => 'string', |
| | | ]) |
| | | ->auth('public') |
| | |
| | | */ |
| | | 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 |
| | |
| | | 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']); |
| | |
| | | */ |
| | | 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']); |
| | |
| | | */ |
| | | 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']); |
| | |
| | | $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) { |
| | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | |
| | | use JVBase\integrations\Square; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\rest\Rest; |
| | | use Exception; |
| | |
| | | } |
| | | |
| | | try { |
| | | /** @var Square $square */ |
| | | $square = JVB()->connect('square'); |
| | | // Step 1: Get or create Square customer |
| | | $customer_id = $square->getOrCreateSquareCustomer($data['customer']); |
| | |
| | | // Step 3: Create Payment in Square |
| | | $payment_response = $square->createSquarePayment( |
| | | $data['source_id'], |
| | | $data['idempotency_key'], |
| | | $idempotency_key, |
| | | $data['amount'], |
| | | $order_id, |
| | | $customer_id |
| | |
| | | { |
| | | $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); |
| | |
| | | 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]); |
| | | } |
| | |
| | | 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', |
| | |
| | | |
| | | ->post([$this, 'createInvitationRequest']) |
| | | ->args([ |
| | | 'user' => 'int|required', |
| | | 'action' => 'string|enum:create,revoke,refresh|default:create', |
| | | 'invites' => 'array', |
| | | 'invitation_id' => 'int' |
| | |
| | | */ |
| | | 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'); |
| | | } |
| | | |
| | |
| | | */ |
| | | 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'); |
| | | } |
| | | |
| | |
| | | ->rateLimit(20) |
| | | ->post([$this, 'handleNewsOperation']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'id' => 'string|required', |
| | | 'post_title' => 'string|required', |
| | | 'post_excerpt' => 'string', |
| | |
| | | { |
| | | $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']); |
| | |
| | | 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', |
| | |
| | | Route::for('notifications/read') |
| | | ->post([$this, 'markRead']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'notification_id' => 'integer|required', |
| | | ]) |
| | | ->auth('user') |
| | |
| | | Route::for('notifications/read-all') |
| | | ->post([$this, 'markAllRead']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'type' => 'string', |
| | | ]) |
| | | ->auth('user') |
| | |
| | | Route::for('notifications/action') |
| | | ->post([$this, 'markActioned']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'notification_id' => 'integer|required', |
| | | ]) |
| | | ->auth('user') |
| | |
| | | Route::for('notifications/dismiss') |
| | | ->post([$this, 'markDismissed']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'notification_id' => 'integer|required', |
| | | ]) |
| | | ->auth('user') |
| | |
| | | Route::for('notifications/count') |
| | | ->get([$this, 'getUnreadCount']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'type' => 'string', |
| | | ]) |
| | | ->auth('user') |
| | |
| | | */ |
| | | 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) { |
| | |
| | | */ |
| | | 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'); |
| | | |
| | |
| | | */ |
| | | 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) { |
| | |
| | | */ |
| | | 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']; |
| | |
| | | */ |
| | | 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) { |
| | |
| | | */ |
| | | 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) { |
| | |
| | | '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'] |
| | |
| | | ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'dash-']])) |
| | | ->rateLimit(3) |
| | | ->args([ |
| | | 'user' => 'int|required', |
| | | 'id' => 'string|required', |
| | | ]) |
| | | ->register(); |
| | |
| | | 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']); |
| | | |
| | |
| | | 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']) |
| | |
| | | : 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'); |
| | | |
| | |
| | | |
| | | 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'); |
| | |
| | | |
| | | 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', |
| | |
| | | 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); |
| | |
| | | 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', |
| | |
| | | // Referral code endpoint |
| | | Route::for('referrals/code') |
| | | ->get([$this, 'getCode']) |
| | | ->args(['user' => 'integer']) |
| | | ->auth('user') |
| | | ->rateLimit(30) |
| | | ->post([$this, 'validateCode']) |
| | |
| | | // Stats endpoint |
| | | Route::for('referrals/stats') |
| | | ->get([$this, 'getStats']) |
| | | ->args(['user' => 'integer']) |
| | | ->auth('user') |
| | | ->rateLimit(30) |
| | | ->register(); |
| | |
| | | */ |
| | | 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 |
| | |
| | | */ |
| | | 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; |
| | | |
| | |
| | | */ |
| | | 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); |
| | |
| | | */ |
| | | 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 |
| | |
| | | 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_'); |
| | |
| | | { |
| | | |
| | | $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(); |
| | | } |
| | | |
| | |
| | | ->args([ |
| | | 'id' => 'string|required', |
| | | 'content' => 'string|required', |
| | | 'user' => 'int|required' |
| | | ]) |
| | | ->register(); |
| | | |
| | |
| | | ->auth(PermissionHandler::combine(['nonce'])) |
| | | ->rateLimit(30) |
| | | ->args([ |
| | | 'user' => 'int|required', |
| | | 'items' => 'array|required', |
| | | 'id' => 'string' |
| | | ]) |
| | |
| | | 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': |
| | |
| | | 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; |
| | | } |
| | |
| | | $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']) { |
| | |
| | | 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 |
| | |
| | | if (!$queuedProcessing['updated_existing']) { |
| | | JVB()->queue()->queueOperation( |
| | | 'attach_upload_to_content', |
| | | $args['user'], |
| | | get_current_user_id(), |
| | | $args, |
| | | [ |
| | | 'priority' => 'high', |
| | |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'temporary_cleanup', |
| | | $args['user'], |
| | | get_current_user_id(), |
| | | [ |
| | | 'files' => $secured_data['files'], |
| | | 'context' => $args, |
| | |
| | | $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'])) { |
| New file |
| | |
| | | <?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()); |
| | | } |
| | | } |
| | | } |
| | |
| | | Route::for('vote') |
| | | ->post([$this, 'handleVote']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'id' => 'string|required', |
| | | 'item_id' => 'integer|required', |
| | | 'content' => 'string|required', |
| | |
| | | ->auth('user') |
| | | ->rateLimit(10) |
| | | ->get([$this, 'getVotes']) |
| | | ->args([ |
| | | 'user' => 'integer', |
| | | ]) |
| | | ->auth('user') |
| | | ->rateLimit(30) |
| | | ->register(); |
| | |
| | | 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; |
| | |
| | | */ |
| | | 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) { |
| | |
| | | protected string $singular = ''; |
| | | protected string $plural = ''; |
| | | protected string $icon; |
| | | protected string $emptyState = ''; |
| | | |
| | | // Capabilities |
| | | protected array $caps = []; |
| | |
| | | return $this; |
| | | } |
| | | |
| | | public function icon(string $icon):void |
| | | { |
| | | $this->icon = $icon; |
| | | } |
| | | |
| | | public function getIcon():string |
| | | { |
| | | return $this->icon; |
| | | } |
| | | /** |
| | | * Set content type information |
| | | */ |
| | |
| | | * Render the CRUD interface |
| | | */ |
| | | public function render(): void { |
| | | $config = $this->build(); |
| | | |
| | | $classes = array_merge(['dashboard-page', $this->dataType], $this->additionalClasses); |
| | | |
| | | // ob_start(); |
| | |
| | | } |
| | | 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 |
| | |
| | | $this->current = $current; |
| | | if ($current) { |
| | | $this->addClass('current'); |
| | | $this->attribute('aria-current', 'page'); |
| | | } |
| | | return $this; |
| | | } |