Jake Vanderwerf
5 days ago a9b3b28d001941921aa70d37fdc87c758a163a44
=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.
47 files modified
1128 ■■■■■ changed files
assets/js/concise/AuthManager.js 117 ●●●●● patch | view | raw | blame | history
assets/js/concise/DataStore.js 2 ●●● patch | view | raw | blame | history
assets/js/concise/Queue.js 5 ●●●●● patch | view | raw | blame | history
assets/js/concise/Referral.js 4 ●●●● patch | view | raw | blame | history
assets/js/min/auth.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/queue.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/referral.min.js 2 ●●● patch | view | raw | blame | history
build/feed/block.json 2 ●●● patch | view | raw | blame | history
build/feed/style-index-rtl.css 2 ●●● patch | view | raw | blame | history
build/feed/style-index.css 2 ●●● patch | view | raw | blame | history
build/feed/view.asset.php 2 ●●● patch | view | raw | blame | history
build/feed/view.js 2 ●●● patch | view | raw | blame | history
inc/blocks/FAQBlock.php 55 ●●●●● patch | view | raw | blame | history
inc/blocks/FeedBlock.php 398 ●●●● patch | view | raw | blame | history
inc/blocks/TimelineBlock.php 8 ●●●● patch | view | raw | blame | history
inc/helpers/all.php 14 ●●●●● patch | view | raw | blame | history
inc/managers/Cache.php 34 ●●●● patch | view | raw | blame | history
inc/managers/CustomTable.php 13 ●●●● patch | view | raw | blame | history
inc/managers/DashboardManager.php 17 ●●●●● patch | view | raw | blame | history
inc/managers/DirectoryManager.php 21 ●●●● patch | view | raw | blame | history
inc/managers/FavouritesManager.php 2 ●●● patch | view | raw | blame | history
inc/managers/InvitationsManager.php 2 ●●● patch | view | raw | blame | history
inc/managers/KarmaManager.php 2 ●●● patch | view | raw | blame | history
inc/managers/LoginManager.php 7 ●●●● patch | view | raw | blame | history
inc/managers/Notifications/EmailDigests.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/OperationQueue.php 2 ●●● patch | view | raw | blame | history
inc/managers/ReferralManager.php 6 ●●●● patch | view | raw | blame | history
inc/managers/SEO/BreadcrumbManager.php 8 ●●●● patch | view | raw | blame | history
inc/managers/ScriptLoader.php 6 ●●●● patch | view | raw | blame | history
inc/managers/UserTermsManager.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/queue/Storage.php 2 ●●● patch | view | raw | blame | history
inc/managers/queue/executors/ContentExecutor.php 20 ●●●● patch | view | raw | blame | history
inc/managers/queue/executors/UploadExecutor.php 10 ●●●● patch | view | raw | blame | history
inc/registrar/Fields.php 5 ●●●●● patch | view | raw | blame | history
inc/registrar/Registrar.php 115 ●●●●● patch | view | raw | blame | history
inc/registrar/config/seo/Meta.php 1 ●●●● patch | view | raw | blame | history
inc/registrar/config/seo/Schema.php 42 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ContentRoutes.php 2 ●●● patch | view | raw | blame | history
inc/rest/routes/FavouritesRoutes.php 8 ●●●● patch | view | raw | blame | history
inc/rest/routes/FeedRoutes.php 80 ●●●●● patch | view | raw | blame | history
inc/rest/routes/LoginRoutes.php 37 ●●●●● patch | view | raw | blame | history
inc/rest/routes/QueueRoutes.php 2 ●●● patch | view | raw | blame | history
inc/utility/Image.php 2 ●●● patch | view | raw | blame | history
jvb.php 9 ●●●● patch | view | raw | blame | history
src/feed/block.json 2 ●●● patch | view | raw | blame | history
src/feed/style.scss 3 ●●●●● patch | view | raw | blame | history
src/feed/view.js 45 ●●●● patch | view | raw | blame | history
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
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);
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) {
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',
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())})})}};
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)}})}};
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)})})})();
(()=>{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)})})})();
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)})})})();
(()=>{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)})})})();
build/feed/block.json
@@ -10,7 +10,7 @@
    "feed",
    "grid"
  ],
  "version": "0.9.0",
  "version": "1.0.0",
  "textdomain": "jvb",
  "supports": {
    "html": false,
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))}
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))}
build/feed/view.asset.php
@@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => '7bbcf703e79934b80731');
<?php return array('dependencies' => array(), 'version' => '7e7d1570989c0c348bd7');
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)})})})();
(()=>{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)})})})();
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>';
        }
        ?>
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);
    }
}
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();
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
    ]);
}
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);
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';
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;
    }
    /**
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);
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',
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']);
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);
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;
    }
    /**************************************************************************
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()
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']);
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();
        }
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;
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',
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);
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
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') {
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;
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,
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];
    }
}
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);
        }
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);
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 = [];
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'));
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;
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 = [
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'])))
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);
                }
            }
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);
}
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,
src/feed/style.scss
@@ -1088,6 +1088,9 @@
                opacity: .8;
            }
        }
        ul {
            margin: 0;
        }
        &[data-timeline] {
            .images {
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();