=Some hefty changes to FeedBlock. Transitioning to loading first page in php to save on extra requests. Got a bit to do yet, but I have to work on Northeh for a bit here.
| | |
| | | this.nonces = {}; |
| | | |
| | | this.subscribers = new Set(); |
| | | this.storageKey = `${jvbBase.base}auth_state`; |
| | | this.cacheMetaKey = `${jvbBase.base}auth_meta`; |
| | | this.cacheExpiry = 5 * 60 * 1000; // 5 minutes |
| | | |
| | | this.init(); |
| | |
| | | * Initialize authentication |
| | | */ |
| | | async init() { |
| | | if (this.isAuthenticating) { |
| | | return this.ready(); |
| | | } |
| | | |
| | | if (this.isAuthenticating) return; |
| | | this.isAuthenticating = true; |
| | | |
| | | try { |
| | | const cached = this.getCachedAuth(); |
| | | if (cached) { |
| | | this.setAuthData(cached); |
| | | // Inlined by wp_localize_script — zero cost |
| | | if (typeof jvbAuth !== 'undefined') { |
| | | this.setAuthData(jvbAuth); |
| | | this.initialized = true; |
| | | this.isAuthenticating = false; |
| | | this.notify('auth-loaded', { fromCache: true }); |
| | | this.notify('auth-loaded', { fromCache: false }); |
| | | return; |
| | | } |
| | | |
| | | // Fallback: REST fetch (Redis-backed, fast server-side) |
| | | await this.fetchAuth(); |
| | | |
| | | } catch (error) { |
| | | console.error('Failed to initialize auth:', error); |
| | | this.clearAuthData(); |
| | | this.initialized = true; |
| | | this.isAuthenticating = false; |
| | |
| | | const response = await fetch(`${jvbSettings.api}auth/status`, { |
| | | method: 'GET', |
| | | credentials: 'same-origin', |
| | | headers: { |
| | | 'Content-Type': 'application/json' |
| | | } |
| | | headers: { 'Content-Type': 'application/json' } |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | throw new Error('Auth check failed'); |
| | | } |
| | | if (!response.ok) throw new Error('Auth check failed'); |
| | | |
| | | const authData = await response.json(); |
| | | |
| | | // Check if session changed (e.g., logout in another tab) |
| | | const cachedMeta = sessionStorage.getItem(this.cacheMetaKey); |
| | | if (cachedMeta) { |
| | | const meta = JSON.parse(cachedMeta); |
| | | if (meta.session_id && meta.session_id !== authData.session_id) { |
| | | this.clearCachedAuth(); |
| | | this.notify('session-changed', {}); |
| | | } |
| | | } |
| | | |
| | | this.cacheAuth(authData); |
| | | this.setAuthData(authData); |
| | | this.initialized = true; |
| | | this.isAuthenticating = false; |
| | | |
| | | this.notify('auth-loaded', { fromCache: false }); |
| | | } |
| | | |
| | |
| | | this.authenticated = false; |
| | | this.user = null; |
| | | this.nonces = {}; |
| | | |
| | | sessionStorage.removeItem(this.storageKey); |
| | | sessionStorage.removeItem(this.cacheMetaKey ); |
| | | } |
| | | |
| | | /** |
| | | * Get cached auth data (only if cookie matches) |
| | | */ |
| | | getCachedAuth() { |
| | | try { |
| | | const cachedAuth = sessionStorage.getItem(this.storageKey); |
| | | const cacheMeta = sessionStorage.getItem(this.cacheMetaKey); |
| | | |
| | | if (!cachedAuth || !cacheMeta) { |
| | | return null; |
| | | } |
| | | |
| | | const meta = JSON.parse(cacheMeta); |
| | | const authData = JSON.parse(cachedAuth); |
| | | |
| | | // Time-based expiry (nonce freshness) |
| | | if (Date.now() - meta.timestamp > this.cacheExpiry) { |
| | | this.clearCachedAuth(); |
| | | return null; |
| | | } |
| | | |
| | | // Session changed (login/logout in another tab/window) |
| | | // We'll verify this on next fetch and clear if mismatched |
| | | |
| | | return authData; |
| | | |
| | | } catch (error) { |
| | | console.error('Error reading cached auth:', error); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Cache auth data in sessionStorage |
| | | */ |
| | | cacheAuth(authData) { |
| | | try { |
| | | sessionStorage.setItem(this.storageKey, JSON.stringify(authData)); |
| | | sessionStorage.setItem(this.cacheMetaKey, JSON.stringify({ |
| | | session_id: authData.session_id || null, |
| | | timestamp: Date.now() |
| | | })); |
| | | } catch (error) { |
| | | console.error('Error caching auth:', error); |
| | | } |
| | | } |
| | | |
| | | clearCachedAuth() { |
| | | sessionStorage.removeItem(this.storageKey); |
| | | sessionStorage.removeItem(this.cacheMetaKey); |
| | | } |
| | | |
| | | /** |
| | | * Refresh authentication (force new fetch) |
| | |
| | | * Handle successful login (call after login completes) |
| | | */ |
| | | async handleLogin(authData = null) { |
| | | // Clear old cache |
| | | sessionStorage.removeItem(this.storageKey); |
| | | sessionStorage.removeItem(this.cacheMetaKey); |
| | | |
| | | // If auth data provided, use it directly |
| | | if (authData) { |
| | | this.cacheAuth(authData); |
| | | this.setAuthData(authData); |
| | | this.initialized = true; |
| | | this.isAuthenticating = false; |
| | | this.notify('auth-loaded', { fromCache: false, fromLogin: true }); |
| | | return; |
| | | } |
| | | |
| | | // Otherwise fetch fresh (for backward compatibility) |
| | | await this.refresh(); |
| | | } |
| | | |
| | |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Wait for auth to be ready |
| | | */ |
| | | ready() { |
| | | if (this.initialized) { |
| | | return Promise.resolve(); |
| | | } |
| | | |
| | | return new Promise(resolve => { |
| | | const unsubscribe = this.subscribe((event) => { |
| | | if (event === 'auth-loaded' || event === 'auth-error') { |
| | | unsubscribe(); |
| | | resolve(); |
| | | } |
| | | }); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // Initialize global instance |
| | |
| | | register(name, configs = [], version = 1.25) { |
| | | if (!Array.isArray(configs)) configs = [configs]; |
| | | if (configs.length === 0) return; |
| | | |
| | | if (!this.dbConfig.has(name)) { |
| | | this.dbConfig.set(name, { |
| | | dbName: `${jvbBase.base}${name}`, |
| | |
| | | } |
| | | }); |
| | | |
| | | |
| | | // Initialize database asynchronously |
| | | this.initDB(name).catch(error => { |
| | | console.error(`Failed to initialize store "${name}":`, error); |
| | |
| | | this.initElements(); |
| | | this.initListeners(); |
| | | this.initStore(); |
| | | |
| | | if (this.canUpdateUI && this.ui.panel) { |
| | | this.popup = window.jvbPopup.registerPopup({ |
| | | popup: this.ui.panel, |
| | |
| | | body: requestBody |
| | | } |
| | | ); |
| | | console.log('Sending request with data: ', req); |
| | | // console.log('Sending request with data: ', req); |
| | | const result = await response.json(); |
| | | if (skip) { |
| | | operation.data = {}; |
| | | } |
| | | console.log('Result: ', result); |
| | | // console.log('Result: ', result); |
| | | if (response.ok && result.success) { |
| | | this.notify('sent-to-server', req); |
| | | if (result.id && operation.id !== result.id) { |
| | |
| | | endpoint: 'referrals/stats', |
| | | TTL: 5 * 60 * 1000, |
| | | showLoading: false, |
| | | delayFetch: false, |
| | | delayFetch: true, |
| | | filters: { |
| | | type: 'dashboard', |
| | | user: window.auth.getUser() |
| | |
| | | endpoint: 'referrals', |
| | | TTL: 10 * 60 * 1000, |
| | | showLoading: false, |
| | | delayFetch: false, |
| | | delayFetch: true, |
| | | filters: { |
| | | user: window.auth.getUser(), |
| | | status: 'all', |
| | |
| | | window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.storageKey=`${jvbBase.base}auth_state`,this.cacheMetaKey=`${jvbBase.base}auth_meta`,this.cacheExpiry=3e5,this.init()}async init(){if(this.isAuthenticating)return this.ready();this.isAuthenticating=!0;try{const t=this.getCachedAuth();if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!0});await this.fetchAuth()}catch(t){console.error("Failed to initialize auth:",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 a={...!(e.body instanceof FormData)&&{"Content-Type":"application/json"},...e.headers,"X-WP-Nonce":this.getNonce()},h=await fetch(t,{...e,credentials:"same-origin",headers:a});if((403===h.status||401===h.status)&&0===s){const t=await h.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 h};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(),i=sessionStorage.getItem(this.cacheMetaKey);if(i){const t=JSON.parse(i);t.session_id&&t.session_id!==e.session_id&&(this.clearCachedAuth(),this.notify("session-changed",{}))}this.cacheAuth(e),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||{},e&&!this.authenticated&&(window.location.href=`/login?redirect_to=${encodeURIComponent(window.location.href)}`)}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={},sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}getCachedAuth(){try{const t=sessionStorage.getItem(this.storageKey),e=sessionStorage.getItem(this.cacheMetaKey);if(!t||!e)return null;const i=JSON.parse(e),s=JSON.parse(t);return Date.now()-i.timestamp>this.cacheExpiry?(this.clearCachedAuth(),null):s}catch(t){return console.error("Error reading cached auth:",t),null}}cacheAuth(t){try{sessionStorage.setItem(this.storageKey,JSON.stringify(t)),sessionStorage.setItem(this.cacheMetaKey,JSON.stringify({session_id:t.session_id||null,timestamp:Date.now()}))}catch(t){console.error("Error caching auth:",t)}}clearCachedAuth(){sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}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(sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey),t)return this.cacheAuth(t),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)}})}ready(){return this.initialized?Promise.resolve():new Promise(t=>{const e=this.subscribe(i=>{"auth-loaded"!==i&&"auth-error"!==i||(e(),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,i={}){const e=async(s=0)=>{const h={...!(i.body instanceof FormData)&&{"Content-Type":"application/json"},...i.headers,"X-WP-Nonce":this.getNonce()},n=await fetch(t,{...i,credentials:"same-origin",headers:h});if((403===n.status||401===n.status)&&0===s){const t=await n.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 n};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||{},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)}})}}; |
| | |
| | | (()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.user=window.auth.getUser(),this.user&&(this.canUpdateUI=!0,this.isProcessing=!1,this.isPolling=!1,this.queue=new Map,this.items=new Map,this.subscribers=new Set,this.loadFromStorage=!1,this.failedFetches=0,this.api=jvbSettings.api,this.endpoint="queue",this.init())}init(){this.headers={"X-WP-Nonce":window.auth.getNonce()},this.initElements(),this.initListeners(),this.initStore(),this.canUpdateUI&&this.ui.panel&&(this.popup=window.jvbPopup.registerPopup({popup:this.ui.panel,toggle:this.ui.toggle.button,name:"Queue Panel"})),this.defineTemplates()}initElements(){this.panelStatuses=["syncing","synced","pending","offline"],this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.pendingStatuses=["queued","localProcessing","uploading"],this.workingStatuses=["pending","processing"],this.completedStatuses=["completed","failed","failed_permanent"],this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:{button:"button.qtoggle",indicator:".qtoggle .indicator",count:".qtoggle .count"},refresh:{button:"#queue .m-actions .refresh",countdown:"#queue .m-actions .refresh .countdown"},popup:{popup:"#queue .popup",message:"#queue .popup span"},items:{container:"#queue .qitems"},actions:{retry:"#queue .retry-all",clear:"#queue .dismiss-all"},filters:{filter:"#queue [data-filter]",all:{label:'#queue [for="qfilter-all"]',radio:'#queue [data-filter="all"]',count:'#queue [data-filter="all"] .count'},queued:{label:'#queue [for="qfilter-queued"]',input:'#queue [data-filter="queued"]',count:'#queue [for="qfilter-queued"] .count'},localProcessing:{label:'#queue [for="qfilter-localProcessing"]',input:'#queue [data-filter="localProcessing"]',count:'#queue [for="qfilter-localProcessing"] .count'},uploading:{label:'#queue [for="qfilter-uploading"]',input:'#queue [data-filter="uploading"]',count:'#queue [for="qfilter-uploading"] .count'},pending:{label:'#queue [for="qfilter-pending"]',input:'#queue [data-filter="pending"]',count:'#queue [for="qfilter-pending"] .count'},processing:{label:'#queue [for="qfilter-processing"]',input:'#queue [data-filter="processing"]',count:'#queue [for="qfilter-processing"] .count'},completed:{label:'#queue [for="qfilter-completed"]',input:'#queue [data-filter="completed"]',count:'#queue [for="qfilter-completed"] .count'},failed:{label:'#queue [for="qfilter-failed"]',input:'#queue [data-filter="failed"]',count:'#queue [for="qfilter-failed"] .count'}},item:{type:".type",status:".status",details:".info .details",icon:".status .icon",startedAt:".started time",completed:{wrap:".completed",label:".completed span",time:".completed time"},progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},actions:{cancel:"button.cancel",retry:"button.retry",refresh:"button.refresh",dismiss:"button.dismiss"}}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}defineTemplates(){const e=window.jvbTemplates;e.define("emptyState"),e.define("queueItem",{setup({el:e,refs:t,manyRefs:s,data:i}){e.dataset.id=i.id}})}initListeners(){this.activityListeners=null,this.clickHandler=this.handleClick.bind(this),this.onlineHandler=this.handleOnline.bind(this),this.offlineHandler=this.handleOffline.bind(this),this.unloadHandler=this.handleBeforeUnload.bind(this),this.visibilityHandler=this.handleVisibilityChange.bind(this),document.addEventListener("click",this.clickHandler),window.addEventListener("online",this.onlineHandler),window.addEventListener("offline",this.offlineHandler),document.addEventListener("visibilitychange",this.visibilityHandler)}handleOnline(){this.updatePanel("synced"),this.getQueueByStatus(this.pendingStatuses).length>0&&this.processQueue()}handleOffline(){this.updatePanel("offline")}handleVisibilityChange(e){this.isPolling&&document.hidden?this.stopPolling():this.maybeStartPolling()}handleBeforeUnload(e){if(!this.ui.panel)return;return this.getQueueByStatus(this.pendingStatuses).length>0?(e.preventDefault(),e.returnValue="",""):void 0}handleClick(e){if(!window.targetCheck(e,this.selectors.panel+", "+this.selectors.toggle.button))return;if(window.targetCheck(e,this.selectors.refresh.button))return this.ui.refresh.button.classList.add("fetching"),this.store.clearCache(),this.store.clearFilters(),void this.store.fetch().finally(()=>{this.ui.refresh.button.classList.remove("fetching")});if(window.targetCheck(e,this.selectors.actions.refresh))return void this.handleRefresh(opId);if(window.targetCheck(e,this.selectors.actions.clear))return void this.opActions("completed","dismiss").then(()=>{});if(window.targetCheck(e,this.selectors.actions.retry))return void this.opActions("failed","retry").then(()=>{});const t=window.targetCheck(e,"[data-action]");if(t){const e=t.closest("[data-id]")?.dataset.id;return void(e&&this.opActions(e,t.dataset.action))}const s=window.targetCheck(e,this.selectors.filters.filter);s&&this.setFilter(s.dataset.filter)}setFilter(e){Object.values(this.ui.filters).forEach(t=>{t.input?.dataset.filter===e&&(t.input.checked=!0)}),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}trackActivity(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map(e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}})}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout(()=>{this.processQueue()},1750)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach(({event:e,handler:t})=>{document.removeEventListener(e,t)}),this.activityListeners=null)}initStore(){if(!this.user)return;const e=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.endpoint,TTL:1/0,isAuth:!0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],filters:{user:window.auth.getUser()},showLoading:!1});this.store=e.queue,this.store.subscribe((e,t)=>{switch(e){case"data-loaded":this.store.getAll().forEach(e=>{const t=this.queue.get(e.id),s=this.mapServerOperation(e);this.queue.set(s.id,s),t&&t.status!==s.status&&this.notify("operation-status",s)}),this.maybeStartPolling(),this.updateUI();break;case"items-save":this.maybeStartPolling(),this.updateUI();break;case"item-saved":t.item&&(this.queue.set(t.item.id,t.item),t.previousItem?.status!==t.item.status&&this.notify("operation-status",t.item)),this.maybeStartPolling()}})}handleRefresh(e){const t=this.getQueue(e);if(!t)return;let s=null;if(s={content_update:t.data?.posts?Object.values(t.data.posts)[0]?.content:null,batch_creation:t.data?.content,image_upload:"uploads",video_upload:"uploads",document_upload:"uploads"}[t.type],s&&window.jvbStore){if(window.jvbStore.stores.get(s)){window.jvbStore.clearCache(s),window.jvbStore.fetch(s);const t=this.items.get(e)?.ui?.actions?.refresh;if(t){const e=t.querySelector("span").textContent;t.querySelector("span").textContent="Refreshed!",t.disabled=!0,setTimeout(()=>{t.querySelector("span").textContent=e,t.disabled=!1},2e3)}}}else confirm("Refresh the page to see changes?")&&window.location.reload()}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},delay:!1,canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),created_at:(new Date).toISOString(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return null;if(t.popup&&this.ui.popup?.message&&(this.ui.popup.message.textContent=t.popup,this.ui.popup.popup.hidden=!1,setTimeout(()=>this.ui.popup.popup.hidden=!0,2e3)),!t.delay)return this.queue.set(t.id,t),this.processOperation(t).then(()=>{}),this.store.clearCache(),this.maybeStartPolling(),this.toggleQueue(),t.id;const s=Array.from(this.getAllQueue()).filter(e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge);if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.setQueue(e),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.trackActivity(),e.id}return this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.trackActivity(),t.id}async opActions(e,t){if(this.statuses.includes(e)?e=this.getQueueByStatus(e).map(e=>e.id):"string"==typeof e&&(e=[e]),0===e.length)return;if(!["cancel","dismiss","retry"].includes(t))return;const s=["cancel","dismiss"].includes(t);s&&e.forEach(e=>{this.removeOperationUI(e)});try{const i=await window.auth.fetch(`${this.api}${this.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({action:t,ids:Array.isArray(e)?e:[e],user:this.user})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const n=await i.json();if(!n.success)throw new Error(n.message||`${t} operation failed`);return e.forEach(e=>{let i=this.getQueue(e);if(i&&this.notify(`${t}-operation`,i),s)this.clearQueue(e);else{let t=this.getQueue(e);t.status="queued",this.setQueue(t),this.updateOperationStatus(t.id,t.status)}}),"retry"===t&&this.trackActivity(),this.updateUI(),n}catch(s){return await window.jvbError.log(s,{component:"Queue",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},()=>this.opActions(e,t)),{success:!1,error:s.message}}}async processQueue(){if(this.isProcessing)return;const e=this.getQueueByStatus("queued");if(0===e.length)return void this.stopActivityTracking();this.setProcessing();for(const t of e)await this.processOperation(t);this.setProcessing(!1);0===this.getQueueByStatus("queued").length?this.stopActivityTracking():this.trackActivity(),this.toggleQueue(this.maybeStartPolling())}async processOperation(e){try{this.queue.has(e.id)||this.queue.set(e.id,e);let t,s,i=!1;if(e.data?._isFormData&&!e.data instanceof FormData&&(i=!0,e.data=await this.store.objectToFormData(e.data)),this.updateOperationStatus(e.id,"uploading"),e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",window.auth.getUser()),t=e.data,s=e.data):(s={...e.data,id:e.id,user:window.auth.getUser()},t=JSON.stringify(s),e.headers["Content-Type"]="application/json"),"unknown"===e.endpoint||null==t)return;const n=await window.auth.fetch(`${this.api}${e.endpoint}`,{method:e.method,headers:e.headers,body:t});console.log("Sending request with data: ",s);const r=await n.json();if(i&&(e.data={}),console.log("Result: ",r),!n.ok||!r.success)throw new Error(r.message||`HTTP ${n.status}`);this.notify("sent-to-server",s),r.id&&e.id!==r.id?e=await this.handleServerMerge(e,r):(e.status=r.status??"pending",e.serverData=r,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing`),this.setQueue(e)}catch(t){console.error("Operation failed: ",t),e.retries++,e.lastError=t.message,e.retries>=3?e.status="failed_permanent":e.status="failed",this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(e.status=t.status||"pending",e.serverData=t,this.mergeOp(s,e)):(this.clearQueue(e.id),this.setQueue(t),t)}mergeOp(e,t){return e.data=window.deepMerge(e.data,t.data),e.status=t.status,Object.hasOwn(t,"serverData")&&(e.serverData=t.serverData),this.updateOperationStatus(e.id,e.status),this.removeOperationUI(t.id),this.clearQueue(t.id),e}sortByDate(e){return e.sort((e,t)=>(e.updated_at??e.timestamp??0)-(t.updated_at??t.timestamp??0))}sortOperations(e){const t={processing:0,uploading:1,pending:2,queued:3,localProcessing:4,failed:5,completed:6,failed_permanent:7};return e.sort((e,s)=>{const i=(t[e.status]??99)-(t[s.status]??99);if(0!==i)return i;const n=e.updated_at??e.timestamp??0,r=s.updated_at??s.timestamp??0;return new Date(r)-new Date(n)})}getAllQueue(){let e=new Set,t=[...Array.from(this.queue.values())];return this.loadFromStorage||(this.loadFromStorage=!0,t=[...t,...Array.from(this.store.data.values())],t=t.filter(t=>{const s=e.has(t.id);return e.add(t.id),!s})),this.sortOperations(t)}getQueueByStatus(e){return"string"==typeof e&&(e=[e]),this.getAllQueue().filter(t=>e.includes(t.status))}updateOperationStatus(e,t){let s=this.getQueue(e);s&&(this.statuses.includes(t)?(s.status=t,this.notify("operation-status",s),this.setQueue(s)):console.log("Invalid status: ",t))}setQueue(e){this.store.save(e),this.queue.set(e.id,e)}getQueue(e){return this.queue.has(e)?this.queue.get(e):this.store.get(e)}clearQueue(e){this.queue.delete(e),this.store.delete(e)}maybeStartPolling(){return this.getQueueByStatus([...this.pendingStatuses,...this.workingStatuses]).length>0?(this.startPolling(),!0):(this.updatePanel("synced"),!1)}startPolling(){this.isPolling||(this.isPolling=!0,this.updatePanel("pending"),this.runPollCycle())}async runPollCycle(){if(this.isPolling){try{if(this.ui.refresh.button.classList.add("fetching"),this.store.clearCache(),429===(await this.store.fetch()).status)return console.log("Too many requests. Waiting 30 seconds"),this.stopPolling(),void this.startCountdown(30,()=>this.runPollCycle());if(this.ui.refresh.button.classList.remove("fetching"),!this.maybeStartPolling())return this.stopPolling(),void this.updatePanel("synced")}catch(e){this.stopPolling(),this.updatePanel("synced"),console.error("Polling error:",e)}this.startCountdown(5,()=>this.runPollCycle())}}startCountdown(e,t){this.ui.refresh.countdown?(this.ui.refresh.countdown.classList.add("counting"),this.ui.refresh.countdown.textContent=e,this.countdownTimer=setInterval(()=>{--e>0?this.ui.refresh.countdown.textContent=e:(this.stopCountdown(),t&&t())},1e3)):console.warn("Countdown element not found")}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.stopCountdown())}stopCountdown(){this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null),this.ui.refresh.countdown.classList.remove("counting"),this.ui.refresh.countdown.textContent=""}updateUI(){this.canUpdateUI&&window.debouncer.schedule("queue-ui",this.handleUpdateUI.bind(this))}handleUpdateUI(){const e=this.getAllQueue();this.ui.actions.retry.disabled=0===e.filter(e=>"failed"===e.status).length,this.ui.actions.clear.disabled=0===e.filter(e=>"completed"===e.status).length;let t=e.filter(e=>[...this.pendingStatuses,...this.workingStatuses].includes(e.status));t=t.length,this.ui.toggle.count.hidden=0===t,this.ui.toggle.count.textContent=t;for(let t of this.statuses){if("failed_permanent"===t)continue;let s=e.filter(e=>e.status===t).length;this.ui.filters[t].label.hidden=0===s,this.ui.filters[t].input.dataset.count=`${s}`,this.ui.filters[t].count.textContent=s>0?s:""}this.renderOperations()}renderOperations(){if(!this.ui.items.container)return;const e=this.store.filters?.status??"all",t="all"===e?this.getAllQueue():this.getQueueByStatus(e),s=this.sortOperations(t);if(0===s.length){window.removeChildren(this.ui.items.container);const e=window.jvbTemplates.create("emptyQueue");return this.ui.items.container.append(e),void this.a11y.announce("No items in queue")}this.ui.items.container.querySelector(".empty-group")?.remove();const i=new Set(s.map(e=>e.id));this.items.forEach((e,t)=>{i.has(t)||(e.element?.remove(),this.items.delete(t))}),s.forEach((e,t)=>{let s=this.items.get(e.id);s||(s=this.createOperationElement(e)),s?.element&&(this.updateOperationUI(e.id),this.ui.items.container.append(s.element))})}createOperationElement(e){const t=window.jvbTemplates.create("queueItem",e),s={element:t,ui:window.uiFromSelectors(this.selectors.item,t)};return this.items.set(e.id,s),s}updateOperationUI(e){let t=this.items.has(e)?this.items.get(e):this.createOperationElement(e);if(!t)return;let s=this.getQueue(e),i=t.element;i.classList.remove(...this.statuses),i.classList.add(s.status);let n=this.getProgress(s);t.ui.type&&t.ui.type.textContent!==s.title&&(t.ui.type.textContent=s.title),t.ui.status&&(t.ui.status.title=this.statusLabel(s.status)),t.ui.icon&&(t.ui.icon.className=`icon icon-${this.icons[s.status]}`),t.ui.details&&(t.ui.details.textContent=this.itemMessage(s)),t.ui.startedAt&&(t.ui.startedAt.setAttribute("datetime",s.created_at),t.ui.startedAt.textContent=window.formatTimeAgo(s.created_at));s.status;const r="completed"===s.status&&(s.completed_at||s.updated_at);if(t.ui.completed.wrap.hidden=!r,r){const e=s.completed_at??s.updated_at;t.ui.completed.label.textContent="Completed: ",t.ui.completed.time.setAttribute("datetime",e),t.ui.completed.time.textContent=window.formatTimeAgo(e)}window.showProgress(t.ui.progress,n,100,this.statusLabel(s.status)),t.ui.actions.cancel&&(t.ui.actions.cancel.hidden=this.completedStatuses.includes(s.status)),t.ui.actions.retry&&(s.retries>=3&&(t.ui.actions.retry.disabled=!0),t.ui.actions.retry.hidden="failed"!==s.status),t.ui.actions.dismiss&&(t.ui.actions.dismiss.hidden=this.pendingStatuses.includes(s.status)),t.ui.actions.refresh&&(t.ui.actions.refresh.hidden="completed"!==s.status)}getProgress(e){if(void 0!==e.progress_percentage)return e.progress_percentage;if(void 0!==e.progress)return e.progress;if(!this.statuses.includes(e.status))return 0;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]??0}removeOperationUI(e){let t=this.items.get(e);t&&window.fade(t.element,!1)}updatePanel(e="syncing"){this.ui.panel&&this.panelStatuses.includes(e)&&(this.ui.panel.classList.remove(...this.panelStatuses),this.ui.panel.classList.add(e))}statusLabel(e){if(!this.statuses.includes(e))return"";return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed",failed_permanent:"Failed permanently",merged:"Merged"}[e]}itemMessage(e){if(Object.hasOwn(e,"message")&&""!==e.message)return e.message;if(Object.hasOwn(e,"error_message")&&e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":if(e.count&&void 0!==e.progress_count){const t=e.progress_count,s=e.count;return`Processing ${t}/${s} items (${Math.round(t/s*100)}%)`}return void 0!==e.progress_percentage?`${e.progress_percentage}% complete`:"Processing...";case"completed":return"Successfully completed. Refresh to see changes.";case"merged":return e.merged_into?`Merged with another operation (${e.merged_into.substring(0,8)}...)`:"Merged with another operation";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/2)`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}toggleQueue(e=!0){this.ui.panel&&(this.ui.panel.hidden=!e,this.ui.toggle.button.hidden=!e)}setProcessing(e=!0){this.isProcessing=e,this.ui.toggle.button.classList.toggle("saving",e)}mapServerOperation(e){const t=this.queue.get(e.id);if(t&&t.endpoint){const s={...t,...e,endpoint:t.endpoint,method:t.method,headers:t.headers,progress_percentage:e.progress_percentage,progress_count:e.progress_count,count:e.count};e.merged_into&&this.handleMergedOperation(s)}const s=e.type?e.type.replace("_update","").replace("_","/"):"unknown",i={...e,endpoint:s,method:"POST",headers:{...this.headers}};return e.merged_into&&this.handleMergedOperation(i),i}handleMergedOperation(e){e.merged_into&&(console.log(`[Queue] Operation ${e.id} merged into ${e.merged_into}`),setTimeout(()=>{this.clearQueue(e.id),this.removeOperationFromUI(e.id)},3e3))}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach(s=>s(e,t))}destroy(){this.isPolling&&this.stopPolling(),this.stopActivityTracking(),document.removeEventListener("click",this.clickHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.jvbQueue=new e)})})})(); |
| | | (()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.user=window.auth.getUser(),this.user&&(this.canUpdateUI=!0,this.isProcessing=!1,this.isPolling=!1,this.queue=new Map,this.items=new Map,this.subscribers=new Set,this.loadFromStorage=!1,this.failedFetches=0,this.api=jvbSettings.api,this.endpoint="queue",this.init())}init(){this.headers={"X-WP-Nonce":window.auth.getNonce()},this.initElements(),this.initListeners(),this.initStore(),this.canUpdateUI&&this.ui.panel&&(this.popup=window.jvbPopup.registerPopup({popup:this.ui.panel,toggle:this.ui.toggle.button,name:"Queue Panel"})),this.defineTemplates()}initElements(){this.panelStatuses=["syncing","synced","pending","offline"],this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.pendingStatuses=["queued","localProcessing","uploading"],this.workingStatuses=["pending","processing"],this.completedStatuses=["completed","failed","failed_permanent"],this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:{button:"button.qtoggle",indicator:".qtoggle .indicator",count:".qtoggle .count"},refresh:{button:"#queue .m-actions .refresh",countdown:"#queue .m-actions .refresh .countdown"},popup:{popup:"#queue .popup",message:"#queue .popup span"},items:{container:"#queue .qitems"},actions:{retry:"#queue .retry-all",clear:"#queue .dismiss-all"},filters:{filter:"#queue [data-filter]",all:{label:'#queue [for="qfilter-all"]',radio:'#queue [data-filter="all"]',count:'#queue [data-filter="all"] .count'},queued:{label:'#queue [for="qfilter-queued"]',input:'#queue [data-filter="queued"]',count:'#queue [for="qfilter-queued"] .count'},localProcessing:{label:'#queue [for="qfilter-localProcessing"]',input:'#queue [data-filter="localProcessing"]',count:'#queue [for="qfilter-localProcessing"] .count'},uploading:{label:'#queue [for="qfilter-uploading"]',input:'#queue [data-filter="uploading"]',count:'#queue [for="qfilter-uploading"] .count'},pending:{label:'#queue [for="qfilter-pending"]',input:'#queue [data-filter="pending"]',count:'#queue [for="qfilter-pending"] .count'},processing:{label:'#queue [for="qfilter-processing"]',input:'#queue [data-filter="processing"]',count:'#queue [for="qfilter-processing"] .count'},completed:{label:'#queue [for="qfilter-completed"]',input:'#queue [data-filter="completed"]',count:'#queue [for="qfilter-completed"] .count'},failed:{label:'#queue [for="qfilter-failed"]',input:'#queue [data-filter="failed"]',count:'#queue [for="qfilter-failed"] .count'}},item:{type:".type",status:".status",details:".info .details",icon:".status .icon",startedAt:".started time",completed:{wrap:".completed",label:".completed span",time:".completed time"},progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},actions:{cancel:"button.cancel",retry:"button.retry",refresh:"button.refresh",dismiss:"button.dismiss"}}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}defineTemplates(){const e=window.jvbTemplates;e.define("emptyState"),e.define("queueItem",{setup({el:e,refs:t,manyRefs:s,data:i}){e.dataset.id=i.id}})}initListeners(){this.activityListeners=null,this.clickHandler=this.handleClick.bind(this),this.onlineHandler=this.handleOnline.bind(this),this.offlineHandler=this.handleOffline.bind(this),this.unloadHandler=this.handleBeforeUnload.bind(this),this.visibilityHandler=this.handleVisibilityChange.bind(this),document.addEventListener("click",this.clickHandler),window.addEventListener("online",this.onlineHandler),window.addEventListener("offline",this.offlineHandler),document.addEventListener("visibilitychange",this.visibilityHandler)}handleOnline(){this.updatePanel("synced"),this.getQueueByStatus(this.pendingStatuses).length>0&&this.processQueue()}handleOffline(){this.updatePanel("offline")}handleVisibilityChange(e){this.isPolling&&document.hidden?this.stopPolling():this.maybeStartPolling()}handleBeforeUnload(e){if(!this.ui.panel)return;return this.getQueueByStatus(this.pendingStatuses).length>0?(e.preventDefault(),e.returnValue="",""):void 0}handleClick(e){if(!window.targetCheck(e,this.selectors.panel+", "+this.selectors.toggle.button))return;if(window.targetCheck(e,this.selectors.refresh.button))return this.ui.refresh.button.classList.add("fetching"),this.store.clearCache(),this.store.clearFilters(),void this.store.fetch().finally(()=>{this.ui.refresh.button.classList.remove("fetching")});if(window.targetCheck(e,this.selectors.actions.refresh))return void this.handleRefresh(opId);if(window.targetCheck(e,this.selectors.actions.clear))return void this.opActions("completed","dismiss").then(()=>{});if(window.targetCheck(e,this.selectors.actions.retry))return void this.opActions("failed","retry").then(()=>{});const t=window.targetCheck(e,"[data-action]");if(t){const e=t.closest("[data-id]")?.dataset.id;return void(e&&this.opActions(e,t.dataset.action))}const s=window.targetCheck(e,this.selectors.filters.filter);s&&this.setFilter(s.dataset.filter)}setFilter(e){Object.values(this.ui.filters).forEach(t=>{t.input?.dataset.filter===e&&(t.input.checked=!0)}),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}trackActivity(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map(e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}})}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout(()=>{this.processQueue()},1750)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach(({event:e,handler:t})=>{document.removeEventListener(e,t)}),this.activityListeners=null)}initStore(){if(!this.user)return;const e=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.endpoint,TTL:1/0,isAuth:!0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],filters:{user:window.auth.getUser()},showLoading:!1});this.store=e.queue,this.store.subscribe((e,t)=>{switch(e){case"data-loaded":this.store.getAll().forEach(e=>{const t=this.queue.get(e.id),s=this.mapServerOperation(e);this.queue.set(s.id,s),t&&t.status!==s.status&&this.notify("operation-status",s)}),this.maybeStartPolling(),this.updateUI();break;case"items-save":this.maybeStartPolling(),this.updateUI();break;case"item-saved":t.item&&(this.queue.set(t.item.id,t.item),t.previousItem?.status!==t.item.status&&this.notify("operation-status",t.item)),this.maybeStartPolling()}})}handleRefresh(e){const t=this.getQueue(e);if(!t)return;let s=null;if(s={content_update:t.data?.posts?Object.values(t.data.posts)[0]?.content:null,batch_creation:t.data?.content,image_upload:"uploads",video_upload:"uploads",document_upload:"uploads"}[t.type],s&&window.jvbStore){if(window.jvbStore.stores.get(s)){window.jvbStore.clearCache(s),window.jvbStore.fetch(s);const t=this.items.get(e)?.ui?.actions?.refresh;if(t){const e=t.querySelector("span").textContent;t.querySelector("span").textContent="Refreshed!",t.disabled=!0,setTimeout(()=>{t.querySelector("span").textContent=e,t.disabled=!1},2e3)}}}else confirm("Refresh the page to see changes?")&&window.location.reload()}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},delay:!1,canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),created_at:(new Date).toISOString(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return null;if(t.popup&&this.ui.popup?.message&&(this.ui.popup.message.textContent=t.popup,this.ui.popup.popup.hidden=!1,setTimeout(()=>this.ui.popup.popup.hidden=!0,2e3)),!t.delay)return this.queue.set(t.id,t),this.processOperation(t).then(()=>{}),this.store.clearCache(),this.maybeStartPolling(),this.toggleQueue(),t.id;const s=Array.from(this.getAllQueue()).filter(e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge);if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.setQueue(e),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.trackActivity(),e.id}return this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.trackActivity(),t.id}async opActions(e,t){if(this.statuses.includes(e)?e=this.getQueueByStatus(e).map(e=>e.id):"string"==typeof e&&(e=[e]),0===e.length)return;if(!["cancel","dismiss","retry"].includes(t))return;const s=["cancel","dismiss"].includes(t);s&&e.forEach(e=>{this.removeOperationUI(e)});try{const i=await window.auth.fetch(`${this.api}${this.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({action:t,ids:Array.isArray(e)?e:[e],user:this.user})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const n=await i.json();if(!n.success)throw new Error(n.message||`${t} operation failed`);return e.forEach(e=>{let i=this.getQueue(e);if(i&&this.notify(`${t}-operation`,i),s)this.clearQueue(e);else{let t=this.getQueue(e);t.status="queued",this.setQueue(t),this.updateOperationStatus(t.id,t.status)}}),"retry"===t&&this.trackActivity(),this.updateUI(),n}catch(s){return await window.jvbError.log(s,{component:"Queue",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},()=>this.opActions(e,t)),{success:!1,error:s.message}}}async processQueue(){if(this.isProcessing)return;const e=this.getQueueByStatus("queued");if(0===e.length)return void this.stopActivityTracking();this.setProcessing();for(const t of e)await this.processOperation(t);this.setProcessing(!1);0===this.getQueueByStatus("queued").length?this.stopActivityTracking():this.trackActivity(),this.toggleQueue(this.maybeStartPolling())}async processOperation(e){try{this.queue.has(e.id)||this.queue.set(e.id,e);let t,s,i=!1;if(e.data?._isFormData&&!e.data instanceof FormData&&(i=!0,e.data=await this.store.objectToFormData(e.data)),this.updateOperationStatus(e.id,"uploading"),e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",window.auth.getUser()),t=e.data,s=e.data):(s={...e.data,id:e.id,user:window.auth.getUser()},t=JSON.stringify(s),e.headers["Content-Type"]="application/json"),"unknown"===e.endpoint||null==t)return;const n=await window.auth.fetch(`${this.api}${e.endpoint}`,{method:e.method,headers:e.headers,body:t}),r=await n.json();if(i&&(e.data={}),!n.ok||!r.success)throw new Error(r.message||`HTTP ${n.status}`);this.notify("sent-to-server",s),r.id&&e.id!==r.id?e=await this.handleServerMerge(e,r):(e.status=r.status??"pending",e.serverData=r,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing`),this.setQueue(e)}catch(t){console.error("Operation failed: ",t),e.retries++,e.lastError=t.message,e.retries>=3?e.status="failed_permanent":e.status="failed",this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(e.status=t.status||"pending",e.serverData=t,this.mergeOp(s,e)):(this.clearQueue(e.id),this.setQueue(t),t)}mergeOp(e,t){return e.data=window.deepMerge(e.data,t.data),e.status=t.status,Object.hasOwn(t,"serverData")&&(e.serverData=t.serverData),this.updateOperationStatus(e.id,e.status),this.removeOperationUI(t.id),this.clearQueue(t.id),e}sortByDate(e){return e.sort((e,t)=>(e.updated_at??e.timestamp??0)-(t.updated_at??t.timestamp??0))}sortOperations(e){const t={processing:0,uploading:1,pending:2,queued:3,localProcessing:4,failed:5,completed:6,failed_permanent:7};return e.sort((e,s)=>{const i=(t[e.status]??99)-(t[s.status]??99);if(0!==i)return i;const n=e.updated_at??e.timestamp??0,r=s.updated_at??s.timestamp??0;return new Date(r)-new Date(n)})}getAllQueue(){let e=new Set,t=[...Array.from(this.queue.values())];return this.loadFromStorage||(this.loadFromStorage=!0,t=[...t,...Array.from(this.store.data.values())],t=t.filter(t=>{const s=e.has(t.id);return e.add(t.id),!s})),this.sortOperations(t)}getQueueByStatus(e){return"string"==typeof e&&(e=[e]),this.getAllQueue().filter(t=>e.includes(t.status))}updateOperationStatus(e,t){let s=this.getQueue(e);s&&(this.statuses.includes(t)?(s.status=t,this.notify("operation-status",s),this.setQueue(s)):console.log("Invalid status: ",t))}setQueue(e){this.store.save(e),this.queue.set(e.id,e)}getQueue(e){return this.queue.has(e)?this.queue.get(e):this.store.get(e)}clearQueue(e){this.queue.delete(e),this.store.delete(e)}maybeStartPolling(){return this.getQueueByStatus([...this.pendingStatuses,...this.workingStatuses]).length>0?(this.startPolling(),!0):(this.updatePanel("synced"),!1)}startPolling(){this.isPolling||(this.isPolling=!0,this.updatePanel("pending"),this.runPollCycle())}async runPollCycle(){if(this.isPolling){try{if(this.ui.refresh.button.classList.add("fetching"),this.store.clearCache(),429===(await this.store.fetch()).status)return console.log("Too many requests. Waiting 30 seconds"),this.stopPolling(),void this.startCountdown(30,()=>this.runPollCycle());if(this.ui.refresh.button.classList.remove("fetching"),!this.maybeStartPolling())return this.stopPolling(),void this.updatePanel("synced")}catch(e){this.stopPolling(),this.updatePanel("synced"),console.error("Polling error:",e)}this.startCountdown(5,()=>this.runPollCycle())}}startCountdown(e,t){this.ui.refresh.countdown?(this.ui.refresh.countdown.classList.add("counting"),this.ui.refresh.countdown.textContent=e,this.countdownTimer=setInterval(()=>{--e>0?this.ui.refresh.countdown.textContent=e:(this.stopCountdown(),t&&t())},1e3)):console.warn("Countdown element not found")}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.stopCountdown())}stopCountdown(){this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null),this.ui.refresh.countdown.classList.remove("counting"),this.ui.refresh.countdown.textContent=""}updateUI(){this.canUpdateUI&&window.debouncer.schedule("queue-ui",this.handleUpdateUI.bind(this))}handleUpdateUI(){const e=this.getAllQueue();this.ui.actions.retry.disabled=0===e.filter(e=>"failed"===e.status).length,this.ui.actions.clear.disabled=0===e.filter(e=>"completed"===e.status).length;let t=e.filter(e=>[...this.pendingStatuses,...this.workingStatuses].includes(e.status));t=t.length,this.ui.toggle.count.hidden=0===t,this.ui.toggle.count.textContent=t;for(let t of this.statuses){if("failed_permanent"===t)continue;let s=e.filter(e=>e.status===t).length;this.ui.filters[t].label.hidden=0===s,this.ui.filters[t].input.dataset.count=`${s}`,this.ui.filters[t].count.textContent=s>0?s:""}this.renderOperations()}renderOperations(){if(!this.ui.items.container)return;const e=this.store.filters?.status??"all",t="all"===e?this.getAllQueue():this.getQueueByStatus(e),s=this.sortOperations(t);if(0===s.length){window.removeChildren(this.ui.items.container);const e=window.jvbTemplates.create("emptyQueue");return this.ui.items.container.append(e),void this.a11y.announce("No items in queue")}this.ui.items.container.querySelector(".empty-group")?.remove();const i=new Set(s.map(e=>e.id));this.items.forEach((e,t)=>{i.has(t)||(e.element?.remove(),this.items.delete(t))}),s.forEach((e,t)=>{let s=this.items.get(e.id);s||(s=this.createOperationElement(e)),s?.element&&(this.updateOperationUI(e.id),this.ui.items.container.append(s.element))})}createOperationElement(e){const t=window.jvbTemplates.create("queueItem",e),s={element:t,ui:window.uiFromSelectors(this.selectors.item,t)};return this.items.set(e.id,s),s}updateOperationUI(e){let t=this.items.has(e)?this.items.get(e):this.createOperationElement(e);if(!t)return;let s=this.getQueue(e),i=t.element;i.classList.remove(...this.statuses),i.classList.add(s.status);let n=this.getProgress(s);t.ui.type&&t.ui.type.textContent!==s.title&&(t.ui.type.textContent=s.title),t.ui.status&&(t.ui.status.title=this.statusLabel(s.status)),t.ui.icon&&(t.ui.icon.className=`icon icon-${this.icons[s.status]}`),t.ui.details&&(t.ui.details.textContent=this.itemMessage(s)),t.ui.startedAt&&(t.ui.startedAt.setAttribute("datetime",s.created_at),t.ui.startedAt.textContent=window.formatTimeAgo(s.created_at));s.status;const r="completed"===s.status&&(s.completed_at||s.updated_at);if(t.ui.completed.wrap.hidden=!r,r){const e=s.completed_at??s.updated_at;t.ui.completed.label.textContent="Completed: ",t.ui.completed.time.setAttribute("datetime",e),t.ui.completed.time.textContent=window.formatTimeAgo(e)}window.showProgress(t.ui.progress,n,100,this.statusLabel(s.status)),t.ui.actions.cancel&&(t.ui.actions.cancel.hidden=this.completedStatuses.includes(s.status)),t.ui.actions.retry&&(s.retries>=3&&(t.ui.actions.retry.disabled=!0),t.ui.actions.retry.hidden="failed"!==s.status),t.ui.actions.dismiss&&(t.ui.actions.dismiss.hidden=this.pendingStatuses.includes(s.status)),t.ui.actions.refresh&&(t.ui.actions.refresh.hidden="completed"!==s.status)}getProgress(e){if(void 0!==e.progress_percentage)return e.progress_percentage;if(void 0!==e.progress)return e.progress;if(!this.statuses.includes(e.status))return 0;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]??0}removeOperationUI(e){let t=this.items.get(e);t&&window.fade(t.element,!1)}updatePanel(e="syncing"){this.ui.panel&&this.panelStatuses.includes(e)&&(this.ui.panel.classList.remove(...this.panelStatuses),this.ui.panel.classList.add(e))}statusLabel(e){if(!this.statuses.includes(e))return"";return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed",failed_permanent:"Failed permanently",merged:"Merged"}[e]}itemMessage(e){if(Object.hasOwn(e,"message")&&""!==e.message)return e.message;if(Object.hasOwn(e,"error_message")&&e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":if(e.count&&void 0!==e.progress_count){const t=e.progress_count,s=e.count;return`Processing ${t}/${s} items (${Math.round(t/s*100)}%)`}return void 0!==e.progress_percentage?`${e.progress_percentage}% complete`:"Processing...";case"completed":return"Successfully completed. Refresh to see changes.";case"merged":return e.merged_into?`Merged with another operation (${e.merged_into.substring(0,8)}...)`:"Merged with another operation";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/2)`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}toggleQueue(e=!0){this.ui.panel&&(this.ui.panel.hidden=!e,this.ui.toggle.button.hidden=!e)}setProcessing(e=!0){this.isProcessing=e,this.ui.toggle.button.classList.toggle("saving",e)}mapServerOperation(e){const t=this.queue.get(e.id);if(t&&t.endpoint){const s={...t,...e,endpoint:t.endpoint,method:t.method,headers:t.headers,progress_percentage:e.progress_percentage,progress_count:e.progress_count,count:e.count};e.merged_into&&this.handleMergedOperation(s)}const s=e.type?e.type.replace("_update","").replace("_","/"):"unknown",i={...e,endpoint:s,method:"POST",headers:{...this.headers}};return e.merged_into&&this.handleMergedOperation(i),i}handleMergedOperation(e){e.merged_into&&(console.log(`[Queue] Operation ${e.id} merged into ${e.merged_into}`),setTimeout(()=>{this.clearQueue(e.id),this.removeOperationFromUI(e.id)},3e3))}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach(s=>s(e,t))}destroy(){this.isPolling&&this.stopPolling(),this.stopActivityTracking(),document.removeEventListener("click",this.clickHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.jvbQueue=new e)})})})(); |
| | |
| | | (()=>{class e{constructor(){this.container=document.querySelector("aside.referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.hasCopy=navigator.clipboard&&navigator.clipboard.writeText,this.initElements(),this.initStore(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copyBtn:".copy-btn",checkCode:".check-code-btn",submit:"[type=submit]",recentList:".recent-referrals-list",stats:{codeUsed:'[data-stat="code_used"]',consultations:'[data-stat="consultations"]',treatments:'[data-stat="treatments"]',rewards:'[data-stat="total_rewards"]'}},this.forms=this.container.querySelectorAll("form"),this.popup=window.jvbPopup.registerPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.bindEventListeners(!0)},onClose:()=>{this.bindEventListeners(!1)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=window.jvbTabs.registerTab(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors),this.hasCopy||document.querySelectorAll(this.selectors.copyBtn).forEach(e=>{e.remove()})}initStore(){if(!this.isLoggedIn())return;const e=window.jvbStore.register("referrals",[{storeName:"stats",keyPath:"user_id",endpoint:"referrals/stats",TTL:3e5,showLoading:!1,delayFetch:!1,filters:{type:"dashboard",user:window.auth.getUser()}},{storeName:"list",keyPath:"id",endpoint:"referrals",TTL:6e5,showLoading:!1,delayFetch:!1,filters:{user:window.auth.getUser(),status:"all",limit:50,offset:0}}]);this.statsStore=e.stats,this.listStore=e.list,this.statsStore&&this.statsStore.subscribe(this.handleStatsEvent.bind(this)),this.listStore&&this.listStore.subscribe(this.handleListEvent.bind(this))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this)}bindEventListeners(e){const t=e?"addEventListener":"removeEventListener";this.forms.forEach(e=>{e[t]("submit",this.submitHandler)}),this.container[t]("click",this.clickHandler),this.container[t]("input",this.inputHandler)}isLoggedIn(){return Boolean(window.auth.getUser())}handleStatsEvent(e,t){switch(e){case"data-loaded":t.items&&t.items.length>0&&this.updateStatsDisplay();break;case"fetch-error":console.error("Error loading stats:",t.error)}}handleListEvent(e,t){switch(e){case"data-loaded":this.ui.recentList&&this.renderRecentReferrals();break;case"fetch-error":console.error("Error loading referrals:",t.error)}}updateStatsDisplay(){if(0===!this.statsStore.data.size)return;let e=this.statsStore.data.get(parseInt(window.auth.getUser()));const t={total:e.code_used||0,treated:e.treatments||0,pending:e.pending||0,rewards:"$"+parseFloat(e.total_rewards||0).toFixed(2)};Object.entries(t).forEach(([e,t])=>{const r=this.container.querySelector(`[data-stat="${e}"]`);r&&(r.textContent=t)});const r=this.container.querySelectorAll(".stats .card");r.length>=4&&(r[0].querySelector(".stat-number").textContent=t.code_used,r[1].querySelector(".stat-number").textContent=t.consultations,r[2].querySelector(".stat-number").textContent=t.treatments,r[3].querySelector(".stat-number").textContent=t.total_rewards)}handleClick(e){const t=e.target.closest(".copy-btn, .check-code-btn, .attn");t&&(t.classList.contains("copy-btn")?this.handleCopyClick(t):t.classList.contains("check-code-btn")?this.handleCheckCode(e):t.classList.contains("attn")&&t.classList.remove("attn"))}handleCopyClick(e){const t=e.dataset.target,r=this.container.querySelector(`#${t}`);if(!r)return;const s=r.textContent.trim();this.hasCopy&&navigator.clipboard.writeText(s).then(()=>{e.classList.toggle("success"),setTimeout(()=>{e.classList.remove("success")},1500)})}handleError(e,t){const{message:r,code:s,field:a}=t;switch(a?this.showFieldError(e,a,r):this.showFormStatus(e,"error",r||"Something went wrong. Please try again."),s){case"duplicate_email":break;case"invalid_code":const t=e.querySelector('[name="referral_code"]');t&&(t.readOnly=!1,t.focus());break;case"turnstile_failed":window.turnstile&&e.querySelector(".cf-turnstile")&&window.turnstile.reset()}}showFieldError(e,t,r){let s=e.querySelector(`.field[data-field="${t}"]`);if(s||(s=e.querySelector(`.field[data-field="referral_${t}"]`)),!s)return void this.showFormStatus(e,"error",r);const a=s.querySelector("input, textarea, select"),i=s.querySelector(".validation-message"),n=s.querySelector(".validation-icon.error"),o=s.querySelector(".validation-icon.success");a?(s.classList.remove("has-success"),s.classList.add("has-error"),a.classList.add("error"),a.setAttribute("aria-invalid","true"),n&&(n.hidden=!1),o&&(o.hidden=!0),i&&(i.textContent=r,i.hidden=!1),a.focus(),this.a11y?.announce(`Error in ${t}: ${r}`)):this.showFormStatus(e,"error",r)}showFormStatus(e,t,r=""){const s=e.querySelector(".fstatus");if(!s)return void console.warn("No .fstatus element found in form");s.hidden=!1;const a=s.querySelector(".message");s.querySelector(".icon")?.remove(),s.querySelector(".actions")?.remove();const i={saving:"Sending...",submitted:"Sent successfully!",error:"Something went wrong",checking:"Checking code..."},n={submitted:"check-circle",error:"close-circle",checking:"loading"};if(n[t]&&window.getIcon){const e=window.getIcon(n[t]);e&&s.prepend(e)}a&&(a.textContent=r||i[t]||t),s.classList.toggle("loading",["saving","checking"].includes(t)),"submitted"===t&&setTimeout(()=>s.hidden=!0,3e3),this.a11y&&this.a11y.announce(r||i[t]||t)}clearFormErrors(e){e.querySelectorAll(".field.has-error, .field.has-success").forEach(e=>{this.clearFieldValidation(e)});const t=e.querySelector(".fstatus");t&&(t.hidden=!0)}clearFieldValidation(e){if(!e)return;const t=e.querySelector("input, textarea, select"),r=e.querySelector(".validation-message"),s=e.querySelectorAll(".validation-icon");e.classList.remove("has-error","has-success"),t&&(t.classList.remove("error"),t.removeAttribute("aria-invalid")),s.forEach(e=>e.hidden=!0),r&&(r.hidden=!0,r.textContent="")}handleInput(e){"referral_code"!==e.target.id&&"referral_code"!==e.target.name||(e.target.value=e.target.value.toUpperCase());const t=e.target.closest(".field");t&&t.classList.contains("has-error")&&this.clearFieldValidation(t)}async handleCheckCode(e){e.preventDefault();const t=e.target.closest("form"),r=t.querySelector('[name="referral_code"]'),s=t.querySelector(".code-status");if(!r||!s)return;const a=r.value.trim();if(a){s.hidden=!1,s.className="code-status loading",s.innerHTML='<span class="spinner"></span> Checking...';try{const e=await this.validateCodeOnly(a);e.success?this.showCodeStatus(s,`✓ Valid! Referred by ${e.referrer_name}`,"success"):this.showCodeStatus(s,e.message||"Invalid code","error")}catch(e){console.error("Error checking code:",e),this.showCodeStatus(s,"Error checking code","error")}}else this.showCodeStatus(s,"Please enter a code","error")}showCodeStatus(e,t,r){e.hidden=!1,e.className=`code-status ${r}`,e.textContent=t,"error"===r&&setTimeout(()=>{e.hidden=!0},5e3)}async checkForReferral(){const e=this.getUrlParameter("ref"),t=this.getUrlParameter("rname"),r=this.getUrlParameter("remail"),s=this.getUrlParameter("seeReferral");if(!e&&!s)return;if(s&&!e)return this.popup.openPopup(),void this.removeUrlParameter("seeReferral");const a=this.container.querySelector('[name="referral_code"]');if(!a)return;const i=e.toUpperCase();if(a.value=i,a.readOnly=!0,t||r){const e=this.container.querySelector('[name="referral_name"]');e&&(e.value=t);const s=this.container.querySelector('[name="referral_email"]');s&&(s.value=r)}this.popup.openPopup();try{const e=await this.validateCodeOnly(i);if(e.success){const t=a.closest("form").querySelector(".code-status");t&&this.showCodeStatus(t,`✓ ${e.referrer_name} invited you!`,"success");const r=this.container.querySelector('[name="referral_name"]');r&&!r.value&&r.focus()}else a.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),a.readOnly=!1}this.removeUrlParameter("ref"),this.removeUrlParameter("rname"),this.removeUrlParameter("remail")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}referrals/code`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({code:e})});return await t.json()}renderRecentReferrals(){let e=this.ui.recentList,t=Array.from(this.listStore.data.values());t&&0!==t.length?e.innerHTML=t.map(e=>`\n\t\t\t<div class="referral-item">\n\t\t\t\t<div class="referral-info">\n\t\t\t\t\t<strong>${window.escapeHtml(e.referee_name)}</strong>\n\t\t\t\t\t<span class="status-badge">${e.referral_status}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class="referral-date">${window.formatTimeAgo(e.referred_at)}</div>\n\t\t\t</div>\n\t\t`).join(""):e.innerHTML='<p class="no-referrals">Share your code to get started!</p>'}async handleFormSubmit(e){e.preventDefault();const t=e.target,r=new FormData(t);this.clearFormErrors(t),this.setFormLoading(!0,t);try{let e={success:!1,message:""};if("referral-code-form"===t.id){let s={name:r.get("referral_name"),email:r.get("referral_email"),referral_code:r.get("referral_code")};const a=t.querySelector('input[name="cf-turnstile-response"]');a&&a.value&&(s["cf-turnstile-response"]=a.value),s.name&&s.email&&s.referral_code?e=await this.makeRequest("auth/register",s):e.message="Please fill in all fields"}else if("login-form"===t.id){let s={type:"login",user_email:r.get("login_email"),context:{redirect_to:window.location.href+"?seeReferral=1"}};const a=t.querySelector('input[name="cf-turnstile-response"]');a&&a.value&&(s["cf-turnstile-response"]=a.value),s.user_email?e=await this.makeRequest("auth/magic",s):e.message="Please fill in your email"}e.success?this.handleSuccess(t,e):this.handleError(t,e)}catch(e){console.error("Error submitting form:",e),this.showFormMessage(t,"Something went wrong. Please try again.","error")}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["auth/magic","auth/register"].includes(e))return{success:!1,message:"Invalid endpoint"};const r=await window.auth.fetch(`${jvbSettings.api}${e}`,{method:"POST",body:JSON.stringify(t)});if(!r.ok){const e=await r.text();console.error("Error response:",r.status,e);try{return JSON.parse(e)}catch{return{success:!1,message:"Server error"}}}return await r.json()}handleSuccess(e,t){e.style.display="none";const r=e.nextElementSibling;r&&r.classList.contains("success-content")&&(r.hidden=!1,r.scrollIntoView({behavior:"smooth",block:"center"})),this.dispatchEvent("emailSent",{email:t.email})}showFormMessage(e,t,r="error"){const s=e.querySelector(".status");if(!s)return;const a=s.querySelector(".message");a&&(a.textContent=t),s.hidden=!1,s.className=`status ${r}`,"error"===r&&setTimeout(()=>{s.hidden=!0},5e3)}setFormLoading(e,t){t.querySelectorAll("input, button, textarea, select").forEach(t=>t.disabled=e),e&&this.showFormStatus(t,"saving")}dispatchEvent(e,t){const r=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(r)}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.jvbReferral=new e)})})})(); |
| | | (()=>{class e{constructor(){this.container=document.querySelector("aside.referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.hasCopy=navigator.clipboard&&navigator.clipboard.writeText,this.initElements(),this.initStore(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copyBtn:".copy-btn",checkCode:".check-code-btn",submit:"[type=submit]",recentList:".recent-referrals-list",stats:{codeUsed:'[data-stat="code_used"]',consultations:'[data-stat="consultations"]',treatments:'[data-stat="treatments"]',rewards:'[data-stat="total_rewards"]'}},this.forms=this.container.querySelectorAll("form"),this.popup=window.jvbPopup.registerPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.bindEventListeners(!0)},onClose:()=>{this.bindEventListeners(!1)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=window.jvbTabs.registerTab(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors),this.hasCopy||document.querySelectorAll(this.selectors.copyBtn).forEach(e=>{e.remove()})}initStore(){if(!this.isLoggedIn())return;const e=window.jvbStore.register("referrals",[{storeName:"stats",keyPath:"user_id",endpoint:"referrals/stats",TTL:3e5,showLoading:!1,delayFetch:!0,filters:{type:"dashboard",user:window.auth.getUser()}},{storeName:"list",keyPath:"id",endpoint:"referrals",TTL:6e5,showLoading:!1,delayFetch:!0,filters:{user:window.auth.getUser(),status:"all",limit:50,offset:0}}]);this.statsStore=e.stats,this.listStore=e.list,this.statsStore&&this.statsStore.subscribe(this.handleStatsEvent.bind(this)),this.listStore&&this.listStore.subscribe(this.handleListEvent.bind(this))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this)}bindEventListeners(e){const t=e?"addEventListener":"removeEventListener";this.forms.forEach(e=>{e[t]("submit",this.submitHandler)}),this.container[t]("click",this.clickHandler),this.container[t]("input",this.inputHandler)}isLoggedIn(){return Boolean(window.auth.getUser())}handleStatsEvent(e,t){switch(e){case"data-loaded":t.items&&t.items.length>0&&this.updateStatsDisplay();break;case"fetch-error":console.error("Error loading stats:",t.error)}}handleListEvent(e,t){switch(e){case"data-loaded":this.ui.recentList&&this.renderRecentReferrals();break;case"fetch-error":console.error("Error loading referrals:",t.error)}}updateStatsDisplay(){if(0===!this.statsStore.data.size)return;let e=this.statsStore.data.get(parseInt(window.auth.getUser()));const t={total:e.code_used||0,treated:e.treatments||0,pending:e.pending||0,rewards:"$"+parseFloat(e.total_rewards||0).toFixed(2)};Object.entries(t).forEach(([e,t])=>{const r=this.container.querySelector(`[data-stat="${e}"]`);r&&(r.textContent=t)});const r=this.container.querySelectorAll(".stats .card");r.length>=4&&(r[0].querySelector(".stat-number").textContent=t.code_used,r[1].querySelector(".stat-number").textContent=t.consultations,r[2].querySelector(".stat-number").textContent=t.treatments,r[3].querySelector(".stat-number").textContent=t.total_rewards)}handleClick(e){const t=e.target.closest(".copy-btn, .check-code-btn, .attn");t&&(t.classList.contains("copy-btn")?this.handleCopyClick(t):t.classList.contains("check-code-btn")?this.handleCheckCode(e):t.classList.contains("attn")&&t.classList.remove("attn"))}handleCopyClick(e){const t=e.dataset.target,r=this.container.querySelector(`#${t}`);if(!r)return;const s=r.textContent.trim();this.hasCopy&&navigator.clipboard.writeText(s).then(()=>{e.classList.toggle("success"),setTimeout(()=>{e.classList.remove("success")},1500)})}handleError(e,t){const{message:r,code:s,field:a}=t;switch(a?this.showFieldError(e,a,r):this.showFormStatus(e,"error",r||"Something went wrong. Please try again."),s){case"duplicate_email":break;case"invalid_code":const t=e.querySelector('[name="referral_code"]');t&&(t.readOnly=!1,t.focus());break;case"turnstile_failed":window.turnstile&&e.querySelector(".cf-turnstile")&&window.turnstile.reset()}}showFieldError(e,t,r){let s=e.querySelector(`.field[data-field="${t}"]`);if(s||(s=e.querySelector(`.field[data-field="referral_${t}"]`)),!s)return void this.showFormStatus(e,"error",r);const a=s.querySelector("input, textarea, select"),i=s.querySelector(".validation-message"),n=s.querySelector(".validation-icon.error"),o=s.querySelector(".validation-icon.success");a?(s.classList.remove("has-success"),s.classList.add("has-error"),a.classList.add("error"),a.setAttribute("aria-invalid","true"),n&&(n.hidden=!1),o&&(o.hidden=!0),i&&(i.textContent=r,i.hidden=!1),a.focus(),this.a11y?.announce(`Error in ${t}: ${r}`)):this.showFormStatus(e,"error",r)}showFormStatus(e,t,r=""){const s=e.querySelector(".fstatus");if(!s)return void console.warn("No .fstatus element found in form");s.hidden=!1;const a=s.querySelector(".message");s.querySelector(".icon")?.remove(),s.querySelector(".actions")?.remove();const i={saving:"Sending...",submitted:"Sent successfully!",error:"Something went wrong",checking:"Checking code..."},n={submitted:"check-circle",error:"close-circle",checking:"loading"};if(n[t]&&window.getIcon){const e=window.getIcon(n[t]);e&&s.prepend(e)}a&&(a.textContent=r||i[t]||t),s.classList.toggle("loading",["saving","checking"].includes(t)),"submitted"===t&&setTimeout(()=>s.hidden=!0,3e3),this.a11y&&this.a11y.announce(r||i[t]||t)}clearFormErrors(e){e.querySelectorAll(".field.has-error, .field.has-success").forEach(e=>{this.clearFieldValidation(e)});const t=e.querySelector(".fstatus");t&&(t.hidden=!0)}clearFieldValidation(e){if(!e)return;const t=e.querySelector("input, textarea, select"),r=e.querySelector(".validation-message"),s=e.querySelectorAll(".validation-icon");e.classList.remove("has-error","has-success"),t&&(t.classList.remove("error"),t.removeAttribute("aria-invalid")),s.forEach(e=>e.hidden=!0),r&&(r.hidden=!0,r.textContent="")}handleInput(e){"referral_code"!==e.target.id&&"referral_code"!==e.target.name||(e.target.value=e.target.value.toUpperCase());const t=e.target.closest(".field");t&&t.classList.contains("has-error")&&this.clearFieldValidation(t)}async handleCheckCode(e){e.preventDefault();const t=e.target.closest("form"),r=t.querySelector('[name="referral_code"]'),s=t.querySelector(".code-status");if(!r||!s)return;const a=r.value.trim();if(a){s.hidden=!1,s.className="code-status loading",s.innerHTML='<span class="spinner"></span> Checking...';try{const e=await this.validateCodeOnly(a);e.success?this.showCodeStatus(s,`✓ Valid! Referred by ${e.referrer_name}`,"success"):this.showCodeStatus(s,e.message||"Invalid code","error")}catch(e){console.error("Error checking code:",e),this.showCodeStatus(s,"Error checking code","error")}}else this.showCodeStatus(s,"Please enter a code","error")}showCodeStatus(e,t,r){e.hidden=!1,e.className=`code-status ${r}`,e.textContent=t,"error"===r&&setTimeout(()=>{e.hidden=!0},5e3)}async checkForReferral(){const e=this.getUrlParameter("ref"),t=this.getUrlParameter("rname"),r=this.getUrlParameter("remail"),s=this.getUrlParameter("seeReferral");if(!e&&!s)return;if(s&&!e)return this.popup.openPopup(),void this.removeUrlParameter("seeReferral");const a=this.container.querySelector('[name="referral_code"]');if(!a)return;const i=e.toUpperCase();if(a.value=i,a.readOnly=!0,t||r){const e=this.container.querySelector('[name="referral_name"]');e&&(e.value=t);const s=this.container.querySelector('[name="referral_email"]');s&&(s.value=r)}this.popup.openPopup();try{const e=await this.validateCodeOnly(i);if(e.success){const t=a.closest("form").querySelector(".code-status");t&&this.showCodeStatus(t,`✓ ${e.referrer_name} invited you!`,"success");const r=this.container.querySelector('[name="referral_name"]');r&&!r.value&&r.focus()}else a.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),a.readOnly=!1}this.removeUrlParameter("ref"),this.removeUrlParameter("rname"),this.removeUrlParameter("remail")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}referrals/code`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({code:e})});return await t.json()}renderRecentReferrals(){let e=this.ui.recentList,t=Array.from(this.listStore.data.values());t&&0!==t.length?e.innerHTML=t.map(e=>`\n\t\t\t<div class="referral-item">\n\t\t\t\t<div class="referral-info">\n\t\t\t\t\t<strong>${window.escapeHtml(e.referee_name)}</strong>\n\t\t\t\t\t<span class="status-badge">${e.referral_status}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class="referral-date">${window.formatTimeAgo(e.referred_at)}</div>\n\t\t\t</div>\n\t\t`).join(""):e.innerHTML='<p class="no-referrals">Share your code to get started!</p>'}async handleFormSubmit(e){e.preventDefault();const t=e.target,r=new FormData(t);this.clearFormErrors(t),this.setFormLoading(!0,t);try{let e={success:!1,message:""};if("referral-code-form"===t.id){let s={name:r.get("referral_name"),email:r.get("referral_email"),referral_code:r.get("referral_code")};const a=t.querySelector('input[name="cf-turnstile-response"]');a&&a.value&&(s["cf-turnstile-response"]=a.value),s.name&&s.email&&s.referral_code?e=await this.makeRequest("auth/register",s):e.message="Please fill in all fields"}else if("login-form"===t.id){let s={type:"login",user_email:r.get("login_email"),context:{redirect_to:window.location.href+"?seeReferral=1"}};const a=t.querySelector('input[name="cf-turnstile-response"]');a&&a.value&&(s["cf-turnstile-response"]=a.value),s.user_email?e=await this.makeRequest("auth/magic",s):e.message="Please fill in your email"}e.success?this.handleSuccess(t,e):this.handleError(t,e)}catch(e){console.error("Error submitting form:",e),this.showFormMessage(t,"Something went wrong. Please try again.","error")}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["auth/magic","auth/register"].includes(e))return{success:!1,message:"Invalid endpoint"};const r=await window.auth.fetch(`${jvbSettings.api}${e}`,{method:"POST",body:JSON.stringify(t)});if(!r.ok){const e=await r.text();console.error("Error response:",r.status,e);try{return JSON.parse(e)}catch{return{success:!1,message:"Server error"}}}return await r.json()}handleSuccess(e,t){e.style.display="none";const r=e.nextElementSibling;r&&r.classList.contains("success-content")&&(r.hidden=!1,r.scrollIntoView({behavior:"smooth",block:"center"})),this.dispatchEvent("emailSent",{email:t.email})}showFormMessage(e,t,r="error"){const s=e.querySelector(".status");if(!s)return;const a=s.querySelector(".message");a&&(a.textContent=t),s.hidden=!1,s.className=`status ${r}`,"error"===r&&setTimeout(()=>{s.hidden=!0},5e3)}setFormLoading(e,t){t.querySelectorAll("input, button, textarea, select").forEach(t=>t.disabled=e),e&&this.showFormStatus(t,"saving")}dispatchEvent(e,t){const r=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(r)}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.jvbReferral=new e)})})})(); |
| | |
| | | "feed", |
| | | "grid" |
| | | ], |
| | | "version": "0.9.0", |
| | | "version": "1.0.0", |
| | | "textdomain": "jvb", |
| | | "supports": { |
| | | "html": false, |
| | |
| | | .feed-block{grid-column:full}.feed-block .placeholder{align-items:center;aspect-ratio:1;background:rgb(var(--base));border:1rem solid rgb(var(--base-50));border-radius:1rem;display:flex;justify-content:center;--w:50%;color:rgb(var(--base-200))}.feed-block .placeholder i.icon{animation:dance 2.5s ease-in-out infinite}.feed-block .item-grid{max-width:var(--full)}.feed-block .item-grid:has([data-timeline]){grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}.feed-block .item{background:rgb(var(--base-50));box-shadow:rgba(var(--base),var(--op-2)) var(--shdw);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0}.feed-block .item h3{font-size:var(--txt-medium);margin:0}.feed-block .item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed-block .item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base),var(--op-2));right:0;position:absolute;top:calc(var(--chip_)*-1);width:100%}.feed-block .item details summary:hover{background-color:rgba(var(--action-0),var(--op-45))}.feed-block .item details[open]{padding:.25rem .5rem}.feed-block .item img:hover{opacity:.8}.feed-block .item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed-block .item[data-timeline] .images span{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast));padding:.25rem .5rem;position:absolute;width:50%}.feed-block .item[data-timeline] .images span:first-of-type{bottom:0;left:50%;text-align:left}.feed-block .item[data-timeline] .images span:last-of-type{right:50%;top:0}.feed-block .item[data-timeline] .images img{width:50%}.feed-block .item[data-timeline] .images img:first-of-type{border-left:2px solid rgb(var(--action-0))}.feed-block .item a:after,.feed-block .item a:before{display:none}.feed-block .item label{font-weight:400;text-transform:none;--w:1.5em}.feed-block .all-filters summary{display:flex;justify-content:space-between}.all-filters{font-size:var(--txt-x-small)}.all-filters[open]{border:2px solid rgb(var(--action-0));border-radius:0 0 var(--radius-outer) var(--radius-outer);padding:0}.all-filters summary{width:100%}.all-filters summary:hover,.all-filters[open] summary{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}.all-filters summary:hover:after,.all-filters[open] summary:after{background-color:rgb(var(--action-contrast))}.all-filters>.row.row{padding:0 .75rem 2rem;width:var(--content)}.all-filters>.row.row.search{padding-bottom:0}.all-filters>.row.row{position:relative}.all-filters>.row.row>.label,.all-filters>.row.row>.row>.label{font-family:var(--heading);font-weight:var(--fw-h-bold);text-transform:uppercase}.all-filters>.row.row>.label{width:20%}.all-filters>.row.row>.row>.label{white-space:nowrap}.all-filters .btn+label,.all-filters button{min-height:var(--chipchip);padding:0;width:var(--chipchip)}.all-filters .btn+label .label,.all-filters .row:has(>.btn:not(:checked)+label:hover) :checked+label .label,.all-filters button .label{bottom:-2rem;opacity:0;position:absolute;white-space:nowrap;width:-moz-max-content;width:max-content;z-index:var(--z-4)}.all-filters .btn+label:hover .label,.all-filters .btn:checked+label .label,.all-filters button:hover .label{opacity:1}.all-filters .search.row,.all-filters .view.row{display:none}.all-filters .ordering{padding:2rem 0 .75rem}.all-filters .ordering>.row label{position:unset}.all-filters .ordering .row .label{color:rgb(var(--contrast));top:-1rem}.all-filters .ordering .row.orderby .label{right:0}.all-filters .ordering .row.order-direction .label{left:0}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{padding:0;transform:scaleX(0);transform-origin:right;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base);width:0}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:right;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.toggle-text input+label{border:1px dashed transparent;color:var(--contrast)!important;cursor:pointer;font-weight:400;padding:.25rem .5rem;position:relative;text-transform:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.toggle-text input+label:after,.toggle-text input+label:before{display:none!important}.toggle-text input+label .text{margin:0;position:relative;--gap:0;border:1px solid rgb(var(--action-50));border-radius:var(--radius);color:rgb(var(--action-50));font-weight:700;padding:2px 4px;width:-moz-fit-content;width:fit-content}.toggle-text input+label .off{--mid:-100%}.toggle-text input+label .on{--mid:100%}.toggle-text input+label .off,.toggle-text input+label .on{transition:var(--trans-transform),opacity var(--trans-base)}.toggle-text input+label .off,.toggle-text input:checked+label .on{max-width:100%;opacity:1;transform:translateZ(0)}.toggle-text input+label .on,.toggle-text input:checked+label .off{max-width:0;opacity:0;transform:translate3d(0,var(--mid),0)}.toggle-text:hover label{border-color:rgb(var(--action-200))}.toggle-text:hover .text{background-color:rgb(var(--action-50));border-color:rgb(var(--action-50));color:rgb(var(--action-contrast))} |
| | | .feed-block{grid-column:full}.feed-block .placeholder{align-items:center;aspect-ratio:1;background:rgb(var(--base));border:1rem solid rgb(var(--base-50));border-radius:1rem;display:flex;justify-content:center;--w:50%;color:rgb(var(--base-200))}.feed-block .placeholder i.icon{animation:dance 2.5s ease-in-out infinite}.feed-block .item-grid{max-width:var(--full)}.feed-block .item-grid:has([data-timeline]){grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}.feed-block .item{background:rgb(var(--base-50));box-shadow:rgba(var(--base),var(--op-2)) var(--shdw);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0}.feed-block .item h3{font-size:var(--txt-medium);margin:0}.feed-block .item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed-block .item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base),var(--op-2));right:0;position:absolute;top:calc(var(--chip_)*-1);width:100%}.feed-block .item details summary:hover{background-color:rgba(var(--action-0),var(--op-45))}.feed-block .item details[open]{padding:.25rem .5rem}.feed-block .item img:hover{opacity:.8}.feed-block .item ul{margin:0}.feed-block .item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed-block .item[data-timeline] .images span{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast));padding:.25rem .5rem;position:absolute;width:50%}.feed-block .item[data-timeline] .images span:first-of-type{bottom:0;left:50%;text-align:left}.feed-block .item[data-timeline] .images span:last-of-type{right:50%;top:0}.feed-block .item[data-timeline] .images img{width:50%}.feed-block .item[data-timeline] .images img:first-of-type{border-left:2px solid rgb(var(--action-0))}.feed-block .item a:after,.feed-block .item a:before{display:none}.feed-block .item label{font-weight:400;text-transform:none;--w:1.5em}.feed-block .all-filters summary{display:flex;justify-content:space-between}.all-filters{font-size:var(--txt-x-small)}.all-filters[open]{border:2px solid rgb(var(--action-0));border-radius:0 0 var(--radius-outer) var(--radius-outer);padding:0}.all-filters summary{width:100%}.all-filters summary:hover,.all-filters[open] summary{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}.all-filters summary:hover:after,.all-filters[open] summary:after{background-color:rgb(var(--action-contrast))}.all-filters>.row.row{padding:0 .75rem 2rem;width:var(--content)}.all-filters>.row.row.search{padding-bottom:0}.all-filters>.row.row{position:relative}.all-filters>.row.row>.label,.all-filters>.row.row>.row>.label{font-family:var(--heading);font-weight:var(--fw-h-bold);text-transform:uppercase}.all-filters>.row.row>.label{width:20%}.all-filters>.row.row>.row>.label{white-space:nowrap}.all-filters .btn+label,.all-filters button{min-height:var(--chipchip);padding:0;width:var(--chipchip)}.all-filters .btn+label .label,.all-filters .row:has(>.btn:not(:checked)+label:hover) :checked+label .label,.all-filters button .label{bottom:-2rem;opacity:0;position:absolute;white-space:nowrap;width:-moz-max-content;width:max-content;z-index:var(--z-4)}.all-filters .btn+label:hover .label,.all-filters .btn:checked+label .label,.all-filters button:hover .label{opacity:1}.all-filters .search.row,.all-filters .view.row{display:none}.all-filters .ordering{padding:2rem 0 .75rem}.all-filters .ordering>.row label{position:unset}.all-filters .ordering .row .label{color:rgb(var(--contrast));top:-1rem}.all-filters .ordering .row.orderby .label{right:0}.all-filters .ordering .row.order-direction .label{left:0}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{padding:0;transform:scaleX(0);transform-origin:right;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base);width:0}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:right;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.toggle-text input+label{border:1px dashed transparent;color:var(--contrast)!important;cursor:pointer;font-weight:400;padding:.25rem .5rem;position:relative;text-transform:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.toggle-text input+label:after,.toggle-text input+label:before{display:none!important}.toggle-text input+label .text{margin:0;position:relative;--gap:0;border:1px solid rgb(var(--action-50));border-radius:var(--radius);color:rgb(var(--action-50));font-weight:700;padding:2px 4px;width:-moz-fit-content;width:fit-content}.toggle-text input+label .off{--mid:-100%}.toggle-text input+label .on{--mid:100%}.toggle-text input+label .off,.toggle-text input+label .on{transition:var(--trans-transform),opacity var(--trans-base)}.toggle-text input+label .off,.toggle-text input:checked+label .on{max-width:100%;opacity:1;transform:translateZ(0)}.toggle-text input+label .on,.toggle-text input:checked+label .off{max-width:0;opacity:0;transform:translate3d(0,var(--mid),0)}.toggle-text:hover label{border-color:rgb(var(--action-200))}.toggle-text:hover .text{background-color:rgb(var(--action-50));border-color:rgb(var(--action-50));color:rgb(var(--action-contrast))} |
| | |
| | | .feed-block{grid-column:full}.feed-block .placeholder{align-items:center;aspect-ratio:1;background:rgb(var(--base));border:1rem solid rgb(var(--base-50));border-radius:1rem;display:flex;justify-content:center;--w:50%;color:rgb(var(--base-200))}.feed-block .placeholder i.icon{animation:dance 2.5s ease-in-out infinite}.feed-block .item-grid{max-width:var(--full)}.feed-block .item-grid:has([data-timeline]){grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}.feed-block .item{background:rgb(var(--base-50));box-shadow:rgba(var(--base),var(--op-2)) var(--shdw);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0}.feed-block .item h3{font-size:var(--txt-medium);margin:0}.feed-block .item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed-block .item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base),var(--op-2));left:0;position:absolute;top:calc(var(--chip_)*-1);width:100%}.feed-block .item details summary:hover{background-color:rgba(var(--action-0),var(--op-45))}.feed-block .item details[open]{padding:.25rem .5rem}.feed-block .item img:hover{opacity:.8}.feed-block .item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed-block .item[data-timeline] .images span{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast));padding:.25rem .5rem;position:absolute;width:50%}.feed-block .item[data-timeline] .images span:first-of-type{bottom:0;right:50%;text-align:right}.feed-block .item[data-timeline] .images span:last-of-type{left:50%;top:0}.feed-block .item[data-timeline] .images img{width:50%}.feed-block .item[data-timeline] .images img:first-of-type{border-right:2px solid rgb(var(--action-0))}.feed-block .item a:after,.feed-block .item a:before{display:none}.feed-block .item label{font-weight:400;text-transform:none;--w:1.5em}.feed-block .all-filters summary{display:flex;justify-content:space-between}.all-filters{font-size:var(--txt-x-small)}.all-filters[open]{border:2px solid rgb(var(--action-0));border-radius:0 0 var(--radius-outer) var(--radius-outer);padding:0}.all-filters summary{width:100%}.all-filters summary:hover,.all-filters[open] summary{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}.all-filters summary:hover:after,.all-filters[open] summary:after{background-color:rgb(var(--action-contrast))}.all-filters>.row.row{padding:0 .75rem 2rem;width:var(--content)}.all-filters>.row.row.search{padding-bottom:0}.all-filters>.row.row{position:relative}.all-filters>.row.row>.label,.all-filters>.row.row>.row>.label{font-family:var(--heading);font-weight:var(--fw-h-bold);text-transform:uppercase}.all-filters>.row.row>.label{width:20%}.all-filters>.row.row>.row>.label{white-space:nowrap}.all-filters .btn+label,.all-filters button{min-height:var(--chipchip);padding:0;width:var(--chipchip)}.all-filters .btn+label .label,.all-filters .row:has(>.btn:not(:checked)+label:hover) :checked+label .label,.all-filters button .label{bottom:-2rem;opacity:0;position:absolute;white-space:nowrap;width:-moz-max-content;width:max-content;z-index:var(--z-4)}.all-filters .btn+label:hover .label,.all-filters .btn:checked+label .label,.all-filters button:hover .label{opacity:1}.all-filters .search.row,.all-filters .view.row{display:none}.all-filters .ordering{padding:2rem 0 .75rem}.all-filters .ordering>.row label{position:unset}.all-filters .ordering .row .label{color:rgb(var(--contrast));top:-1rem}.all-filters .ordering .row.orderby .label{left:0}.all-filters .ordering .row.order-direction .label{right:0}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{padding:0;transform:scaleX(0);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base);width:0}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.toggle-text input+label{border:1px dashed transparent;color:var(--contrast)!important;cursor:pointer;font-weight:400;padding:.25rem .5rem;position:relative;text-transform:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.toggle-text input+label:after,.toggle-text input+label:before{display:none!important}.toggle-text input+label .text{margin:0;position:relative;--gap:0;border:1px solid rgb(var(--action-50));border-radius:var(--radius);color:rgb(var(--action-50));font-weight:700;padding:2px 4px;width:-moz-fit-content;width:fit-content}.toggle-text input+label .off{--mid:-100%}.toggle-text input+label .on{--mid:100%}.toggle-text input+label .off,.toggle-text input+label .on{transition:var(--trans-transform),opacity var(--trans-base)}.toggle-text input+label .off,.toggle-text input:checked+label .on{max-width:100%;opacity:1;transform:translateZ(0)}.toggle-text input+label .on,.toggle-text input:checked+label .off{max-width:0;opacity:0;transform:translate3d(0,var(--mid),0)}.toggle-text:hover label{border-color:rgb(var(--action-200))}.toggle-text:hover .text{background-color:rgb(var(--action-50));border-color:rgb(var(--action-50));color:rgb(var(--action-contrast))} |
| | | .feed-block{grid-column:full}.feed-block .placeholder{align-items:center;aspect-ratio:1;background:rgb(var(--base));border:1rem solid rgb(var(--base-50));border-radius:1rem;display:flex;justify-content:center;--w:50%;color:rgb(var(--base-200))}.feed-block .placeholder i.icon{animation:dance 2.5s ease-in-out infinite}.feed-block .item-grid{max-width:var(--full)}.feed-block .item-grid:has([data-timeline]){grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}.feed-block .item{background:rgb(var(--base-50));box-shadow:rgba(var(--base),var(--op-2)) var(--shdw);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0}.feed-block .item h3{font-size:var(--txt-medium);margin:0}.feed-block .item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed-block .item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base),var(--op-2));left:0;position:absolute;top:calc(var(--chip_)*-1);width:100%}.feed-block .item details summary:hover{background-color:rgba(var(--action-0),var(--op-45))}.feed-block .item details[open]{padding:.25rem .5rem}.feed-block .item img:hover{opacity:.8}.feed-block .item ul{margin:0}.feed-block .item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed-block .item[data-timeline] .images span{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast));padding:.25rem .5rem;position:absolute;width:50%}.feed-block .item[data-timeline] .images span:first-of-type{bottom:0;right:50%;text-align:right}.feed-block .item[data-timeline] .images span:last-of-type{left:50%;top:0}.feed-block .item[data-timeline] .images img{width:50%}.feed-block .item[data-timeline] .images img:first-of-type{border-right:2px solid rgb(var(--action-0))}.feed-block .item a:after,.feed-block .item a:before{display:none}.feed-block .item label{font-weight:400;text-transform:none;--w:1.5em}.feed-block .all-filters summary{display:flex;justify-content:space-between}.all-filters{font-size:var(--txt-x-small)}.all-filters[open]{border:2px solid rgb(var(--action-0));border-radius:0 0 var(--radius-outer) var(--radius-outer);padding:0}.all-filters summary{width:100%}.all-filters summary:hover,.all-filters[open] summary{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}.all-filters summary:hover:after,.all-filters[open] summary:after{background-color:rgb(var(--action-contrast))}.all-filters>.row.row{padding:0 .75rem 2rem;width:var(--content)}.all-filters>.row.row.search{padding-bottom:0}.all-filters>.row.row{position:relative}.all-filters>.row.row>.label,.all-filters>.row.row>.row>.label{font-family:var(--heading);font-weight:var(--fw-h-bold);text-transform:uppercase}.all-filters>.row.row>.label{width:20%}.all-filters>.row.row>.row>.label{white-space:nowrap}.all-filters .btn+label,.all-filters button{min-height:var(--chipchip);padding:0;width:var(--chipchip)}.all-filters .btn+label .label,.all-filters .row:has(>.btn:not(:checked)+label:hover) :checked+label .label,.all-filters button .label{bottom:-2rem;opacity:0;position:absolute;white-space:nowrap;width:-moz-max-content;width:max-content;z-index:var(--z-4)}.all-filters .btn+label:hover .label,.all-filters .btn:checked+label .label,.all-filters button:hover .label{opacity:1}.all-filters .search.row,.all-filters .view.row{display:none}.all-filters .ordering{padding:2rem 0 .75rem}.all-filters .ordering>.row label{position:unset}.all-filters .ordering .row .label{color:rgb(var(--contrast));top:-1rem}.all-filters .ordering .row.orderby .label{left:0}.all-filters .ordering .row.order-direction .label{right:0}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{padding:0;transform:scaleX(0);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base);width:0}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.toggle-text input+label{border:1px dashed transparent;color:var(--contrast)!important;cursor:pointer;font-weight:400;padding:.25rem .5rem;position:relative;text-transform:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.toggle-text input+label:after,.toggle-text input+label:before{display:none!important}.toggle-text input+label .text{margin:0;position:relative;--gap:0;border:1px solid rgb(var(--action-50));border-radius:var(--radius);color:rgb(var(--action-50));font-weight:700;padding:2px 4px;width:-moz-fit-content;width:fit-content}.toggle-text input+label .off{--mid:-100%}.toggle-text input+label .on{--mid:100%}.toggle-text input+label .off,.toggle-text input+label .on{transition:var(--trans-transform),opacity var(--trans-base)}.toggle-text input+label .off,.toggle-text input:checked+label .on{max-width:100%;opacity:1;transform:translateZ(0)}.toggle-text input+label .on,.toggle-text input:checked+label .off{max-width:0;opacity:0;transform:translate3d(0,var(--mid),0)}.toggle-text:hover label{border-color:rgb(var(--action-200))}.toggle-text:hover .text{background-color:rgb(var(--action-50));border-color:rgb(var(--action-50));color:rgb(var(--action-contrast))} |
| | |
| | | <?php return array('dependencies' => array(), 'version' => '7bbcf703e79934b80731'); |
| | | <?php return array('dependencies' => array(), 'version' => '7e7d1570989c0c348bd7'); |
| | |
| | | (()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={contextId:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},{timeout:2e3}):setTimeout(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".all-filters",showing:".all-filters summary .current",content:'[data-filter="content"]',ordering:".ordering",orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',orderWrap:".order-direction",match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),["content","orderby","order","taxonomy"].forEach(e=>{let t=this.ui.filters.container.querySelectorAll(this.selectors.filters[e]);this.ui[e]=Array.from(t)}),this.contentTypes=this.ui.content.length>0?this.ui.content.map(e=>e.value):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map(e=>e.dataset.taxonomy):[]}getChecked(e){["content","orderby","order"].includes(e)||console.log("Invalid item to check: ",e);let t=this.ui[e];if(!t)return;let i=t.filter(e=>e.checked);return"content"===e&&i.length>0&&this.updateContentFor(i[0].value),0===i.length?t[0].value:i[0].value}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.getChecked("content"),orderby:this.getChecked("orderby"),order:this.getChecked("order"),page:1};this.config.context&&(e.context=this.config.context),this.config.contextId&&(e.contextId=this.config.contextId),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach(e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}}),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach(e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)})}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach(e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()}),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){return this.selector.getFieldId(this.ui.taxonomies.filter(t=>t.dataset.taxonomy===e)[0]??null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter(e=>e!==t),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){let t=[this.ui.taxonomies,this.ui.orderby];this.ui.filters.showing.textContent=this.ui.content.filter(t=>t.value===e)[0].dataset.label,t.forEach(t=>{t&&t.forEach(t=>{const i=t.dataset.for?.split(",")??[];t.hidden=i.length>0&&!i.includes(e),t.hidden&&t.checked&&(t.checked=!1)})})}updateOrderOptions(e){if(this.ui.filters.orderWrap){let t=this.ui.filters.orderWrap.dataset.forOrder.split(",")??[];this.ui.filters.orderWrap.hidden=!t.includes(e)}}updateFilterControls(){const e=Object.keys(this.taxFilters);this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=0===e.length),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e.length<=1)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach(e=>{this.createTermElement(e)}),this.updateFilterControls())}getTaxonomyIcon(e){let t=this.ui.taxonomies.find(t=>t.dataset.taxonomy===e);return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}processCachedFilters(){Object.keys(this.filters).forEach(e=>{let t=this.cache.get(`${this.config.contextId}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)})}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach(i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)});let i=!1;return e.forEach((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}}),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach(t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])});for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach(e=>{const t=`${this.config.contextId}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)});const e=`${this.config.contextId}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()})}initStore(){let e=this.ui.orderby.filter(e=>!["date","date_modified","title","random"].includes(e.value)),t=[];e.forEach(e=>{t.push({name:e.value,keyPath:e.value})});const i=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=i.feed,this.store.subscribe((e,t)=>{"data-loaded"===e&&(this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=!this.store.lastResponse?.has_more??!0))})}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=e??this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,e=>this.createItemElement(e),t=>{this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),this.store.lastResponse?.has_more??!1)},5).then(()=>{}),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map(e=>parseInt(e.trim())).filter(e=>e)}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some(t=>Object.keys(e.images).map(e=>parseInt(e)).includes(parseInt(t)))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach(s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)}),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){let s=i.images[t]??!1;s&&([e.src,e.srcset,e.alt]=[s.tiny,`${s.tiny} 50w, ${s.small} 300w, ${s.medium} 1024w`,s["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){let r=t.taxonomies[i][s]??!1;if(!r)continue;let a=o.cloneNode(!0),n=a.querySelector("a");if(!n)continue;let l=window.decodeHTMLEntities(r.title);[n.href,n.title,n.textContent]=[r.url,`See more ${l}`,l],e.append(a)}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=window.decodeHTMLEntities(t)}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number-1} Tx`),s&&(s.textContent=e.number-1),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach(e=>e.remove())}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon-${s.icon}`),t.span&&(t.span.textContent=window.decodeHTMLEntities(s.name))}}),e.define("emptyState"),this.contentTypes.forEach(i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${r.fields.post_title??"Item"}`),o&&t.addTimelineElements(r,e)}}})})}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.feedBlock=new e)})})})(); |
| | | (()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.isFirstLoad=!0,this.config={contextId:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),this.initStore(),this.initTaxonomies().then(e=>{}),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".all-filters",showing:".all-filters summary .current",content:'[data-filter="content"]',ordering:".ordering",orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',orderWrap:".order-direction",match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),["content","orderby","order","taxonomy"].forEach(e=>{let t=this.ui.filters.container.querySelectorAll(this.selectors.filters[e]);this.ui[e]=Array.from(t)}),this.contentTypes=this.ui.content.length>0?this.ui.content.map(e=>e.value):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map(e=>e.dataset.taxonomy):[]}getChecked(e){["content","orderby","order"].includes(e)||console.log("Invalid item to check: ",e);let t=this.ui[e];if(!t)return;let i=t.filter(e=>e.checked);return"content"===e&&i.length>0&&this.updateContentFor(i[0].value),0===i.length?t[0].value:i[0].value}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.getChecked("content"),orderby:this.getChecked("orderby"),order:this.getChecked("order"),page:1};this.config.context&&(e.context=this.config.context),this.config.contextId&&(e.contextId=this.config.contextId),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach(e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}}),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach(e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)})}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach(e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()}),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){return this.selector.getFieldId(this.ui.taxonomies.filter(t=>t.dataset.taxonomy===e)[0]??null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter(e=>e!==t),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){let t=[this.ui.taxonomies,this.ui.orderby];this.ui.filters.showing.textContent=this.ui.content.filter(t=>t.value===e)[0].dataset.label,t.forEach(t=>{t&&t.forEach(t=>{const i=t.dataset.for?.split(",")??[];t.hidden=i.length>0&&!i.includes(e),t.hidden&&t.checked&&(t.checked=!1)})})}updateOrderOptions(e){if(this.ui.filters.orderWrap){let t=this.ui.filters.orderWrap.dataset.forOrder.split(",")??[];this.ui.filters.orderWrap.hidden=!t.includes(e)}}updateFilterControls(){const e=Object.keys(this.taxFilters);this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=0===e.length),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e.length<=1)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach(e=>{this.createTermElement(e)}),this.updateFilterControls())}getTaxonomyIcon(e){let t=this.ui.taxonomies.find(t=>t.dataset.taxonomy===e);return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}processCachedFilters(){Object.keys(this.filters).forEach(e=>{let t=this.cache.get(`${this.config.contextId}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)})}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach(i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)});let i=!1;return e.forEach((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}}),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach(t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])});for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach(e=>{const t=`${this.config.contextId}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)});const e=`${this.config.contextId}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()})}initStore(){let e=this.ui.orderby.filter(e=>!["date","date_modified","title","random"].includes(e.value)),t=[];e.forEach(e=>{t.push({name:e.value,keyPath:e.value})});const i=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"},2);this.store=i.feed,this.store.subscribe((e,t)=>{if("data-loaded"===e){if(this.isFirstLoad)return void(this.isFirstLoad=!1);this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=!this.store.lastResponse?.has_more??!0)}})}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=e??this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,e=>this.createItemElement(e),t=>{this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),this.store.lastResponse?.has_more??!1)},5).then(()=>{}),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map(e=>parseInt(e.trim())).filter(e=>e)}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some(t=>Object.keys(e.images).map(e=>parseInt(e)).includes(parseInt(t)))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach(s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)}),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){let s=i.images[t]??!1;s&&([e.src,e.srcset,e.alt]=[s.tiny,`${s.tiny} 50w, ${s.small} 300w, ${s.medium} 1024w`,s["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){let r=t.taxonomies[i][s]??!1;if(!r)continue;let a=o.cloneNode(!0),n=a.querySelector("a");if(!n)continue;let l=window.decodeHTMLEntities(r.title);[n.href,n.title,n.textContent]=[r.url,`See more ${l}`,l],e.append(a)}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=window.decodeHTMLEntities(t)}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number} Tx`),s&&(s.textContent=e.number),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach(e=>e.remove())}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon-${s.icon}`),t.span&&(t.span.textContent=window.decodeHTMLEntities(s.name))}}),e.define("emptyState"),this.contentTypes.forEach(i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${r.fields.post_title??"Item"}`),o&&t.addTimelineElements(r,e)}}})})}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.feedBlock=new e)})})})(); |
| | |
| | | namespace JVBase\blocks; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Block; |
| | | use WP_Query; |
| | | |
| | | class FAQBlock { |
| | | protected Cache $cache; |
| | | protected string $postType; |
| | | protected string $section; |
| | | protected string $slug; |
| | | protected bool $isDirectory; |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('faq_block', WEEK_IN_SECONDS)->connect('post', true)->connect('taxonomy', true); |
| | |
| | | 'render_callback' => [$this, 'render'], |
| | | ] |
| | | ); |
| | | |
| | | |
| | | $faq = array_values(Registrar::getFeatured('is_faq','post')); |
| | | $registrar = Registrar::getInstance($faq[0]); |
| | | $this->section = array_map('jvbCheckBase', $registrar->registrar->taxonomies)[0]; |
| | | $this->postType = $registrar->getBased(); |
| | | $this->slug = $registrar->getSlug(); |
| | | $this->isDirectory = $registrar->hasFeature('show_directory'); |
| | | } |
| | | |
| | | /** |
| | |
| | | public function localizeData(): void |
| | | { |
| | | // Get all sections |
| | | $section_taxonomy = BASE . 'section'; |
| | | $sections_data = $this->cache->remember( |
| | | 'sections', |
| | | function() { |
| | | $sections = get_terms([ |
| | | 'taxonomy' => BASE.'section', |
| | | 'taxonomy' => $this->section, |
| | | 'hide_empty' => false, |
| | | 'orderby' => 'name', |
| | | 'order' => 'ASC', |
| | |
| | | 'jvb-faq-editor-script', |
| | | 'jvbFaq', |
| | | [ |
| | | 'sectionTaxonomy' => $section_taxonomy, |
| | | 'faqPostType' => BASE . 'faq', |
| | | 'sectionTaxonomy' => $this->section, |
| | | 'faqPostType' => $this->postType, |
| | | 'sections' => $sections_data, |
| | | ] |
| | | ); |
| | |
| | | * @param string $content Block content |
| | | * @param WP_Block $block Block instance |
| | | */ |
| | | // Get BASE constant |
| | | $base = defined('BASE') ? BASE : ''; |
| | | $faq_post_type = $base . 'faq'; |
| | | $section_taxonomy = $base . 'section'; |
| | | |
| | | // Get block attributes |
| | | $section_order = $attributes['sectionOrder'] ?? []; |
| | |
| | | $collapse_by_default = $attributes['collapseByDefault'] ?? false; |
| | | |
| | | // Determine if we're on a taxonomy archive or main FAQ archive |
| | | $is_tax_archive = is_tax($section_taxonomy); |
| | | $is_tax_archive = is_tax($this->section); |
| | | $current_term = null; |
| | | |
| | | if ($is_tax_archive) { |
| | |
| | | } else { |
| | | // Build query args based on context |
| | | $query_args = [ |
| | | 'post_type' => $faq_post_type, |
| | | 'post_type' => $this->postType, |
| | | 'posts_per_page' => -1, |
| | | 'post_status' => 'publish', |
| | | 'orderby' => 'menu_order title', |
| | |
| | | |
| | | |
| | | if (!$faq_query->have_posts()) { |
| | | echo '<div class="faq-block faq-block--empty">'; |
| | | echo '<p>' . esc_html__('No FAQs found.', 'jvb') . '</p>'; |
| | | echo '</div>'; |
| | | return; |
| | | return sprintf( |
| | | '<div class="faq-block empty"><p>%s</p></div>', |
| | | esc_html__('No FAQs found.', 'jvb') |
| | | ); |
| | | } |
| | | |
| | | // Organize FAQs by section |
| | |
| | | $faq_query->the_post(); |
| | | $post_id = get_the_ID(); |
| | | |
| | | $terms = get_the_terms($post_id, $section_taxonomy); |
| | | $terms = get_the_terms($post_id, $this->section); |
| | | |
| | | if ($terms && !is_wp_error($terms)) { |
| | | foreach ($terms as $term) { |
| | |
| | | if (!empty($section_order)) { |
| | | $nav = '<nav id="faq"><h2>Sections:</h2><ol>'; |
| | | foreach ($section_order as $term_id) { |
| | | $term = get_term($term_id, $section_taxonomy); |
| | | $term = get_term($term_id, $this->section); |
| | | if ($term && !is_wp_error($term)) { |
| | | $url = (!$is_tax_archive) ? "#{$term->slug}" : get_term_link($term); |
| | | $nav .= '<li><a href="'.$url.'">'.html_entity_decode($term->name).'</a></li>'; |
| | | } |
| | | } |
| | | $seeAll = ($is_tax_archive) ? '<p><a href="'.get_post_type_archive_link(BASE.'faq').'">'.__('See All FAQs', 'jvb').'</a></p>' : ''; |
| | | $nav .= '</ol>'.$seeAll.'</nav>'; |
| | | $buttons = ''; |
| | | if ($this->isDirectory) { |
| | | $directory = JVB()->directories()->directories($this->slug); |
| | | $url = $directory['url']??''; |
| | | if (!empty($url)) { |
| | | $buttons .= '<li><a href="'.$url.'">'.__('See Alphabetical List', 'jvb').'</a></li>'; |
| | | } |
| | | } |
| | | if (is_tax()) { |
| | | $buttons .= '<li><a href="'.get_home_url(null, '/faq/').'">'.__('See All FAQs', 'jvb').'</a></li>'; |
| | | } |
| | | $buttons = empty($buttons) ? '' : '<ul class="buttons">'.$buttons.'</ul>'; |
| | | $nav .= '</ol>'.$buttons.'</nav>'; |
| | | } |
| | | ?> |
| | | |
| | |
| | | namespace JVBase\blocks; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | use JVBase\forms\TaxonomySelector; |
| | |
| | | protected ?int $contextID = null; |
| | | |
| | | protected bool $isGallery = false; |
| | | protected array $args; |
| | | protected bool $isContentTax = false; |
| | | protected bool $hasMore = false; |
| | | |
| | | public function __construct() |
| | | { |
| | |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | | $this->cache->flush(); |
| | | |
| | | add_action('init', [$this, 'registerBlock']); |
| | | } |
| | |
| | | protected function determineContent(array $attrs):void |
| | | { |
| | | if (array_key_exists('inheritQuery', $attrs) && $attrs['inheritQuery'] === true) { |
| | | if (is_post_type_archive()) { |
| | | $args = [ |
| | | 'posts_per_page' => 36, |
| | | 'fields' => 'ids', |
| | | ]; |
| | | if (is_post_type_archive() &&!is_tax()) { |
| | | $obj = get_queried_object(); |
| | | $registrar = Registrar::getInstance($obj->name); |
| | | if ($registrar && $registrar->hasFeature('is_timeline')) { |
| | | $args['post_parent'] = 0; |
| | | } |
| | | $this->content = [jvbNoBase($obj->name)]; |
| | | $args['post_type'] = $obj->name; |
| | | $this->args = $args; |
| | | return; |
| | | } elseif (!empty(Registrar::getProfileTypes()) && is_singular(Registrar::getProfileTypes())) { |
| | | global $post; |
| | | $author = $post->post_author; |
| | | $role = jvbUserRole($author); |
| | | |
| | | $args['post_author'] = $author; |
| | | $this->context = jvbNoBase($role); |
| | | $this->contextID = $author; |
| | | $registrar = Registrar::getInstance($role); |
| | |
| | | return; |
| | | } |
| | | $this->content = $registrar->getCreatable(); |
| | | $args['post_type'] = array_map('jvbCheckBase', $this->content); |
| | | foreach($args['post_type'] as $post_type) { |
| | | $reg = Registrar::getInstance($post_type); |
| | | if ($reg && $reg->hasFeature('is_timeline')) { |
| | | $args['post_parent'] = 0; |
| | | } |
| | | } |
| | | $this->args = $args; |
| | | return; |
| | | } elseif (is_tax()) { |
| | | $obj = get_queried_object(); |
| | | $this->context = jvbNoBase($obj->taxonomy); |
| | | $this->contextID = $obj->term_id; |
| | | $args['tax_query'] = []; |
| | | $args['tax_query'][] = [ |
| | | 'taxonomy' => $obj->taxonomy, |
| | | 'terms' => $obj->term_id, |
| | | ]; |
| | | $registrar = Registrar::getInstance($obj->taxonomy); |
| | | if (!$registrar) { |
| | | return; |
| | | } |
| | | if ($registrar->hasFeature('is_content')) { |
| | | //example: tattoo shop, etc TODO |
| | | $this->isContentTax = true; |
| | | $this->content = [$registrar->getBased()]; |
| | | return; |
| | | } |
| | | $this->content = array_map(function ($item) { return jvbNoBase($item); }, $registrar->registrar->for); |
| | | $args['post_type'] = array_map('jvbCheckBase', $registrar->registrar->for); |
| | | foreach($args['post_type'] as $post_type) { |
| | | $reg = Registrar::getInstance($post_type); |
| | | if ($reg && $reg->hasFeature('is_timeline')) { |
| | | $args['post_parent'] = 0; |
| | | } |
| | | } |
| | | $this->args = $args; |
| | | return; |
| | | } |
| | | } |
| | | // not inheriting, getting from config |
| | | $this->content = $attrs['contentTypes']??[]; |
| | | |
| | | $args['post_type'] = array_map('jvbCheckBase', $attrs['contentTypes']); |
| | | $this->args = $args; |
| | | } |
| | | protected function getContent():string |
| | | { |
| | |
| | | 'fields' => 'ids', |
| | | ]; |
| | | if (!is_null($this->context)) { |
| | | |
| | | $context = Registrar::getInstance($this->context); |
| | | switch ($context->getType()) { |
| | | case 'term': |
| | |
| | | } |
| | | $check = new WP_Query($args); |
| | | $hasPosts = !empty($check->posts); |
| | | wp_reset_postdata(); |
| | | |
| | | $disabled = !$hasPosts; |
| | | $checked = $i === 0 && $hasPosts; |
| | |
| | | ); |
| | | } |
| | | |
| | | protected function renderGrid():string |
| | | protected function renderPlaceholders():string |
| | | { |
| | | $placeholders = ''; |
| | | $total = count($this->content) - 1; |
| | |
| | | ); |
| | | } |
| | | |
| | | |
| | | protected function renderGrid():string |
| | | { |
| | | if (empty($this->args)) { |
| | | return $this->renderPlaceholders(); |
| | | } |
| | | $items = $this->getItems(); |
| | | |
| | | $out = '<div class="item-grid">'; |
| | | $out .= implode('',array_map(function ($ID) { |
| | | $content = $this->isContentTax |
| | | ? jvbNoBase($this->content[0]) |
| | | : jvbNoBase(get_post_type($ID)); |
| | | |
| | | return $this->renderItem($content, $ID); |
| | | }, $items)); |
| | | $out .= '</div>'; |
| | | return $out; |
| | | } |
| | | protected function getItems():array |
| | | { |
| | | if ($this->isContentTax) { |
| | | $items = get_terms([ |
| | | 'taxonomy' => $this->content, |
| | | 'fields' => 'ids', |
| | | 'meta_key' => BASE.'date_modified', |
| | | 'meta_type' => 'DATETIME', |
| | | 'orderby' => 'meta_value', |
| | | 'order' => 'desc', |
| | | 'number' => $this->args['posts_per_page'] |
| | | ]); |
| | | $items = $items && !is_wp_error($items) ? $items : []; |
| | | } else { |
| | | $items = new WP_Query($this->args); |
| | | $this->hasMore = $items->found_posts > $this->args['posts_per_page']; |
| | | $items = $items->posts; |
| | | wp_reset_postdata(); |
| | | } |
| | | return $items; |
| | | } |
| | | |
| | | protected function renderLoader():string |
| | | { |
| | | return sprintf( |
| | | '<button type="button" class="load-more">%s<span>Show Me More</span>%s</button> |
| | | %s%s', |
| | | $button = sprintf( |
| | | '<button type="button" class="load-more"%s>%s<span>Show Me More</span>%s</button>', |
| | | $this->hasMore ? '' : ' hidden', |
| | | jvbIcon('arrow-elbow-left-down'), |
| | | jvbIcon('arrow-elbow-right-down'), |
| | | ); |
| | | return sprintf( |
| | | '%s%s%s', |
| | | $button, |
| | | jvbLoadingScreen(), |
| | | $this->isGallery ? jvbRenderGallery(false) : '', |
| | | ); |
| | |
| | | |
| | | protected function renderActions():string |
| | | { |
| | | return sprintf( |
| | | return is_user_logged_in() ? sprintf( |
| | | '<button data-action="refresh" data-ignore>%s<span>Hard Refresh</span></span></button>', |
| | | jvbIcon('arrows-clockwise') |
| | | ); |
| | | ) : ''; |
| | | } |
| | | |
| | | public static function getFavouritesButton(string $content):string |
| | |
| | | if (!$registrar || !Site::has('favourites') || !$registrar->hasFeature('favouritable')) { |
| | | return ''; |
| | | } |
| | | return '<button class="favourite" type="button" title="Add to favourites" data-action="favourite"> |
| | | '.jvbIcon('heart') |
| | | .jvbIcon('heart', ['style'=>'fill']).' |
| | | </button>'; |
| | | return sprintf( |
| | | '<button class="favourite" type="button" title="Add to favourites" data-action="favourite">%s%s</button>', |
| | | jvbIcon('heart'), |
| | | jvbIcon('heart', ['style'=>'fill']) |
| | | ); |
| | | } |
| | | public static function getUpvotesButton(string $content):string |
| | | { |
| | |
| | | if (!Site::has('karma') || !$registrar || !$registrar->hasFeature('karma')){ |
| | | return ''; |
| | | } |
| | | return '<div class="karma row"> |
| | | return sprintf( |
| | | '<div class="karma row"> |
| | | <button type="button" class="vote" data-action="upvote"> |
| | | '.jvbIcon('arrow-fat-up') |
| | | .jvbIcon('arrow-fat-up', ['style'=>'fill']). |
| | | '</button> |
| | | %s%s |
| | | </button> |
| | | <button type="button" class="vote" data-action="downvote"> |
| | | '.jvbIcon('arrow-fat-down') |
| | | .jvbIcon('arrow-fat-down', ['style'=>'fill']). |
| | | '</button> |
| | | %s%s |
| | | </button> |
| | | <span class="score"></span> |
| | | </div>'; |
| | | </div>', |
| | | jvbIcon('arrow-fat-up'), |
| | | jvbIcon('arrow-fat-up', ['style'=>'fill']), |
| | | jvbIcon('arrow-fat-down'), |
| | | jvbIcon('arrow-fat-down', ['style'=>'fill']) |
| | | ); |
| | | } |
| | | protected function getDefaultTemplate(string $content): string |
| | | { |
| | | $template = apply_filters('jvbFeedItem', '', $content); |
| | | if (empty($template)) { |
| | | $config = Registrar::getInstance($content)->getConfig('feed'); |
| | | $allFields = Registrar::getFieldsFor($content); |
| | | $images = $config['images']??['post_thumbnail']; |
| | | $fields = $config['fields']??['post_title','post_date','post_excerpt']; |
| | | $fields = array_filter($fields, function($field) use($images) { |
| | | return !in_array($field, $images); |
| | | }); |
| | | $fields = array_filter($allFields, function($field) use($fields) { |
| | | return in_array($field, $fields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | |
| | | $template = sprintf( |
| | | '<div class="feed item col %s">%s%s', |
| | | $content, |
| | | self::getFavouritesButton($content), |
| | | self::getUpvotesButton($content) |
| | | ); |
| | | |
| | | //Add all defined images, but allow for filtering |
| | | $imageTemplate = '<a>'; |
| | | foreach ($images as $image) { |
| | | $imageTemplate .= sprintf( |
| | | '<img data-field="%s" width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">', |
| | | $image |
| | | ); |
| | | } |
| | | $imageTemplate .= '</a>'; |
| | | |
| | | $template .= sprintf( |
| | | '<div class="images">%s</div>', |
| | | apply_filters('jvbFeedImages', $imageTemplate, $content, $images) |
| | | ); |
| | | |
| | | //Output default fields, but allow for filtering |
| | | $template .= sprintf( |
| | | '<details> |
| | | <summary>%s</summary>', |
| | | apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content) |
| | | ); |
| | | |
| | | $fieldsTemplate = ''; |
| | | foreach ($fields as $fieldName => $config) { |
| | | $fieldsTemplate .= apply_filters('jvbFeedItemField', $this->defaultFieldTemplate($config['type'], $fieldName), $content, $fieldName, $config['type']); |
| | | } |
| | | $template .= sprintf( |
| | | '<div class="item-info">%s</div>', |
| | | apply_filters('jvbFeedItemFields', $fieldsTemplate, $content, $fields) |
| | | ); |
| | | $template .= '</details></div>'; |
| | | } |
| | | |
| | | |
| | | return sprintf( |
| | | '<template class="feedItem%s">%s</template>', |
| | | ucfirst($content), |
| | | $template |
| | | $this->renderItem($content) |
| | | ); |
| | | } |
| | | |
| | | protected function defaultFieldTemplate(string $fieldType, string $fieldName):string |
| | | protected function renderItem(string $content, ?int $ID = null):string |
| | | { |
| | | /** |
| | | * Allow plugins to replace the content within the feed item |
| | | */ |
| | | $function = BASE.'render_'.$content.'_feed_item'; |
| | | if (function_exists($function)) { |
| | | $out = $function($ID); |
| | | } else { |
| | | $out = $this->buildFeedItem($content, $ID); |
| | | } |
| | | $registrar = Registrar::getInstance($content); |
| | | $data = []; |
| | | if ($registrar && $registrar->hasFeature('is_timeline')) { |
| | | $data[] = 'data-timeline'; |
| | | } |
| | | |
| | | $data = !empty($data) ? ' '.implode(' ', $data) : ''; |
| | | |
| | | return sprintf( |
| | | '<div class="feed item col %s"%s>%s%s%s</div>', |
| | | $content, |
| | | $data, |
| | | self::getFavouritesButton($content), |
| | | self::getUpvotesButton($content), |
| | | $out |
| | | ); |
| | | } |
| | | protected function buildFeedItem(string $content, ?int $ID = null):string |
| | | { |
| | | $registrar = Registrar::getInstance($content); |
| | | $meta = is_null($ID) ? false : |
| | | match ($registrar->getType()) { |
| | | 'post' => Meta::forPost($ID), |
| | | 'term' => Meta::forTerm($ID), |
| | | 'user' => Meta::forUser($ID), |
| | | default => false |
| | | }; |
| | | |
| | | [$images, $fields] = $registrar->getFeedFields(); |
| | | |
| | | /** |
| | | * Get the main image for the feed item. |
| | | * Output can be overridden with the $imagesFn |
| | | */ |
| | | $imagesFn = BASE.'render_'.$content.'_feed_item_images'; |
| | | if (function_exists($imagesFn)) { |
| | | $img = $imagesFn($ID, $images); |
| | | } else { |
| | | $img = ''; |
| | | foreach ($images as $config) { |
| | | $field = $config['name']; |
| | | $img .= $meta ? jvbFormatImage($meta->get($field), 'tiny', 'medium') |
| | | : $this->defaultFieldTemplate($config['type'], $field); |
| | | } |
| | | } |
| | | $img = sprintf( |
| | | '<div class="images"><a href="%s">%s</a></div>', |
| | | $ID ? get_the_permalink($ID) : '', |
| | | $img |
| | | ); |
| | | |
| | | /** |
| | | * Start the Details with the fields |
| | | * Plugins can modify the summary title with the 'jvbFeedItemSummary' filter |
| | | */ |
| | | $summary = sprintf( |
| | | '<details><summary>%s</summary>', |
| | | apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content) |
| | | ); |
| | | /** |
| | | * Work through the fields |
| | | * Each field output can be overridden with $field or $fieldType |
| | | */ |
| | | foreach ($fields as $config) { |
| | | $f = $config['name']; |
| | | $functions = [ |
| | | BASE.'render_'.$content.'_field_'.$f, //Overrides field for this content |
| | | BASE.'render_field_'.$f, //Overrides field with this name |
| | | BASE.'render_field_type_'.$config['type'] //Overrides field of this type |
| | | ]; |
| | | |
| | | $didIt = false; |
| | | foreach ($functions as $func) { |
| | | if (!$didIt && function_exists($func)) { |
| | | $didIt = true; |
| | | $summary .= $func($ID, is_null($ID)); |
| | | } |
| | | } |
| | | if (!$didIt) { |
| | | $summary .= $this->defaultFieldTemplate($config['type'], $f, is_null($ID), $meta, $config); |
| | | } |
| | | |
| | | } |
| | | $summary .= '</details>'; |
| | | |
| | | return $img.$summary; |
| | | } |
| | | |
| | | protected function defaultFieldTemplate(string $fieldType, string $fieldName, bool $isTemplate = true, bool|Meta $meta = false, array $config = []):string |
| | | { |
| | | $data = ' data-field="'.$fieldName.'"'; |
| | | $value = $meta ? $meta->get($fieldName) : ''; |
| | | if (!$isTemplate && empty($value)) { |
| | | return ''; |
| | | } |
| | | switch ($fieldName) { |
| | | case 'post_title': |
| | | return '<h3'.$data.'></h3>'; |
| | | return sprintf( |
| | | '<h3%s>%s</h3>', |
| | | $data, |
| | | $meta && !empty($value) ? $value : '', |
| | | ); |
| | | case 'post_date': |
| | | case 'post_modified': |
| | | return '<time'.$data.'></time>'; |
| | | return sprintf( |
| | | '<time%s%s>%s</time>', |
| | | $data, |
| | | $meta && !empty($value) ? date('c', strtotime($value)) : '', |
| | | $meta && !empty($value) ? date('F j, Y', strtotime($value)) : '', |
| | | ); |
| | | } |
| | | |
| | | return match($fieldType) { |
| | | 'date','datetime','time' => '<time'.$data.'></time>', |
| | | 'upload' => '<img'.$data.' width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">', |
| | | 'taxonomy' => '<ul'.$data.'><li><a><i></i></a></li></ul>', |
| | | default => '<p'.$data.'></p>', |
| | | 'date' => sprintf( |
| | | '<time%s%s>%s</time>', |
| | | $data, |
| | | $meta && !empty($value) ? date('c', strtotime($value)) : '', |
| | | $meta && !empty($value) ? date('F j, Y', strtotime($value)) : '', |
| | | ), |
| | | 'datetime' => sprintf( |
| | | '<time%s%s>%s</time>', |
| | | $data, |
| | | $meta && !empty($value) ? date('c', strtotime($value)) : '', |
| | | $meta && !empty($value) ? date('F j, Y at h:iA', strtotime($value)) : '', |
| | | ), |
| | | 'time' => sprintf( |
| | | '<time%s%s>%s</time>', |
| | | $data, |
| | | $value, |
| | | $value |
| | | ), |
| | | 'upload' => empty($value) ? sprintf( |
| | | '<img%s width="300px" height="300px" loading="lazy" decoding="async">', |
| | | $data |
| | | ) : |
| | | str_replace('<img', '<img' . $data, jvbFormatImage($value)), |
| | | 'selector' => sprintf( |
| | | '<ul%s class="terms %s">%s</ul>', |
| | | $data, |
| | | $config['taxonomy']??$config['post_type']??$config['role']??'', |
| | | empty($value) ? sprintf('<li><a>%s</a></li>', |
| | | $this->iconFor($config) |
| | | ) : |
| | | implode('', array_filter(array_map(function ($ID) use ($config) { |
| | | $type = $config['subtype']; |
| | | $taxonomy = isset($config['taxonomy']) ? jvbCheckBase($config['taxonomy']) : ''; |
| | | |
| | | $item = match ($type) { |
| | | 'taxonomy' => get_term($ID, $taxonomy), |
| | | 'user' => get_userdata($ID), |
| | | 'post' => get_post($ID), |
| | | default => false, |
| | | }; |
| | | |
| | | if (!$item || is_wp_error($item)) { |
| | | return ''; |
| | | } |
| | | |
| | | $icon = $this->iconFor($config); |
| | | |
| | | $name = match ($type) { |
| | | 'taxonomy' => $item->name, |
| | | 'user' => $item->display_name, |
| | | 'post' => $item->post_title, |
| | | default => '', |
| | | }; |
| | | $url = match ($type) { |
| | | 'taxonomy' => get_term_link((int)$ID, $taxonomy), |
| | | 'user' => get_the_permalink(get_user_meta($ID, BASE . 'userLink', true)), |
| | | 'post' => get_the_permalink($ID), |
| | | default => '' |
| | | }; |
| | | return sprintf( |
| | | '<li><a href="%s">%s%s</a></li>', |
| | | $url, |
| | | !empty($icon) ? jvbIcon($icon) : '', |
| | | $name |
| | | ); |
| | | }, explode(',', $value)))) |
| | | ), |
| | | default => sprintf( |
| | | '<p%s>%s</p>', |
| | | $data, |
| | | $value |
| | | ), |
| | | }; |
| | | } |
| | | protected function iconFor(array $fieldConfig):string |
| | | { |
| | | $icon = match ($fieldConfig['type']??'') { |
| | | 'taxonomy' => Registrar::getInstance($fieldConfig['taxonomy'])->getIcon(), |
| | | 'user' => isset($fieldConfig['role']) ? Registrar::getInstance($fieldConfig['role'])->getIcon() : 'user', |
| | | 'post' => Registrar::getInstance($fieldConfig['post_type'])->getIcon(), |
| | | default => '' |
| | | }; |
| | | |
| | | return empty($icon) ? $icon : jvbIcon($icon); |
| | | } |
| | | |
| | | } |
| | |
| | | } |
| | | $this->content = jvbNoBase($post->post_type); |
| | | |
| | | $this->children = get_children([ |
| | | 'post_parent' => $this->parentID, |
| | | 'post_status' => 'publish', |
| | | 'orderby' => 'date', |
| | | 'order' => 'ASC', |
| | | 'fields' => 'ids' |
| | | ]); |
| | | $this->children = jvbTimelinePoints($this->parentID,$post->post_type); |
| | | $this->total = count($this->children); |
| | | |
| | | ob_start(); |
| | |
| | | 'message' => is_null($msg) ? ($success ? 'Completed successfully' : 'Something went wrong') : $msg |
| | | ]; |
| | | } |
| | | |
| | | function jvbTimelinePoints(int $ID, string $type, array $status = ['publish']):array |
| | | { |
| | | $type = jvbCheckBase($type); |
| | | return get_children([ |
| | | 'post_parent' => $ID, |
| | | 'orderby' => 'date', |
| | | 'order' => 'ASC', |
| | | 'posts_per_page'=> -1, |
| | | 'post_status' => $status, |
| | | 'fields' => 'ids', |
| | | 'post_type' => $type |
| | | ]); |
| | | } |
| | |
| | | |
| | | private static array $instances = []; |
| | | private bool $hasRedis; |
| | | private bool $varyByAuth = false; |
| | | private bool $varyByUser = false; |
| | | |
| | | private function __construct(string $group, int $ttl) |
| | | { |
| | |
| | | add_action('deleted_user_meta', [self::class, 'onUserMetaDelete'], 10, 2); |
| | | } |
| | | |
| | | public function auth(): static |
| | | { |
| | | $this->varyByAuth = true; |
| | | return $this; |
| | | } |
| | | |
| | | public function user(): static |
| | | { |
| | | $this->varyByUser = true; |
| | | return $this; |
| | | } |
| | | |
| | | private function resolveKey(int|string $key): string |
| | | { |
| | | if ($this->varyByUser) { |
| | | return $key . ':u' . get_current_user_id(); |
| | | } |
| | | if ($this->varyByAuth) { |
| | | return $key . ':' . (is_user_logged_in() ? 'auth' : 'guest'); |
| | | } |
| | | return (string) $key; |
| | | } |
| | | |
| | | /* --------------------------------------------------------------------- |
| | | * Factory |
| | | * ------------------------------------------------------------------- */ |
| | |
| | | ?int $ttl = null |
| | | ): mixed { |
| | | if (is_array($key)) { |
| | | $id = $this->generateKey($key); |
| | | $key = $this->generateKey($key); |
| | | } |
| | | $instanceTags = array_map(fn($tag) => [$this->group, $tag], $this->getTags()); |
| | | $tags = array_unique(array_merge( |
| | | $this->getTags(), |
| | | array_map('sanitize_key', $tags) |
| | | )); |
| | | $instanceTags, |
| | | array_map(fn($tag) => [sanitize_key($tag[0]), $tag[1]], $tags) |
| | | ), SORT_REGULAR); |
| | | |
| | | |
| | | $value = wp_cache_get($key, $this->group); |
| | |
| | | * |
| | | * @example CustomTable::for('favourites')->insert($data); |
| | | */ |
| | | public static function for(string $tableName): self |
| | | public static function for(string $tableName, bool $user = false, bool $auth = false, bool $useTransactions = false): self |
| | | { |
| | | if (!isset(self::$instances[$tableName])) { |
| | | self::$instances[$tableName] = new self($tableName); |
| | | self::$instances[$tableName] = new self($tableName, $user, $auth, $useTransactions); |
| | | } |
| | | |
| | | |
| | | return self::$instances[$tableName]; |
| | | } |
| | | |
| | | public static function destroyInstance(string $tableName):void |
| | | { |
| | | if (isset(self::$instances[$tableName])) { |
| | |
| | | * @param string $tableName Table name without prefix/BASE (e.g., 'favourites', 'notifications') |
| | | * @param bool $useTransactions Whether to auto-wrap operations in transactions |
| | | */ |
| | | public function __construct(string $tableName, bool $useTransactions = false) |
| | | public function __construct(string $tableName, bool $user = false, bool $auth = false, bool $useTransactions = false) |
| | | { |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | |
| | | $this->useTransactions = $useTransactions; |
| | | |
| | | $this->cache = Cache::for($tableName); |
| | | if ($user) { |
| | | $this->cache->user(); |
| | | } |
| | | if ($auth) { |
| | | $this->cache->auth(); |
| | | } |
| | | |
| | | $usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->users}'"); |
| | | $parentCollation = $usersStatus->Collation ?? 'utf8mb4_general_ci'; |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('dashboard', WEEK_IN_SECONDS)->connect('user'); |
| | | $this->cache = Cache::for('dashboard', WEEK_IN_SECONDS)->connect('user')->user(); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | | $this->cache->flush(); |
| | | |
| | | add_action('init', [$this, 'registerDashboard']); |
| | | |
| | | $this->user = wp_get_current_user(); |
| | |
| | | |
| | | jvb_register_do_once('buildDashboard', [$this, 'activate']); |
| | | |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeDashboard'], 10, 1); |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeDashboard'], 8, 1); |
| | | } |
| | | |
| | | public function excludeDashboard(array $ids):array { |
| | | $cached = $this->cache->remember( |
| | | public function excludeDashboard(array $IDs):array { |
| | | $this->cache->flush(); |
| | | $exclude = $this->cache->remember( |
| | | 'dashboardIDs', |
| | | function() { |
| | | return get_posts([ |
| | |
| | | 'fields' => 'ids', |
| | | ]); |
| | | }); |
| | | return array_merge($ids, $cached); |
| | | if (!empty($exclude)) { |
| | | $IDs = array_merge($IDs, $exclude); |
| | | } |
| | | |
| | | return $IDs; |
| | | } |
| | | |
| | | /** |
| | |
| | | string $name = '', |
| | | string $url = '', |
| | | string $ID = '', |
| | | $extra = false |
| | | ?array $extra = null |
| | | ):array { |
| | | if ($name == '') { |
| | | $name = get_the_title(); |
| | |
| | | return $this->cache->remember( |
| | | $cacheKey, |
| | | function() use ($slug, $type, $registrar, $config, $paged) { |
| | | $out = '<h1>' . $this->directoryTitle($registrar) . '</h1>'; |
| | | |
| | | $out = '<h1>' . $config['title'] . '</h1>'; |
| | | $out .= '<div class="description">'; |
| | | foreach ($config['description'] ?? [] as $p) { |
| | | $out .= '<p>' . $p . '</p>'; |
| | | } |
| | | $out .= '</div>'; |
| | | $link = match($registrar->getType()) { |
| | | 'post' => get_post_type_archive_link($registrar->getBased()), |
| | | 'term' => false, |
| | | 'user' => get_post_type_archive_link($registrar->getProfile()->getBased()), |
| | | default => false, |
| | | }; |
| | | $out .= $link ? '<ul class="buttons"><li><a href="'.$link.'">See All</a></li></ul>' : ''; |
| | | $out .= $this->renderIndex($slug); |
| | | |
| | | $list = []; |
| | |
| | | |
| | | $get = new WP_Query($args); |
| | | |
| | | $hasExtra = $registrar->hasFeature('directory_extra'); |
| | | $hasExtra = !empty($config['directory_extra']??[]); |
| | | if ($get->have_posts()) { |
| | | while ( $get->have_posts() ) { |
| | | $get->the_post(); |
| | |
| | | foreach ($items as $item) { |
| | | $extra = ''; |
| | | if (!empty($item['extra'])) { |
| | | $extra = '<span>'; |
| | | $extra = '<ul class="term-list row">'; |
| | | foreach ($item['extra'] as $ext) { |
| | | $icon = Registrar::getInstance($ext['type'])->getIcon(); |
| | | $umamiType = ($ext['type'] === BASE.'shop') ? 'click_shop' : 'click_taxonomy'; |
| | | $extra .= '<a href="'.$ext['url'].'"'.$umami->trackContentClick($item['id'],$umamiType, ['source_type' => 'directory']).'>'.$ext['name'].'</a>'; |
| | | $extra .= '<li><a href="'.$ext['url'].'"'.$umami->trackContentClick($item['id'],$umamiType, ['source_type' => 'directory']).'>'.jvbIcon($icon).$ext['name'].'</a></li>'; |
| | | } |
| | | $extra .= '</span>'; |
| | | $extra .= '</ul>'; |
| | | } |
| | | |
| | | $item_html = apply_filters('jvb_directory_render_item', '', $item, $type, $extra); |
| | |
| | | } |
| | | private function defineFavouriteTable():void |
| | | { |
| | | $table = CustomTable::for('favourites'); |
| | | $table = CustomTable::for('favourites', true); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | |
| | | return; |
| | | } |
| | | $this->setInviteConfig(); |
| | | $this->cache = Cache::for('invitations'); |
| | | $this->cache = Cache::for('invitations')->user(); |
| | | add_action('init', [$this, 'registerInvitationExecutors'], 5); |
| | | |
| | | add_action('user_register', [$this, 'checkInvitation']); |
| | |
| | | // break; |
| | | // } |
| | | if (!isset(self::$cache)) { |
| | | self::$cache = Cache::for('user_karma')->connect('user'); |
| | | self::$cache = Cache::for('user_karma')->connect('user')->user(); |
| | | } |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | |
| | | // Allow other features to register handlers |
| | | do_action('jvbLoginManagerInit', $this); |
| | | add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2); |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 10, 1); |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 8, 1); |
| | | } |
| | | public static function getInstance():self |
| | | { |
| | |
| | | |
| | | public function excludeLoginSitemap(array $ids): array |
| | | { |
| | | $ids[] = $this->getLoginPage(); |
| | | $loginPage = $this->getLoginPage(); |
| | | if (!empty($loginPage)) { |
| | | $ids = array_merge($ids, [$loginPage]); |
| | | } |
| | | return $ids; |
| | | } |
| | | /************************************************************************** |
| | |
| | | class EmailDigests |
| | | { |
| | | protected string $campaign; |
| | | protected Cache $terms; |
| | | protected Cache $users; |
| | | protected CustomTable $userIndex; |
| | | protected CustomTable $termIndex; |
| | | public function __construct() |
| | |
| | | { |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->cache = Cache::for('queue', DAY_IN_SECONDS)->connect('user'); |
| | | $this->cache = Cache::for('queue', DAY_IN_SECONDS)->connect('user')->user(); |
| | | add_action('jvb_process_queue', [ $this, 'checkQueue' ]); |
| | | add_action('jvb_queue_maintenance', [$this, 'hourlyMaintenance']); |
| | | add_action('jvbEmailDailyMetricsReport', [$this, 'emailDailyMetricsReport']); |
| | |
| | | $this->default_settings['referral_role'] = $this->role; |
| | | |
| | | $this->cache = Cache::for('referrals', WEEK_IN_SECONDS); |
| | | $this->requestCache = Cache::for('referral_requests', WEEK_IN_SECONDS)->connect('referrals', true); |
| | | $this->statsCache = Cache::for('referral_stats', WEEK_IN_SECONDS)->connect('referrals', true); |
| | | // $this->requestCache = Cache::for('referral_requests', WEEK_IN_SECONDS)->connect('referrals', true); |
| | | $this->statsCache = Cache::for('referral_stats', WEEK_IN_SECONDS)->connect('referrals', true)->user(); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | $this->requestCache->flush(); |
| | | // $this->requestCache->flush(); |
| | | $this->statsCache->flush(); |
| | | } |
| | | |
| | |
| | | case is_singular(): |
| | | $key = get_queried_object_id(); |
| | | break; |
| | | case is_post_type_archive(): |
| | | $obj = get_queried_object(); |
| | | $key = $obj->name; |
| | | break; |
| | | case is_tax(): |
| | | $obj = get_queried_object(); |
| | | $key = $obj->taxonomy; |
| | | break; |
| | | case is_post_type_archive(): |
| | | $obj = get_queried_object(); |
| | | $key = $obj->name; |
| | | break; |
| | | case is_home(): |
| | | $obj = get_queried_object(); |
| | | $key = $obj->post_type; |
| | |
| | | <?php |
| | | |
| | | use JVBase\rest\routes\LoginRoutes; |
| | | |
| | | add_action('init', 'jvbRegisterScripts', 5); |
| | | |
| | | function jvbRegisterScripts() { |
| | | $version = '1.1.6'; |
| | | $version = '1.1.66'; |
| | | $strategy = [ |
| | | 'strategy' => 'defer', |
| | | 'in_footer' => true |
| | |
| | | $strategy |
| | | ); |
| | | wp_localize_script('jvb-auth', 'jvbBase', ['base' => BASE]); |
| | | wp_localize_script('jvb-auth', 'jvbAuth', LoginRoutes::auth()); |
| | | wp_register_script( |
| | | 'jvb-interactions', |
| | | JVB_URL.'assets/js/min/interactions.min.js', |
| | |
| | | } |
| | | class UserTermsManager |
| | | { |
| | | private Cache $cache; |
| | | |
| | | protected CustomTable $index; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | $this->cache = Cache::for('term_ids')->connect('user'); |
| | | |
| | | // Register hooks |
| | | add_action('set_object_terms', [$this, 'handleTermAssignment'], 10, 6); |
| | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | $this->cache = Cache::for('queue', DAY_IN_SECONDS); |
| | | $this->cache = Cache::for('queue', DAY_IN_SECONDS)->user(); |
| | | } |
| | | |
| | | public function hasProcessingOperations(): bool |
| | |
| | | return []; |
| | | } |
| | | |
| | | $children = get_children([ |
| | | 'post_parent' => $parentID, |
| | | 'posts_per_page' => -1, |
| | | 'post_status' => ['publish', 'draft'], |
| | | ]); |
| | | |
| | | $children = jvbTimelinePoints($parentID, $parent->post_type, ['publish', 'draft']); |
| | | |
| | | if (count($children) === 0) { |
| | | return []; |
| | |
| | | $meta = Meta::forPost($parentID); |
| | | $values = $meta->getAll($shared); |
| | | |
| | | $children = get_children([ |
| | | 'post_parent' => $parentID, |
| | | 'posts_per_page' => -1, |
| | | 'fields' => 'ids', |
| | | ]); |
| | | $children = jvbTimelinePoints($parentID, get_post_type($parentID), ['any']); |
| | | |
| | | if (empty($children)) { |
| | | continue; |
| | |
| | | }); |
| | | |
| | | foreach ($updates as $parentID => $status) { |
| | | $children = get_children([ |
| | | 'post_parent' => $parentID, |
| | | 'posts_per_page' => -1, |
| | | 'fields' => 'ids' |
| | | ]); |
| | | $children = jvbTimelinePoints($parentID, get_post_type($parentID), ['any']); |
| | | |
| | | if (!empty($children)) { |
| | | foreach($children as $child) { |
| | | if ($status === 'trash') { |
| | |
| | | $postID = wp_get_post_parent_id($attachmentId); |
| | | if ($postID && !in_array($postID, $postsAttachedTo)){ |
| | | $postsAttachedTo[] = $postID; |
| | | //TODO: is there a better way? |
| | | } |
| | | } |
| | | |
| | |
| | | private function updateTimelineMetadata(int $parentId): void |
| | | { |
| | | // Get all child posts |
| | | $children = get_children([ |
| | | 'post_parent' => $parentId, |
| | | 'post_type' => get_post_type($parentId), |
| | | 'post_status' => ['publish', 'draft'], |
| | | 'orderby' => 'date', |
| | | 'order' => 'DESC', |
| | | 'fields' => 'ids' |
| | | ]); |
| | | $children = jvbTimelinePoints($parentId, get_post_type($parentId), ['any']); |
| | | |
| | | // Count includes parent + children |
| | | $number = count($children) + 1; |
| | |
| | | 'type' => 'datetime', |
| | | 'label' => 'Date', |
| | | ], |
| | | 'post_modified' => [ |
| | | 'type' => 'datetime', |
| | | 'label' => 'Date Modified', |
| | | 'hidden' => true, |
| | | ], |
| | | 'post_content' => [ |
| | | 'type' => 'textarea', |
| | | 'quill' => true, |
| | |
| | | public ?string $rewrite_taxonomy = null; |
| | | |
| | | public bool $add_image_column = false; |
| | | public bool $prefix_post_type = false; |
| | | public string $prefix_with = 'by'; |
| | | |
| | | protected static array $allFlags = [ |
| | | //Shared Flags |
| | |
| | | //Post Flags |
| | | 'hide_single', 'redirect_to_author', 'is_calendar', 'single_image', 'is_timeline', 'is_gallery', 'is_faq', 'is_glossary', 'rewrite_taxonomy', 'add_image_column', |
| | | //Taxonomy Flags |
| | | 'is_content', 'is_ownable', 'verify_entry', 'track_changes', 'associate_user_content', |
| | | 'is_content', 'is_ownable', 'verify_entry', 'track_changes', 'associate_user_content', 'prefix_post_type', |
| | | //User Flags |
| | | 'has_dashboard', 'can_register', 'can_create', 'keep_stats', 'can_favourite', 'member_verified', 'profile_link', 'manage_others' |
| | | ]; |
| | |
| | | add_filter('jvbDashboardPage', [$this, 'renderDashPage'], 10, 3); |
| | | } |
| | | |
| | | public static function maybeExcludeSingles(array $IDs):array |
| | | { |
| | | self::ensureInstanced(); |
| | | |
| | | $features = ['hide_single', 'is_timeline']; |
| | | foreach ($features as $feature) { |
| | | foreach (self::getFeatured($feature) as $instance) { |
| | | $instance = self::getInstance($instance); |
| | | $cache = Cache::for('tsf')->connect($instance->getType()); |
| | | $cache->flush(); |
| | | |
| | | $exclude = $cache->remember( |
| | | $feature, |
| | | function () use ($instance, $feature) { |
| | | switch ($feature) { |
| | | case 'hide_single': |
| | | return $instance->excludeSingle(); |
| | | case 'is_timeline': |
| | | return $instance->excludeTimeline(); |
| | | default: |
| | | return []; |
| | | } |
| | | } |
| | | ); |
| | | |
| | | if (!empty($exclude)) { |
| | | $IDs = array_merge($IDs, $exclude); |
| | | } |
| | | } |
| | | } |
| | | |
| | | return $IDs; |
| | | } |
| | | protected function excludeSingle():array |
| | | { |
| | | return get_posts([ |
| | | 'post_type' => $this->based, |
| | | 'posts_per_page'=> -1, |
| | | 'fields' => 'ids', |
| | | 'post_status' => 'publish', |
| | | ]); |
| | | } |
| | | protected function excludeTimeline():array |
| | | { |
| | | return get_posts([ |
| | | 'post_type' => $this->based, |
| | | 'posts_per_page'=> -1, |
| | | 'fields' => 'ids', |
| | | 'post_status' => 'publish', |
| | | 'post_parent__not_in' => [0], // Only get posts with a parent |
| | | ]); |
| | | } |
| | | |
| | | protected function initRegistrar():void { |
| | | $this->registrar = match ($this->type) { |
| | | 'post' => new Posts($this->slug, $this->singular, $this->plural), |
| | |
| | | } |
| | | return $this; |
| | | } |
| | | public function prefixWith(string $prefix):self |
| | | { |
| | | $this->prefix_with = sanitize_title($prefix); |
| | | return $this; |
| | | } |
| | | public function removeAll(array $flags):self |
| | | { |
| | | $flags = array_filter($flags, function($flag) { |
| | |
| | | } |
| | | } |
| | | |
| | | if ($this->prefix_post_type) { |
| | | $this->addPostTypeRewrites(); |
| | | } |
| | | |
| | | if ($this->registrar) { |
| | | $this->registrar->register(); |
| | | } |
| | |
| | | echo get_the_post_thumbnail($postID, 'tiny'); |
| | | } |
| | | } |
| | | |
| | | protected function addPostTypeRewrites():void |
| | | { |
| | | $for = $this->registrar->for; |
| | | foreach ($for as $type) { |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar) { |
| | | $base = $registrar->registrar->rewrite['slug']??$registrar->slug; |
| | | |
| | | $prefix = empty($this->prefix_with) ? '' : '/'.$this->prefix_with; |
| | | $prefix = str_replace('//', '/', $prefix); |
| | | |
| | | $slug = str_contains($this->slug, '_') ? str_replace('_','-', $this->slug) : $this->slug; |
| | | add_rewrite_rule( |
| | | $base.$prefix.'/'.$slug.'/([a-z0-9-]+)/?$', |
| | | 'index.php?post_type='.$registrar->getBased().'&'.$this->based.'=$matches[1]', |
| | | 'top' |
| | | ); |
| | | add_rewrite_rule( |
| | | $base.$prefix.'/'.$slug.'/([a-z0-9-]+)/page/([0-9-]+)/?$', |
| | | 'index.php?post_type='.$registrar->getBased().'&'.$this->based.'=$matches[1]&paged=$matches[2]', |
| | | 'top' |
| | | ); |
| | | } |
| | | } |
| | | } |
| | | |
| | | public function getFeedFields():array |
| | | { |
| | | $config = $this->getConfig('feed'); |
| | | $all = $this->getFields(); |
| | | $img = $config['images']??['post_thumbnail']; |
| | | $f = $config['fields']??['post_title', 'post_date', 'post_excerpt']; |
| | | |
| | | $f = array_filter($f, function($field) use ($img) { |
| | | return !in_array($field, $img); |
| | | }); |
| | | $images = []; |
| | | $fields = []; |
| | | |
| | | foreach($img as $i) { |
| | | $images[] = $all[$i]; |
| | | } |
| | | foreach ($f as $x) { |
| | | $fields[] = $all[$x]; |
| | | } |
| | | |
| | | return [$images,$fields]; |
| | | } |
| | | } |
| | |
| | | if (!function_exists('tsf')){ |
| | | return; |
| | | } |
| | | |
| | | if ($this->hasTitle()){ |
| | | add_filter('the_seo_framework_title_from_generation', [$this, 'filterTitle'], 10, 2); |
| | | } |
| | |
| | | add_action('wp_head', [$this, 'outputSchema'], 1); |
| | | add_filter('the_seo_framework_schema_graph_data', [$this, 'filterTSFSchema'], 10, 2); |
| | | add_filter('the_seo_framework_title_from_custom_field', [$this, 'filterTSFOGTitle'], 10, 2); |
| | | } |
| | | |
| | | $this->maybeExcludeSingles(); |
| | | } |
| | | protected function maybeExcludeSingles(): void |
| | | { |
| | | $exclude = $this->cache->remember( |
| | | 'excludeSingles', |
| | | function () { |
| | | $exclude = []; |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | if ($registrar && $registrar->hasFeature('hide_single')) { |
| | | $exclude = $this->excludeSingle(); |
| | | } |
| | | if ($registrar && $registrar->hasFeature('is_timeline')) { |
| | | $exclude = array_merge($exclude, $this->excludeTimeline()); |
| | | } |
| | | return $exclude; |
| | | } |
| | | ); |
| | | |
| | | if (!empty($exclude)) { |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', $exclude); |
| | | } |
| | | } |
| | | protected function excludeSingle():array |
| | | { |
| | | return get_posts([ |
| | | 'post_type' => jvbCheckBase($this->slug), |
| | | 'posts_per_page'=> -1, |
| | | 'fields' => 'ids', |
| | | 'post_status' => 'publish', |
| | | ]); |
| | | } |
| | | protected function excludeTimeline():array |
| | | { |
| | | return get_posts([ |
| | | 'post_type' => jvbCheckBase($this->slug), |
| | | 'posts_per_page'=> -1, |
| | | 'fields' => 'ids', |
| | | 'post_status' => 'publish', |
| | | 'post_parent__not_in' => [0], // Only get posts with a parent |
| | | ]); |
| | | } |
| | | public function filterTSFSchema(array $graph, ?array $args):array |
| | | { |
| | | $based = jvbCheckBase($this->slug); |
| | |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer', |
| | | 'text' => wp_strip_all_tags(str_replace("\n", '', $meta->get('post_content'))), |
| | | ], |
| | | 'answerCount' => 1, |
| | | ]; |
| | | $question = SchemaHelper::classFromConfig($question); |
| | | $page->setMainEntity($question); |
| | |
| | | $item['fields'] = $mainMeta->getAll($this->timelineSharedFields); |
| | | |
| | | //Step 2: Get the fields for each individual posts |
| | | $children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields' => 'ids']); |
| | | $children = jvbTimelinePoints($post->ID, $post->post_type,['publish', 'draft']); |
| | | array_unshift($children, $post->ID); |
| | | |
| | | $subFields = []; |
| | |
| | | parent::__construct(); |
| | | |
| | | // Set up cache connections |
| | | $this->cache->connect('post')->connect('user')->connect('taxonomy'); |
| | | $this->listsCache = Cache::for('lists')->connect('favourites', true); |
| | | $this->sharedListsCache = Cache::for('sharedLists')->connect('favourites', true); |
| | | $this->favouritesCache = Cache::for('allFavourites')->connect('favourites', true); |
| | | $this->cache->connect('post')->connect('user')->connect('taxonomy')->user(); |
| | | $this->listsCache = Cache::for('lists')->connect('favourites', true)->user(); |
| | | $this->sharedListsCache = Cache::for('sharedLists')->connect('favourites', true)->user(); |
| | | $this->favouritesCache = Cache::for('allFavourites')->connect('favourites', true)->user(); |
| | | |
| | | $this->valid_types = array_merge(Registrar::getRegistered('post'), Registrar::getRegistered('term')); |
| | | |
| | |
| | | $meta = Meta::forUser($postID); |
| | | $registrar = Registrar::getInstance($type); |
| | | break; |
| | | default: |
| | | $meta = false; |
| | | } |
| | | if (!$registrar) { |
| | | if (!$registrar || !$meta) { |
| | | return []; |
| | | } |
| | | $fields = $registrar->getFields(); |
| | | [$images, $fields] = $registrar->getFeedFields(); |
| | | |
| | | //Allow custom filtering for public fields |
| | | if (!empty($registrar->getConfig('feed')['fields'])) { |
| | | $fields = array_filter($fields, function($field) use ($registrar) { |
| | | return in_array($field, $registrar->getConfig('feed')['fields']); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | } |
| | | |
| | | $values = $meta->getAll(array_keys($fields)); |
| | | |
| | | $all = array_merge($images, $fields); |
| | | $values = $meta->getAll(array_map(function($f) { return $f['name']; }, $all)); |
| | | $out = [ |
| | | 'fields' => $values, |
| | | ]; |
| | |
| | | |
| | | //Add images |
| | | $imgIDs = []; |
| | | $temp = array_filter($fields, function($field) { |
| | | return in_array($field['type'], [ 'upload', 'image', 'gallery']); |
| | | }); |
| | | |
| | | foreach ($temp as $key => $config) { |
| | | if (array_key_exists($key, $out['fields']) && $out['fields'][$key] !== '') { |
| | | $IDs = array_map('absint', explode(',',$out['fields'][$key])); |
| | | $imgs = $meta->getAll(array_map(function($f) {return $f['name'];}, $images)); |
| | | foreach ($imgs as $img) { |
| | | $IDs = array_map('absint', explode(',',$img)); |
| | | foreach ($IDs as $ID) { |
| | | $imgIDs[$ID] = jvbImageData($ID); |
| | | } |
| | | } |
| | | } |
| | | $out['images'] = $imgIDs; |
| | | |
| | | $out['id'] = $postID; |
| | | $out['content'] = $type; |
| | | $out['icon'] = $registrar->getIcon()??jvbDefaultIcon(); |
| | | $out['icon'] = $registrar->getIcon(); |
| | | if ($out['icon'] === '') { |
| | | $out['icon'] = jvbDefaultIcon(); |
| | | } |
| | |
| | | $out['title'] = html_entity_decode($post->name); |
| | | break; |
| | | case 'post': |
| | | $out['date'] = $post->post_date; |
| | | $out['date_modified'] = $post->post_modified; |
| | | $out['user_id'] = (int)$post->post_author; |
| | | $out['url'] = get_the_permalink($postID); |
| | | $out['title']= get_the_title($postID); |
| | |
| | | |
| | | protected function formatTimeline(int $postID, WP_Post $post):array |
| | | { |
| | | if (!$this->timelineSharedFields || !$this->timelineUniqueFields){ |
| | | $this->initTimelineFields($post->post_type); |
| | | } |
| | | $item = $this->formatItem($postID, 'post', true); |
| | | //Step 1: Get the fields that apply to all posts |
| | | $mainMeta = Meta::forPost($post->ID); |
| | | $item['fields'] = $mainMeta->getAll($this->timelineSharedFields); |
| | | $item = $this->formatItem($postID, 'post', true); |
| | | |
| | | //Step 2: Get the fields for each individual posts |
| | | $children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish'], 'fields'=> 'ids']); |
| | | array_unshift($children, $post->ID); |
| | | $children = jvbTimelinePoints($postID, $post->post_type); |
| | | $last = $children[array_key_last($children)]; |
| | | $lastMeta = Meta::forPost($last); |
| | | $lastImg = $lastMeta->get('post_thumbnail'); |
| | | |
| | | $item['taxonomies'] = $this->extractTaxonomies($item['fields'], $postID, jvbNoBase($post->post_type)); |
| | | $item['fields']['after'] = $lastImg; |
| | | $item['images'][$lastImg] = jvbImageData((int) $lastImg); |
| | | $item['fields']['post_modified'] = $lastMeta->get('post_date'); |
| | | |
| | | $subFields = []; |
| | | $images = []; |
| | | foreach ($children as $child) { |
| | | $meta = Meta::forPost($child); |
| | | $f = $meta->getAll($this->timelineUniqueFields); |
| | | $f = ['id' => $child] + $f; |
| | | $subFields[] = $f; |
| | | $item['taxonomies'] = array_merge($item['taxonomies'], $this->extractTaxonomies($f, $postID, jvbNoBase($post->post_type))); |
| | | $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']); |
| | | $count = count(array_filter($children, function ($child) { |
| | | $tmp = Meta::forPost($child); |
| | | return empty($tmp->get('is_update')) || $tmp->get('is_update') !==1; |
| | | })); |
| | | |
| | | foreach($item['taxonomies'] as $tax => $items) { |
| | | $item[$tax] = array_keys($items); |
| | | } |
| | | $item['number'] = (int)get_post_meta($post->ID,BASE.'number', true); |
| | | $item['fields']['before'] = get_post_thumbnail_id($children[0]); |
| | | $item['fields']['after'] = get_post_thumbnail_id($children[array_key_last($children)]); |
| | | |
| | | $item['fields']['timeline_gallery'] = $subFields; |
| | | $item['images'] = $item['images'] + $images; |
| | | |
| | | $item['number'] = $count; |
| | | $item['fields']['before'] = $item['fields']['post_thumbnail']; |
| | | |
| | | return $item; |
| | | } |
| | |
| | | // Add term to tax query |
| | | $args['tax_query'][] = [ |
| | | 'taxonomy' => $registrar->getBased(), |
| | | // 'field' => 'term_id', |
| | | 'terms' => [(int)$context['id']], |
| | | ]; |
| | | break; |
| | |
| | | |
| | | protected function handleContentTaxonomies(array $args): array |
| | | { |
| | | |
| | | //TODO: This needs to be better. We have CustomTable now, maybe pass it along to that? |
| | | $taxonomy = jvbNoBase($args['post_type']); |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . BASE . 'content_' . $taxonomy; |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | |
| | | } |
| | | } |
| | | |
| | | protected function buildAuth(?int $user = null): array |
| | | public static function auth():array |
| | | { |
| | | if (is_user_logged_in()) { |
| | | $user = ($user) ?: get_current_user_id(); |
| | | return [ |
| | | 'authenticated' => true, |
| | | 'user' => $user, |
| | | 'nonces' => $this->getUserNonces($user) |
| | | ]; |
| | | return (new self)->buildAuth(); |
| | | } |
| | | |
| | | protected function buildAuth(?int $user = null): array |
| | | { |
| | | $userId = $user ?? (is_user_logged_in() ? get_current_user_id() : 0); |
| | | $cacheKey = $userId ?: 'guest'; |
| | | |
| | | return Cache::for('auth', 300)->remember($cacheKey, function() use ($userId) { |
| | | if ($userId) { |
| | | return [ |
| | | 'authenticated' => true, |
| | | 'user' => $userId, |
| | | 'nonces' => $this->getUserNonces($userId), |
| | | ]; |
| | | } |
| | | return [ |
| | | 'authenticated' => false, |
| | | 'user' => false, |
| | | 'nonces' => [ |
| | | 'wp_rest' => wp_create_nonce('wp_rest') |
| | | ] |
| | | 'nonces' => ['wp_rest' => wp_create_nonce('wp_rest')], |
| | | ]; |
| | | }); |
| | | } |
| | | protected function getUserNonces(int $userID):array { |
| | | $nonces = [ |
| | |
| | | { |
| | | $params = $request->get_params(); |
| | | $user_id = absint($params['user']); |
| | | $this->cache = Cache::for($user_id.'_queue'); |
| | | $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(',', $params['ids']))) |
| | |
| | | $postSlug = jvbNoBase($tax->taxonomy); |
| | | }elseif (is_post_type_archive()) { |
| | | $obj = get_queried_object(); |
| | | $postSlug = jvbNoBase($obj->post_type); |
| | | $postSlug = jvbNoBase($obj->name); |
| | | } |
| | | |
| | | } |
| | |
| | | )); |
| | | } |
| | | |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-queue'); |
| | | // jvbAddScriptDependency('jvb-feed-view-script', 'jvb-queue'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-selector'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-data-store'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-cache'); |
| | |
| | | debug_print_backtrace(); |
| | | } |
| | | } ); |
| | | |
| | | |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', 'jvb_maybe_exclude_singles', 8, 1); |
| | | function jvb_maybe_exclude_singles(array $IDs):array |
| | | { |
| | | return Registrar::maybeExcludeSingles($IDs); |
| | | } |
| | |
| | | "icon": "grid-view", |
| | | "description": "Displays a filterable feed of registered content types", |
| | | "keywords": [ "feed", "grid" ], |
| | | "version": "0.9.0", |
| | | "version": "1.0.0", |
| | | "textdomain": "jvb", |
| | | "supports": { |
| | | "html": false, |
| | |
| | | opacity: .8; |
| | | } |
| | | } |
| | | ul { |
| | | margin: 0; |
| | | } |
| | | |
| | | &[data-timeline] { |
| | | .images { |
| | |
| | | this.error = window.jvbError; |
| | | this.cache = new window.jvbCache('feed'); |
| | | this.templates = window.jvbTemplates; |
| | | this.isFirstLoad = true; |
| | | |
| | | this.config = { |
| | | contextId: '', |
| | |
| | | ... this.container.dataset |
| | | }; |
| | | |
| | | |
| | | this.init(); |
| | | } |
| | | init() { |
| | | this.initElements(); |
| | | this.defineTemplates(); |
| | | |
| | | this.initListeners(); |
| | | this.initFilters(); |
| | | |
| | | if ('requestIdleCallback' in window) { |
| | | requestIdleCallback(() => { |
| | | this.initStore(); |
| | | this.initTaxonomies(); |
| | | this.initTaxonomies().then(r => {}); |
| | | |
| | | this.processCachedFilters(); |
| | | this.processURLFilters(); |
| | | this.updateFilterUI(); |
| | | this.initGallery(); |
| | | }, { timeout: 2000 }); |
| | | } else { |
| | | setTimeout(() => { |
| | | this.initStore(); |
| | | this.initTaxonomies(); |
| | | |
| | | this.processCachedFilters(); |
| | | this.processURLFilters(); |
| | | this.updateFilterUI(); |
| | | this.initGallery(); |
| | | }, 100); |
| | | } |
| | | } |
| | | |
| | | initElements() { |
| | |
| | | TTL: 6 * 60 * 60 * 1000, //6 hours |
| | | showLoading: true, |
| | | required: 'content', |
| | | } |
| | | }, |
| | | 2 |
| | | ); |
| | | |
| | | this.store = store.feed; |
| | |
| | | this.store.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'data-loaded': |
| | | if (this.isFirstLoad) { |
| | | //We rendered the first page in php already |
| | | this.isFirstLoad = false; |
| | | return; |
| | | } |
| | | // if (this.store.filters.page === 1) { |
| | | // return; |
| | | // } |
| | | this.renderItems(data.items); |
| | | this.ui.buttons.loadMore.hidden = true; |
| | | if (this.store.lastResponse && this.store.lastResponse?.has_more) { |
| | |
| | | ]; |
| | | |
| | | if (afterEl) { |
| | | afterEl.textContent = `After ${item.number - 1} Tx`; |
| | | afterEl.textContent = `After ${item.number} Tx`; |
| | | } |
| | | if (number) { |
| | | number.textContent = item.number - 1; |
| | | number.textContent = item.number; |
| | | } |
| | | if (started) { |
| | | this.formatTimeField(started, item.fields.timeline[0]['post_date']); |
| | |
| | | if (manyRefs.fields) { |
| | | for (let field of manyRefs.fields) { |
| | | if (isTimeline && ['timeline','number'].includes(field.dataset.field)) continue; |
| | | |
| | | const value = Object.hasOwn(data.fields, field.dataset.field)? data.fields[field.dataset.field] : false; |
| | | if (!value) { |
| | | field.remove(); |