From a9b3b28d001941921aa70d37fdc87c758a163a44 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 05 Jun 2026 16:47:03 +0000
Subject: [PATCH] =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.
---
inc/managers/UserTermsManager.php | 2
inc/managers/CustomTable.php | 13
inc/managers/Cache.php | 34 +
jvb.php | 9
inc/rest/routes/FavouritesRoutes.php | 8
inc/blocks/FAQBlock.php | 55 +
inc/managers/OperationQueue.php | 2
inc/managers/ReferralManager.php | 6
inc/managers/DashboardManager.php | 17
inc/managers/queue/Storage.php | 2
inc/rest/routes/ContentRoutes.php | 2
assets/js/min/referral.min.js | 2
build/feed/block.json | 2
assets/js/min/queue.min.js | 2
inc/rest/routes/FeedRoutes.php | 80 +--
inc/registrar/config/seo/Meta.php | 1
assets/js/concise/AuthManager.js | 117 -----
assets/js/min/auth.min.js | 2
inc/registrar/Fields.php | 5
inc/managers/FavouritesManager.php | 2
inc/registrar/config/seo/Schema.php | 42 --
inc/rest/routes/QueueRoutes.php | 2
inc/managers/LoginManager.php | 7
inc/managers/queue/executors/UploadExecutor.php | 10
inc/blocks/TimelineBlock.php | 8
inc/managers/queue/executors/ContentExecutor.php | 20
inc/managers/ScriptLoader.php | 6
inc/utility/Image.php | 2
assets/js/concise/Queue.js | 5
inc/managers/Notifications/EmailDigests.php | 2
build/feed/view.asset.php | 2
assets/js/concise/DataStore.js | 2
src/feed/style.scss | 3
build/feed/style-index-rtl.css | 2
inc/helpers/all.php | 14
assets/js/concise/Referral.js | 4
build/feed/view.js | 2
build/feed/style-index.css | 2
inc/rest/routes/LoginRoutes.php | 37 +
inc/managers/DirectoryManager.php | 21
inc/managers/InvitationsManager.php | 2
inc/blocks/FeedBlock.php | 398 ++++++++++++++++----
inc/managers/SEO/BreadcrumbManager.php | 8
src/feed/view.js | 45 +-
inc/managers/KarmaManager.php | 2
inc/registrar/Registrar.php | 115 ++++++
src/feed/block.json | 2
47 files changed, 695 insertions(+), 433 deletions(-)
diff --git a/assets/js/concise/AuthManager.js b/assets/js/concise/AuthManager.js
index 760934f..ab70c92 100644
--- a/assets/js/concise/AuthManager.js
+++ b/assets/js/concise/AuthManager.js
@@ -17,8 +17,6 @@
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();
@@ -28,26 +26,23 @@
* 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;
@@ -110,31 +105,16 @@
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 });
}
@@ -161,62 +141,8 @@
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)
@@ -256,21 +182,13 @@
* 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();
}
@@ -312,23 +230,6 @@
});
}
- /**
- * 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
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 284c913..539c82b 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -48,7 +48,6 @@
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}`,
@@ -127,6 +126,7 @@
}
});
+
// Initialize database asynchronously
this.initDB(name).catch(error => {
console.error(`Failed to initialize store "${name}":`, error);
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 1122684..0290aa9 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -31,6 +31,7 @@
this.initElements();
this.initListeners();
this.initStore();
+
if (this.canUpdateUI && this.ui.panel) {
this.popup = window.jvbPopup.registerPopup({
popup: this.ui.panel,
@@ -616,12 +617,12 @@
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) {
diff --git a/assets/js/concise/Referral.js b/assets/js/concise/Referral.js
index e715af0..7764657 100644
--- a/assets/js/concise/Referral.js
+++ b/assets/js/concise/Referral.js
@@ -78,7 +78,7 @@
endpoint: 'referrals/stats',
TTL: 5 * 60 * 1000,
showLoading: false,
- delayFetch: false,
+ delayFetch: true,
filters: {
type: 'dashboard',
user: window.auth.getUser()
@@ -91,7 +91,7 @@
endpoint: 'referrals',
TTL: 10 * 60 * 1000,
showLoading: false,
- delayFetch: false,
+ delayFetch: true,
filters: {
user: window.auth.getUser(),
status: 'all',
diff --git a/assets/js/min/auth.min.js b/assets/js/min/auth.min.js
index eb5d36f..08e1ae0 100644
--- a/assets/js/min/auth.min.js
+++ b/assets/js/min/auth.min.js
@@ -1 +1 @@
-window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.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())})})}};
\ No newline at end of file
+window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.cacheExpiry=3e5,this.init()}async init(){if(!this.isAuthenticating){this.isAuthenticating=!0;try{if("undefined"!=typeof jvbAuth)return this.setAuthData(jvbAuth),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1});await this.fetchAuth()}catch(t){this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,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)}})}};
\ No newline at end of file
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index 08dbcb7..cdda0fd 100644
--- a/assets/js/min/queue.min.js
+++ b/assets/js/min/queue.min.js
@@ -1 +1 @@
-(()=>{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)})})})();
\ No newline at end of file
+(()=>{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)})})})();
\ No newline at end of file
diff --git a/assets/js/min/referral.min.js b/assets/js/min/referral.min.js
index 528347c..75c6822 100644
--- a/assets/js/min/referral.min.js
+++ b/assets/js/min/referral.min.js
@@ -1 +1 @@
-(()=>{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)})})})();
\ No newline at end of file
+(()=>{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)})})})();
\ No newline at end of file
diff --git a/build/feed/block.json b/build/feed/block.json
index d8b7aff..a1e0f3a 100644
--- a/build/feed/block.json
+++ b/build/feed/block.json
@@ -10,7 +10,7 @@
"feed",
"grid"
],
- "version": "0.9.0",
+ "version": "1.0.0",
"textdomain": "jvb",
"supports": {
"html": false,
diff --git a/build/feed/style-index-rtl.css b/build/feed/style-index-rtl.css
index 198f690..7791d84 100644
--- a/build/feed/style-index-rtl.css
+++ b/build/feed/style-index-rtl.css
@@ -1 +1 @@
-.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))}
diff --git a/build/feed/style-index.css b/build/feed/style-index.css
index 6c9880f..aabd955 100644
--- a/build/feed/style-index.css
+++ b/build/feed/style-index.css
@@ -1 +1 @@
-.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))}
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index 1f56951..83fa6e6 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '7bbcf703e79934b80731');
+<?php return array('dependencies' => array(), 'version' => '7e7d1570989c0c348bd7');
diff --git a/build/feed/view.js b/build/feed/view.js
index 4ddf769..2360009 100644
--- a/build/feed/view.js
+++ b/build/feed/view.js
@@ -1 +1 @@
-(()=>{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)})})})();
\ No newline at end of file
+(()=>{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)})})})();
\ No newline at end of file
diff --git a/inc/blocks/FAQBlock.php b/inc/blocks/FAQBlock.php
index 90443e6..81e3db9 100644
--- a/inc/blocks/FAQBlock.php
+++ b/inc/blocks/FAQBlock.php
@@ -2,11 +2,16 @@
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);
@@ -25,6 +30,14 @@
'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');
}
/**
@@ -64,12 +77,11 @@
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',
@@ -96,8 +108,8 @@
'jvb-faq-editor-script',
'jvbFaq',
[
- 'sectionTaxonomy' => $section_taxonomy,
- 'faqPostType' => BASE . 'faq',
+ 'sectionTaxonomy' => $this->section,
+ 'faqPostType' => $this->postType,
'sections' => $sections_data,
]
);
@@ -123,10 +135,6 @@
* @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'] ?? [];
@@ -134,7 +142,7 @@
$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) {
@@ -144,7 +152,7 @@
} 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',
@@ -156,10 +164,10 @@
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
@@ -169,7 +177,7 @@
$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) {
@@ -245,14 +253,25 @@
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>';
}
?>
diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 3693d93..abe8395 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -2,6 +2,7 @@
namespace JVBase\blocks;
use JVBase\managers\Cache;
+use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use JVBase\forms\TaxonomySelector;
@@ -25,6 +26,9 @@
protected ?int $contextID = null;
protected bool $isGallery = false;
+ protected array $args;
+ protected bool $isContentTax = false;
+ protected bool $hasMore = false;
public function __construct()
{
@@ -33,6 +37,7 @@
if (JVB_TESTING) {
$this->cache->flush();
}
+ $this->cache->flush();
add_action('init', [$this, 'registerBlock']);
}
@@ -78,15 +83,26 @@
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);
@@ -94,25 +110,50 @@
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
{
@@ -212,7 +253,6 @@
'fields' => 'ids',
];
if (!is_null($this->context)) {
-
$context = Registrar::getInstance($this->context);
switch ($context->getType()) {
case 'term':
@@ -229,6 +269,7 @@
}
$check = new WP_Query($args);
$hasPosts = !empty($check->posts);
+ wp_reset_postdata();
$disabled = !$hasPosts;
$checked = $i === 0 && $hasPosts;
@@ -463,7 +504,7 @@
);
}
- protected function renderGrid():string
+ protected function renderPlaceholders():string
{
$placeholders = '';
$total = count($this->content) - 1;
@@ -492,13 +533,58 @@
);
}
+
+ 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) : '',
);
@@ -542,10 +628,10 @@
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
@@ -554,10 +640,11 @@
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
{
@@ -565,98 +652,239 @@
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>',
+
+ return match ($fieldType) {
+ '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);
+ }
}
diff --git a/inc/blocks/TimelineBlock.php b/inc/blocks/TimelineBlock.php
index 3f2666c..85b9add 100644
--- a/inc/blocks/TimelineBlock.php
+++ b/inc/blocks/TimelineBlock.php
@@ -59,13 +59,7 @@
}
$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();
diff --git a/inc/helpers/all.php b/inc/helpers/all.php
index 67d79fe..0891fab 100644
--- a/inc/helpers/all.php
+++ b/inc/helpers/all.php
@@ -72,3 +72,17 @@
'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
+ ]);
+}
diff --git a/inc/managers/Cache.php b/inc/managers/Cache.php
index 7653b9a..c144013 100644
--- a/inc/managers/Cache.php
+++ b/inc/managers/Cache.php
@@ -18,6 +18,8 @@
private static array $instances = [];
private bool $hasRedis;
+ private bool $varyByAuth = false;
+ private bool $varyByUser = false;
private function __construct(string $group, int $ttl)
{
@@ -58,6 +60,29 @@
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
* ------------------------------------------------------------------- */
@@ -358,12 +383,13 @@
?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);
diff --git a/inc/managers/CustomTable.php b/inc/managers/CustomTable.php
index cb458ce..572d8a2 100644
--- a/inc/managers/CustomTable.php
+++ b/inc/managers/CustomTable.php
@@ -49,15 +49,16 @@
*
* @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])) {
@@ -260,7 +261,7 @@
* @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;
@@ -269,6 +270,12 @@
$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';
diff --git a/inc/managers/DashboardManager.php b/inc/managers/DashboardManager.php
index 26b724f..395e6c2 100644
--- a/inc/managers/DashboardManager.php
+++ b/inc/managers/DashboardManager.php
@@ -25,11 +25,11 @@
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();
@@ -45,11 +45,12 @@
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([
@@ -58,7 +59,11 @@
'fields' => 'ids',
]);
});
- return array_merge($ids, $cached);
+ if (!empty($exclude)) {
+ $IDs = array_merge($IDs, $exclude);
+ }
+
+ return $IDs;
}
/**
diff --git a/inc/managers/DirectoryManager.php b/inc/managers/DirectoryManager.php
index a72afb8..4645f97 100644
--- a/inc/managers/DirectoryManager.php
+++ b/inc/managers/DirectoryManager.php
@@ -325,7 +325,7 @@
string $name = '',
string $url = '',
string $ID = '',
- $extra = false
+ ?array $extra = null
):array {
if ($name == '') {
$name = get_the_title();
@@ -438,12 +438,20 @@
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 = [];
@@ -466,7 +474,7 @@
$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();
@@ -673,12 +681,13 @@
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);
diff --git a/inc/managers/FavouritesManager.php b/inc/managers/FavouritesManager.php
index 563dde2..0ea911c 100644
--- a/inc/managers/FavouritesManager.php
+++ b/inc/managers/FavouritesManager.php
@@ -29,7 +29,7 @@
}
private function defineFavouriteTable():void
{
- $table = CustomTable::for('favourites');
+ $table = CustomTable::for('favourites', true);
$table->setColumns([
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
diff --git a/inc/managers/InvitationsManager.php b/inc/managers/InvitationsManager.php
index ccb2f2f..c8d17a2 100644
--- a/inc/managers/InvitationsManager.php
+++ b/inc/managers/InvitationsManager.php
@@ -25,7 +25,7 @@
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']);
diff --git a/inc/managers/KarmaManager.php b/inc/managers/KarmaManager.php
index 4aaa56e..7b4cccc 100644
--- a/inc/managers/KarmaManager.php
+++ b/inc/managers/KarmaManager.php
@@ -60,7 +60,7 @@
// 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);
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index 68db551..39930db 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -66,7 +66,7 @@
// 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
{
@@ -75,7 +75,10 @@
public function excludeLoginSitemap(array $ids): array
{
- $ids[] = $this->getLoginPage();
+ $loginPage = $this->getLoginPage();
+ if (!empty($loginPage)) {
+ $ids = array_merge($ids, [$loginPage]);
+ }
return $ids;
}
/**************************************************************************
diff --git a/inc/managers/Notifications/EmailDigests.php b/inc/managers/Notifications/EmailDigests.php
index 978ff7f..8ab699e 100644
--- a/inc/managers/Notifications/EmailDigests.php
+++ b/inc/managers/Notifications/EmailDigests.php
@@ -18,8 +18,6 @@
class EmailDigests
{
protected string $campaign;
- protected Cache $terms;
- protected Cache $users;
protected CustomTable $userIndex;
protected CustomTable $termIndex;
public function __construct()
diff --git a/inc/managers/OperationQueue.php b/inc/managers/OperationQueue.php
index 3b93ccf..7ea6aaa 100644
--- a/inc/managers/OperationQueue.php
+++ b/inc/managers/OperationQueue.php
@@ -59,7 +59,7 @@
{
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']);
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 55f95a6..fb58b94 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -55,11 +55,11 @@
$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();
}
diff --git a/inc/managers/SEO/BreadcrumbManager.php b/inc/managers/SEO/BreadcrumbManager.php
index 633ade4..41f82ac 100644
--- a/inc/managers/SEO/BreadcrumbManager.php
+++ b/inc/managers/SEO/BreadcrumbManager.php
@@ -56,14 +56,14 @@
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;
diff --git a/inc/managers/ScriptLoader.php b/inc/managers/ScriptLoader.php
index 22a52b4..b8fa823 100644
--- a/inc/managers/ScriptLoader.php
+++ b/inc/managers/ScriptLoader.php
@@ -1,8 +1,11 @@
<?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
@@ -45,6 +48,7 @@
$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',
diff --git a/inc/managers/UserTermsManager.php b/inc/managers/UserTermsManager.php
index cf8d595..ffc982e 100644
--- a/inc/managers/UserTermsManager.php
+++ b/inc/managers/UserTermsManager.php
@@ -12,14 +12,12 @@
}
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);
diff --git a/inc/managers/queue/Storage.php b/inc/managers/queue/Storage.php
index 2a5d540..68b8d19 100644
--- a/inc/managers/queue/Storage.php
+++ b/inc/managers/queue/Storage.php
@@ -19,7 +19,7 @@
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
diff --git a/inc/managers/queue/executors/ContentExecutor.php b/inc/managers/queue/executors/ContentExecutor.php
index 55835f0..6b6aa1a 100644
--- a/inc/managers/queue/executors/ContentExecutor.php
+++ b/inc/managers/queue/executors/ContentExecutor.php
@@ -249,12 +249,7 @@
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 [];
@@ -465,11 +460,7 @@
$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;
@@ -488,11 +479,8 @@
});
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') {
diff --git a/inc/managers/queue/executors/UploadExecutor.php b/inc/managers/queue/executors/UploadExecutor.php
index 5f244d6..ac659a4 100644
--- a/inc/managers/queue/executors/UploadExecutor.php
+++ b/inc/managers/queue/executors/UploadExecutor.php
@@ -329,6 +329,7 @@
$postID = wp_get_post_parent_id($attachmentId);
if ($postID && !in_array($postID, $postsAttachedTo)){
$postsAttachedTo[] = $postID;
+ //TODO: is there a better way?
}
}
@@ -681,14 +682,7 @@
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;
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index c2688ae..522b5d1 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -107,6 +107,11 @@
'type' => 'datetime',
'label' => 'Date',
],
+ 'post_modified' => [
+ 'type' => 'datetime',
+ 'label' => 'Date Modified',
+ 'hidden' => true,
+ ],
'post_content' => [
'type' => 'textarea',
'quill' => true,
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index c4f0989..88668ec 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -49,6 +49,8 @@
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
@@ -56,7 +58,7 @@
//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'
];
@@ -236,6 +238,59 @@
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),
@@ -503,6 +558,11 @@
}
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) {
@@ -702,6 +762,10 @@
}
}
+ if ($this->prefix_post_type) {
+ $this->addPostTypeRewrites();
+ }
+
if ($this->registrar) {
$this->registrar->register();
}
@@ -1079,4 +1143,53 @@
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];
+ }
}
diff --git a/inc/registrar/config/seo/Meta.php b/inc/registrar/config/seo/Meta.php
index 26a4ac9..f7522e1 100644
--- a/inc/registrar/config/seo/Meta.php
+++ b/inc/registrar/config/seo/Meta.php
@@ -35,6 +35,7 @@
if (!function_exists('tsf')){
return;
}
+
if ($this->hasTitle()){
add_filter('the_seo_framework_title_from_generation', [$this, 'filterTitle'], 10, 2);
}
diff --git a/inc/registrar/config/seo/Schema.php b/inc/registrar/config/seo/Schema.php
index c9e83e5..86401cc 100644
--- a/inc/registrar/config/seo/Schema.php
+++ b/inc/registrar/config/seo/Schema.php
@@ -93,49 +93,8 @@
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);
@@ -487,6 +446,7 @@
'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);
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index dd25f8e..b5beed6 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -598,7 +598,7 @@
$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 = [];
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index 3494f5b..32d7866 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -37,10 +37,10 @@
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'));
diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index 76f6b61..6abb894 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -149,21 +149,16 @@
$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,
];
@@ -173,23 +168,18 @@
//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]));
- foreach ($IDs as $ID) {
- $imgIDs[$ID] = jvbImageData($ID);
- }
+ $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();
}
@@ -212,8 +202,6 @@
$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);
@@ -253,37 +241,30 @@
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;
}
@@ -546,7 +527,6 @@
// Add term to tax query
$args['tax_query'][] = [
'taxonomy' => $registrar->getBased(),
-// 'field' => 'term_id',
'terms' => [(int)$context['id']],
];
break;
@@ -630,7 +610,7 @@
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;
diff --git a/inc/rest/routes/LoginRoutes.php b/inc/rest/routes/LoginRoutes.php
index 0ef1550..2e863ee 100644
--- a/inc/rest/routes/LoginRoutes.php
+++ b/inc/rest/routes/LoginRoutes.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\rest\routes;
+use JVBase\managers\Cache;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\rest\Route;
@@ -711,24 +712,30 @@
}
}
+ public static function auth():array
+ {
+ return (new self)->buildAuth();
+ }
+
protected function buildAuth(?int $user = null): array
{
- if (is_user_logged_in()) {
- $user = ($user) ?: get_current_user_id();
- return [
- 'authenticated' => true,
- 'user' => $user,
- 'nonces' => $this->getUserNonces($user)
- ];
- }
+ $userId = $user ?? (is_user_logged_in() ? get_current_user_id() : 0);
+ $cacheKey = $userId ?: 'guest';
- return [
- 'authenticated' => false,
- 'user' => false,
- 'nonces' => [
- 'wp_rest' => wp_create_nonce('wp_rest')
- ]
- ];
+ 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')],
+ ];
+ });
}
protected function getUserNonces(int $userID):array {
$nonces = [
diff --git a/inc/rest/routes/QueueRoutes.php b/inc/rest/routes/QueueRoutes.php
index 954e0a6..c1017be 100644
--- a/inc/rest/routes/QueueRoutes.php
+++ b/inc/rest/routes/QueueRoutes.php
@@ -88,7 +88,7 @@
{
$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'])))
diff --git a/inc/utility/Image.php b/inc/utility/Image.php
index 6c7f41d..0626891 100644
--- a/inc/utility/Image.php
+++ b/inc/utility/Image.php
@@ -81,7 +81,7 @@
$postSlug = jvbNoBase($tax->taxonomy);
}elseif (is_post_type_archive()) {
$obj = get_queried_object();
- $postSlug = jvbNoBase($obj->post_type);
+ $postSlug = jvbNoBase($obj->name);
}
}
diff --git a/jvb.php b/jvb.php
index 226515f..e4e4bb7 100644
--- a/jvb.php
+++ b/jvb.php
@@ -463,7 +463,7 @@
));
}
- 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');
@@ -596,3 +596,10 @@
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);
+}
diff --git a/src/feed/block.json b/src/feed/block.json
index 3fec37e..8a5498f 100644
--- a/src/feed/block.json
+++ b/src/feed/block.json
@@ -7,7 +7,7 @@
"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,
diff --git a/src/feed/style.scss b/src/feed/style.scss
index 5bf4033..f18c929 100644
--- a/src/feed/style.scss
+++ b/src/feed/style.scss
@@ -1088,6 +1088,9 @@
opacity: .8;
}
}
+ ul {
+ margin: 0;
+ }
&[data-timeline] {
.images {
diff --git a/src/feed/view.js b/src/feed/view.js
index 5abe47d..a8c9946 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -7,6 +7,7 @@
this.error = window.jvbError;
this.cache = new window.jvbCache('feed');
this.templates = window.jvbTemplates;
+ this.isFirstLoad = true;
this.config = {
contextId: '',
@@ -17,35 +18,23 @@
... this.container.dataset
};
+
this.init();
}
init() {
this.initElements();
this.defineTemplates();
+
this.initListeners();
this.initFilters();
- if ('requestIdleCallback' in window) {
- requestIdleCallback(() => {
- this.initStore();
- this.initTaxonomies();
+ this.initStore();
+ 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);
- }
+ this.processCachedFilters();
+ this.processURLFilters();
+ this.updateFilterUI();
+ this.initGallery();
}
initElements() {
@@ -486,7 +475,8 @@
TTL: 6 * 60 * 60 * 1000, //6 hours
showLoading: true,
required: 'content',
- }
+ },
+ 2
);
this.store = store.feed;
@@ -494,6 +484,14 @@
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) {
@@ -659,10 +657,10 @@
];
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']);
@@ -710,7 +708,6 @@
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();
--
Gitblit v1.10.0