From 97e7c319d656a5f05489ca996e249e7359303d4d Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 31 May 2026 22:42:33 +0000
Subject: [PATCH] =Jakevan edits done?

---
 src/feed/viewOld.js | 2237 +++++++++++++++++-----------------------------------------
 1 files changed, 675 insertions(+), 1,562 deletions(-)

diff --git a/src/feed/viewOld.js b/src/feed/viewOld.js
index 347e997..c65be60 100644
--- a/src/feed/viewOld.js
+++ b/src/feed/viewOld.js
@@ -1,1657 +1,770 @@
-class FeedBlock {
-    static LOADING_QUIPS = JSON.parse(feedSettings.quips);
-    constructor(container){
-
-        this.cache = window.jvbCache;
-        this.eventHandlers = new Map();
-
-        this.rendered = {};
-
-        // Store container references
-        this.container = container;
-        this.a11y = window.jvbA11y;
-
-
-        //For tracking cache and current requests
-        this.currentRequest = null;
-        this.timeoutId = null;
-
-        // Initialize Config
-        this.initConfig();
-
-
-        // Setup error handler
-        this.error = window.jvbError;
-
-        this.resetState();
-        this.state.firstLoad = false;
-        this.state.URLProcessed = false;
-        this.highlightGot = false;
-
-        this.selectorInstances = {};
-
-        this.elements = {
-            filters: this.container.querySelector('form.feed-filters'),
-            selected: this.container.querySelector('.selected-items'),
-            clearFilters: this.container.querySelector('button.clear-filters'),
-            grid: this.container.querySelector('.feed-grid'),
-            loadMore: this.container.querySelector('button.load-more'),
-            spinner: this.container.querySelector('.loading-spinner'),
-            loading: this.container.querySelector('.feed-overlay'),
-            matchAll: this.container.querySelector('.filter-actions .toggle-text'),
-        }
-        this.filters = {
-            content: this.getCurrentContent(),
-            taxonomies: {},
-            favourites: false,
-            orderby: 'date',
-            order: 'desc',
-        }
-
-        this.feed = {
-            imageLoadThreshold: 5,
-            lazyLoadOffset: '100px',
-            gallery: [],
-            loaded: 0,
-            intersectionObserver: null,
-            templates: new Map()
-        }
-        this.imageObserver = null;
-        this.resizeObserver = null;
-
-        this.highlight = null;
-        //Loading settings
-        this.loadingIndex = 0;
-        this.quips = this.initializeQuips();
-        this.loadingMessage = this.container.querySelector('.loading-message');
-        this.dotsElement = this.container.querySelector('.loading-dots');
-        this.quipInterval = null;
-        this.loadingOptions = {
-            loadingMessages: {},
-            cycleInterval: 2000, //time between loading messages
-        }
-
-        this.initFilters();
-        if(this.container.dataset.gallery){
-            this.initGallery();
-            this.setupGalleryAccessibility();
-        }
-        this.initListeners();
-        if(!this.state.URLProcessed){
-            this.updateFilters();
-        }
-
-        this.selectedListeners = this.checkSelectedClicks.bind(this);
-        this.updateSelectedListeners();
-    }
-
-    initConfig() {
-        // Get settings from container data attribute
-        const settings = JSON.parse(this.container.dataset.settings || '{}');
-        let content = Array.from(this.container.querySelectorAll('input[name="content"]')).map(content=> content.value);
-        this.config = {
-            api: feedSettings.apiUrl,
-            nonce: feedSettings.nonce,
-            currentUser: jvbSettings.currentUser || null,
-
-            content: content[0],
-            contentTypes: content,
-            taxonomies: Array.from(this.container.querySelectorAll('.jvb-selector')).map(taxonomy => taxonomy.dataset.taxonomy),
-
-            // Source information for analytics
-            source: this.container.dataset.source || '',
-            context: this.container.dataset.context || '',
-
-            // Optional highlight
-            highlight: null,
-
-            // Gallery mode
-            isGallery: this.container.dataset.gallery || false,
-            showAuthor: !this.container.dataset.gallery || true,
-            showDate: this.container.dataset.gallery || false,
-
-
-            // User preferences
-            viewMode: localStorage.getItem('feedViewMode') || 'grid',
-        }
-
-        if(settings.isGallery){
-            this.config.highlight = this.getHighlight();
-        }
-    }
-
-    initFilters(){
-        this.updateContentFor(this.getCurrentContent());
-    }
-
-    checkSelectedClicks(e){
-        if(e.target.closest('.remove-item')){
-            let tag = e.target.closest('.selected-item');
-
-            // Uncheck checkbox if it exists
-            let taxonomy = tag.dataset.taxonomy;
-            this.clearSelectedTerm(tag.dataset.id, taxonomy);
-            // Remove tag
-            tag.remove();
-
-            // Update clear filters button visibility
-            this.updateClearFiltersButton();
-            this.updateFilters();
-            this.updateSelectedListeners();
-        }
-    }
-
-    updateSelectedListeners(){
-        this.elements.selected.removeEventListener('click', this.selectedListeners);
-        if(this.elements.selected.children.length>0){
-            this.elements.selected.addEventListener('click', this.selectedListeners);
-        }
-    }
-
-    handleFilterChange(e){
-        if(e.target.closest('.jvb-selector')){
-            return;
-        }
-        this.resetPage();
-        if(e.target.name === 'content'){
-            let content = e.target.value;
-            this.updateContentFor(content);
-        }
-        this.updateFilters();
-    }
-    handleLoadMore(){
-        if (this.state.loading || !this.state.hasMore) {
-            return;
-        }
-        this.fetchFeed();
-    }
-    initListeners(){
-        window.addEventListener('popstate', this.handlePopState.bind(this));
-        this.addEvent(this.elements.filters, 'change', (e) => this.handleFilterChange(e));
-        this.addEvent(this.elements.filters, 'submit', e => e.preventDefault());
-        this.addEvent(this.elements.loadMore, 'click', () => this.handleLoadMore());
-        this.addEvent(this.elements.clearFilters, 'click', () => this.clearSelectedTaxonomies());
-        if(this.config.isGallery){
-            this.addEvent(this.elements.grid, 'click', (e) =>this.handleGalleryOpen(e));
-        }
-
-        // Intersection observer for lazy loading
-        if ('IntersectionObserver' in window) {
-            this.imageObserver = new IntersectionObserver(entries => {
-                entries.forEach(entry => {
-                    if (entry.isIntersecting) {
-                        this.loadImage(entry.target);
-                        this.imageObserver.unobserve(entry.target);
-                    }
-                });
-            }, {
-                rootMargin: this.feed.lazyLoadOffset,
-                threshold: 0.1
-            });
-        }
-        // Resize observer for responsive images
-        if ('ResizeObserver' in window) {
-            this.resizeObserver = new ResizeObserver(window.debounce(() => {
-                this.updateImageSizes();
-            }, 250));
-
-            // Observe the container
-            this.resizeObserver.observe(this.container);
-        } else {
-            // Fallback to window resize
-            window.addEventListener('resize', window.debounce(() => {
-                this.updateImageSizes();
-            }, 250));
-        }
-    }
-    handleGalleryOpen(e){
-        const feedImage = e.target.closest('.feed-image');
-        if(feedImage){
-            const item = feedImage.closest('.feed-item');
-            if(item) {
-                const index = Array.from(this.container.querySelectorAll('.feed-item')).indexOf(item);
-                if(index !== -1){
-                    this.openGallery(index);
-                }
-            }
-        }
-    }
-    initGallery(){
-        this.gallery = {
-            items: this.getGalleryItems() || [],
-            index: 0,
-            touchStart: null,
-            touchEnd: null,
-            minSwipe: 50,
-            modal: this.createGalleryModal(),
-            keyHandler: null,
-            loading: false
-        }
-        document.body.appendChild(this.gallery.modal);
-    }
-
-    /**
-     * Handle browser history navigation
-     */
-    handlePopState(e) {
-        if (e.state && e.state.filters) {
-            if(this.processURLFilters()){
-
-                // Load items with updated filters
-                this.resetPage();
-                this.fetchFeed();
-
-                // Announce to screen readers
-                this.a11y.announce('Feed filters updated from browser history.');
-            }
-        }
-    }
-
-    processURLFilters(){
-        const params = new URLSearchParams(window.location.search);
-        if (!params.toString()) {
-            return false; // No parameters to process
-        }
-        // Initialize content type
-        const content = params.get('f_content');
-        params.delete('f_content');
-        if (content && this.elements.filters.querySelector(`input[value="${content}"]`)) {
-            this.elements.filters.querySelector(`input[value="${content}"]`).checked = true;
-            this.filters.content = content;
-        }
-
-        // Initialize order
-        const order = params.get('f_order');
-        params.delete('f_order');
-        if (order && this.elements.filters.querySelector(`input[name="order"][value="${order}"]`)) {
-            this.elements.filters.querySelector(`input[name="order"][value="${order}"]`).checked = true;
-            this.filters.order = order;
-        }
-
-        // Initialize orderby
-        const orderby = params.get('f_orderby');
-        params.delete('f_orderby');
-        if (orderby && this.elements.filters.querySelector(`input[name="orderby"][value="${orderby}"]`)) {
-            this.elements.filters.querySelector(`input[name="orderby"][value="${orderby}"]`).checked = true;
-            this.filters.orderby = orderby;
-        }
-
-        // Initialize favourites filter
-        if (params.get('f_favourites') === 'true' && this.config.currentUser !== null) {
-            params.delete('f_favourites');
-            const favCheckbox = this.elements.filters.querySelector(`input[name="favourites_only"]`);
-            if (favCheckbox) favCheckbox.checked = true;
-            this.filters.favourites = true;
-        }
-
-        // Initialize match, if present
-        if(params.get('f_match') === 'all'){
-            params.delete('f_match');
-            this.elements.matchAll.querySelector('input').checked = true;
-        }
-
-        window.removeChildren(this.elements.selected);
-
-        let filters = JSON.parse(JSON.stringify(this.filters));
-        let unprocessed = {};
-        for (var [key, value] of Object.entries(Object.fromEntries(params))) {
-            if (key.startsWith('f_') ) {
-                var taxName = key.replace('f_', '');
-                let cache = this.cache.getItem(taxName+'List');
-                if(!Object.keys(filters['taxonomies']).includes(taxName)){
-                    filters.taxonomies[taxName] = {};
-                }
-
-                // Handle both single values and comma-separated values
-                const termIds = value.includes(',') ? value.split(',') : [value];
-                termIds.forEach(termId=>{
-                    if(cache && cache.hasOwnProperty(termId)){
-                        filters.taxonomies[taxName][termId] = cache[termId].name;
-                        let tag = this.createFilterTag(taxName, termId, cache[termId].name);
-                        this.elements.selected.appendChild(tag);
-                    }else{
-                        if(!unprocessed.hasOwnProperty(taxName)){
-                            unprocessed[taxName] = [];
-                        }
-                        unprocessed[taxName].push(termId);
-                    }
-                });
-            }
-        }
-
-        this.filters = filters;
-
-        if(!isEmptyObject(unprocessed)){
-            this.fetchTermDetails(unprocessed);
-        }
-
-        if(this.config.isGallery){
-            this.getHighlight();
-        }
-
-        this.updateContentFor(content);
-        this.updateSelectedListeners();
-        return true;
-
-    }
-
-    async loadFromURL() {
-
-        if(this.processURLFilters()){
-            // Announce to screen readers
-            this.a11y.announce('Feed filters updated from URL.');
-        }
-        return true;
-    }
-
-    //We already checked the cache, now fetch the remaining term information
-    async fetchTermDetails(terms) {
-        let params = new URLSearchParams(terms);
-
-        // Otherwise fetch the term details
-        try {
-            // Format the URL - might need to adjust based on your API structure
-            const url = `${this.config.api}terms/check?`+params.toString();
-
-            const termData = await this.cache.fetchWithCache(
-                url, {
-                    method: 'GET',
-                },
-                {
-            });
-
-            for(const [taxonomy, terms] of Object.entries(termData.terms)){
-                if(!this.filters.taxonomies.hasOwnProperty(taxonomy)){
-                    this.filters.taxonomies[taxonomy] = {};
-                }
-
-                this.cache.setItem(taxonomy+'List', terms, taxonomy);
-
-                for(const [termId, termData] of Object.entries(terms)){
-                    this.filters.taxonomies[taxonomy][termId] = termData.name;
-                    let tag = this.createFilterTag(taxonomy, termId, termData.name);
-                    this.elements.selected.appendChild(tag);
-                }
-                this.updateSelectedListeners();
-            }
-
-        } catch (error) {
-            console.error(`Error fetching term details for ${terms}:`, error);
-
-        }
-    }
-
-    //State Management
-    nextPage(hasMore = true){
-        this.state.page++;
-        this.state.hasMore = hasMore;
-    }
-    resetPage(hasMore = true){
-        this.state.page = 1;
-        this.state.hasMore = hasMore;
-    }
-    resetState() {
-        this.state = {
-            page: 1,
-            loading: false,
-            hasMore: true,
-            retries: {
-                count: 0,
-                max: 3,
-                delay: 1000
-            }
-        }
-    }
-
-    updateFilters(reset = true){
-        let updated = false;
-        let content = this.getCurrentContent();
-        let favourites = this.getFavouritesOnly();
-        let order = this.getCurrentOrder();
-        let orderby = this.getCurrentOrderby();
-        let taxonomies = this.getSelectedTaxonomies();
-
-        if(this.filters.content !== content ||
-            this.filters.favourites !== favourites ||
-            this.filters.order !== order ||
-            this.filters.orderby !== orderby ||
-            this.filters.taxonomies !== taxonomies ){
-            updated = true;
-        }
-        (this.filters.content !== content)??this.updateContentFor(this.filters.content);
-
-        this.filters.content = content;
-        this.filters.favourites = favourites;
-        this.filters.order = order;
-        this.filters.orderby = orderby;
-        this.filters.taxonomies = taxonomies;
-
-        if(this.state.firstLoad){
-            this.updateURL();
-        }else{
-            this.state.firstLoad = true;
-        }
-
-
-        if(reset){
-            this.resetPage();
-        }
-        this.fetchFeed();
-    }
-    getCurrentContent(){
-        return this.elements.filters.querySelector('input[name="content"]:checked').value;
-    }
-    getFavouritesOnly(){
-        return this.elements.filters.querySelector('input[name="favourites_only"]')?.checked ??false;
-    }
-    getCurrentOrder(){
-        return this.elements.filters.querySelector('input[name="order"]:checked').value;
-    }
-    getCurrentOrderby(){
-        return this.elements.filters.querySelector('input[name="orderby"]:checked').value;
-    }
-
-    getCurrentTaxonomies(){
-        let taxonomies = this.filters.taxonomies;
-        let out = {};
-        if(!isEmptyObject(taxonomies)){
-            for(var [tax, terms] of Object.entries(taxonomies)){
-                if(!isEmptyObject(terms)){
-                    out[tax] =  Object.values(terms);
-                }
-            }
-        }
-        return out;
-    }
-    updateContentFor(content){
-
-        this.elements.filters.querySelectorAll('.jvb-selector').forEach(tax=> {
-            tax.hidden = !tax.dataset.for.includes(content);
-            //Ensure any selected Taxonomies are removed
-            if(!tax.dataset.for.includes(content)){
-                this.clearSelectedTerms(tax);
-            }
-            //TODO: Ensure clean up of filtered out window.jvbTaxonomySelector instances?
-            //Maybe cache them?
-            let taxonomy = tax.dataset.taxonomy;
-            if(tax.dataset.for.includes(content)){
-                if(!this.selectorInstances.hasOwnProperty(taxonomy)){
-                    this.selectorInstances[taxonomy] = new window.jvbTaxonomySelector(tax, {
-                        multiple: true,
-                        feed: true,
-                        selected: this.filters[taxonomy]??{},
-                        onClose: () => this.setSelectedTerms(taxonomy)
-                    });
-                }
-            }else if(this.selectorInstances.hasOwnProperty(taxonomy)){
-                this.clearSelectedTerms(taxonomy);
-                delete this.selectorInstances[taxonomy];
-            }
-        });
-        this.elements.filters.querySelectorAll('input[data-for]').forEach(toggle=>{
-            toggle.hidden = !toggle.dataset.for.includes(content);
-        });
-        this.elements.filters.querySelectorAll('input[name="order"]').forEach(order=>{
-            order.hidden = this.filters.orderby === 'random';
-        });
-    }
-    updateURL(){
-        const params = new URLSearchParams();
-
-        let taxonomies = this.filters.taxonomies;
-        if(!isEmptyObject(taxonomies)){
-            for(var [tax, terms] of Object.entries(taxonomies)){
-                if(!isEmptyObject(terms)){
-                    params.set('f_'+tax, Object.keys(terms));
-                }
-
-            }
-        }
-
-        // Clone to avoid modifying original
-        let filters = JSON.parse(JSON.stringify(this.filters));
-        delete filters.taxonomies;
-        for(const [key, value] of Object.entries(filters)){
-            if(value !== false){
-                params.set('f_'+key, value);
-            }
-        }
-        if(this.elements.matchAll.querySelector('input:checked')){
-            params.set('f_match', 'all');
-        }
-        const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
-        history.pushState({ filters }, '', newUrl);
-    }
-
-    /**
-     * Feed Fetching
-     * @param reset clear grid
-     * @param force force cache busting
-     * @returns {Promise<void>}
-     */
-    async fetchFeed(reset = false, force = false){
-        if(this.state.loading){
-            return;
-        }
-
-        this.updateLoading(true);
-        try{
-            // Track URL processing
-            if(this.state.page === 1){
-                reset = true;
-                await this.loadFromURL();
-            }
-
-            const params = this.buildFilterRequest();
-            if(this.elements.matchAll.querySelector('input:checked')){
-                params.append('match', true);
-            }
-            params.append('page', this.state.page);
-            params.append('source', this.config.source);
-            params.append('context', this.config.context);
-            if(this.config.highlight){
-                params.append('highlight', JSON.stringify(this.config.highlight));
-            }
-
-            //fetch data with cache busting
-            const data = await this.cache.fetchWithCache(
-                `${this.config.api}feed?${params.toString()}`,
-                {
-                    method: 'GET',
-                },
-                {
-                    context: 'feed',
-                    forceRefresh: true,
-                    // forceRefresh: force,
-                }
-            )
-
-			console.log(data, 'Fetched data: ');
-
-            //Clear grid on first page
-            if(this.state.page === 1){
-                window.removeChildren(this.elements.grid);
-            }
-
-            //Handle empty results
-            if(!data || !data.items || data.items.length === 0){
-                if(this.state.page === 1){
-                    this.showEmptyState();
-                }
-                this.state.hasMore = false;
-            }else{
-                this.state.hasMore = data.hasMore;
-                if(this.state.hasMore){
-                    this.nextPage();
-                }
-                //Render items
-                this.renderItems(data.items, this.state.page > 1);
-
-            }
-
-        }catch(error){
-            this.handleError(error);
-        }finally{
-            this.updateLoading(false);
-            this.elements.loadMore.hidden = !this.state.hasMore;
-        }
-
-    }
-
-    buildFilterRequest(){
-        // Clone to avoid modifying original
-        const filters = JSON.parse(JSON.stringify(this.filters));
-
-        if(filters.taxonomies && !isEmptyObject(filters.taxonomies)){
-            let temp = {};
-            for(var [tax, terms] of Object.entries(filters.taxonomies)){
-                if(!isEmptyObject(terms)){
-                    temp[tax] =Object.keys(terms);
-                }
-            }
-            delete filters.taxonomies;
-            if(!isEmptyObject(temp)){
-                filters.taxonomies =   JSON.stringify(temp);
-            }
-        }else{
-            delete filters.taxonomies;
-        }
-
-
-        if(!filters.favourites){
-            delete filters.favourites;
-        }
-        return new URLSearchParams(filters);
-    }
-
-
-    handleError(error){
-        return this.error.handleApiError(
-            error,
-            {
-                component: 'Feed Block',
-                action: 'loaditems'
-            },
-            () => this.fetchFeed()
-        );
-    }
-
-
-
-    getHighlight() {
-        if(!this.config.highlight){
-            const searchParams = new URLSearchParams(window.location.search);
-
-            // Check for content type parameters
-			this.config.contentTypes.forEach(type => {
-				if (searchParams.has(type)) {
-					this.config.highlight = {};
-					this.config.highlight[type] = searchParams.get(type);
-				}
-			});
-        }
-        return this.config.highlight;
-    }
-
-    /**
-     * Loading
-     */
-    updateLoading(loading) {
-        this.state.loading = loading;
-        if (loading) {
-            this.showLoading();
-
-            let content = this.filters.content;
-            content = (content === 'artwork') ? content : content+'s';
-            let tax = '';
-            let taxonomies = this.getCurrentTaxonomies();
-            if(!isEmptyObject(taxonomies)){
-                let total = 0;
-                total = Object.values(taxonomies).map((tax)=> total+tax.length);
-                let all = [];
-                let join = (total[0] === 2) ? ' and ' : ', ';
-                tax = Object.values(taxonomies).map((tax) => all.push(tax.join(join)));
-                tax = all.join(', ');
-                let index = tax.lastIndexOf(',')+1;
-                if(index> 0){
-                    tax = tax.substr(0, index)+' and'+tax.substr(index);
-                }
-
-            }
-
-
-            this.a11y.announce(`Checking for more ${tax} ${content}...`);
-        } else {
-            this.hideLoading();
-        }
-
-        // Update loading spinner
-        this.elements.spinner.hidden = !loading;
-
-        // Update load more button
-        this.elements.loadMore.disabled = loading;
-
-    }
-
-
-    /**
-     * Show the loading overlay
-     */
-    showLoading() {
-        this.hideBody();
-        this.elements.loading.classList.add('active');
-        this.startQuipCycle();
-        document.body.classList.add('loading');
-    }
-
-    /**
-     * Hide the loading overlay
-     */
-    hideLoading() {
-        this.showBody();
-        this.container.classList.remove('active');
-        this.stopQuipCycle();
-        document.body.classList.remove('loading');
-    }
-
-
-    /**
-     * Start cycling through loading messages
-     */
-    startQuipCycle() {
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-        }
-
-        if (!this.quips.length) return;
-
-        // Set initial message
-        this.updateMessage(this.quips[0]);
-        this.container.classList.remove('changing');
-
-        this.quipInterval = setInterval(() => {
-            this.container.classList.add('changing');
-
-            setTimeout(() => {
-                this.loadingIndex = (this.loadingIndex + 1) % this.quips.length;
-                this.updateMessage(this.quips[this.loadingIndex]);
-
-                setTimeout(() => {
-                    this.container.classList.remove('changing');
-                }, 50);
-            }, 350);
-        }, this.loadingOptions.cycleInterval);
-    }
-
-    /**
-     * Stop cycling through loading messages
-     */
-    stopQuipCycle() {
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-            this.quipInterval = null;
-        }
-    }
-
-    /**
-     * Update the loading message
-     */
-    updateMessage(quipData) {
-        if (!this.loadingMessage) return;
-
-        const icon = feedSettings?.icons?.[quipData.icon] || '';
-        this.loadingMessage.innerHTML = `${icon}<p>${quipData.quip}</p>`;
-    }
-
-    /**
-     * Set a custom loading message
-     */
-    setMessage(message, icon = null) {
-        if (!this.loadingMessage) return;
-
-        this.stopQuipCycle();
-
-        const iconHtml = icon ? feedSettings?.icons?.[icon] || '' : '';
-        this.loadingMessage.innerHTML = `${iconHtml}<p>${message}</p>`;
-    }
-
-    /**
-     * Shuffle an array (Fisher-Yates algorithm)
-     */
-    shuffleArray(array) {
-        const newArray = [...array];
-        for (let i = newArray.length - 1; i > 0; i--) {
-            const j = Math.floor(Math.random() * (i + 1));
-            [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
-        }
-        return newArray;
-    }
-
-    /**
-     * Initialize quips for loading messages
-     */
-    initializeQuips() {
-        // Map pstyle, artstyle, etc. to their base types for quips
-        const typeMapping = {
-            'pstyle': 'style',
-            'artstyle': 'style',
-            'arttheme': 'theme',
-            'artmedium': 'style',
-            'artform': 'style',
-            'placement': 'style',
-            'colour': 'style'
-        };
-
-        // Gather all quips based on content types and taxonomies
-        const allQuips = [];
-
-        // Add content type quips
-        if (FeedBlock.LOADING_QUIPS[this.filters.content]) {
-            FeedBlock.LOADING_QUIPS[this.filters.content].forEach(quip => {
-                allQuips.push({
-                    icon: this.filters.content,
-                    quip: quip
-                });
-            });
-        }
-
-        // Add taxonomy quips
-        if(this.filters.taxonomies && !isEmptyObject(this.filters.taxonomies)){
-            Object.keys(this.filters.taxonomies).forEach(taxonomy => {
-                const baseType = typeMapping[taxonomy] || taxonomy;
-                if (FeedBlock.LOADING_QUIPS[baseType]) {
-                    FeedBlock.LOADING_QUIPS[baseType].forEach(quip => {
-                        allQuips.push({
-                            icon: taxonomy,
-                            quip: quip
-                        });
-                    });
-                }
-            });
-        }
-
-
-        // Shuffle the quips array
-        return this.shuffleArray(allQuips);
-    }
-
-    /**
-     * Feed Grid
-     */
-
-    renderItems(items, append = false) {
-        // Reset if not appending
-        if (!append) {
-            window.removeChildren(this.elements.grid);
-            this.feed.loaded = 0;
-            this.feed.gallery = [];
-        }
-
-        // Add items to gallery if in gallery mode
-        if (this.config.isGallery) {
-            this.feed.gallery = this.feed.gallery.concat(items);
-        }
-
-        // Bail early if no items
-        if (items.length === 0) {
-            this.a11y.announceUpdate(0, append);
-            return;
-        }
-
-        // Use DocumentFragment for better performance
-        const fragment = document.createDocumentFragment();
-
-        // Process items in batches for better performance
-        const batchSize = 10;
-        const processBatch = (startIndex) => {
-            const endIndex = Math.min(startIndex + batchSize, items.length);
-
-            // Process this batch
-            for (let i = startIndex; i < endIndex; i++) {
-                const item = items[i];
-                const element = this.createItemElement(item);
-                fragment.appendChild(element);
-
-                // Lazy load images beyond threshold
-                if (this.feed.loaded >= this.feed.imageLoadThreshold && this.imageObserver) {
-                    this.imageObserver.observe(element);
-                } else {
-                    this.loadImage(element);
-                }
-
-                this.feed.loaded++;
-            }
-
-            // If we have more items, process next batch in next frame
-            if (endIndex < items.length) {
-                requestAnimationFrame(() => {
-                    processBatch(endIndex);
-                });
-            } else {
-                // All batches processed, append fragment
-                this.elements.grid.appendChild(fragment);
-                if(this.container.dataset.gallery){
-					console.log(this.getGalleryItems());
-                    this.updateGalleryItems(this.getGalleryItems());
-                    //pagination already updated, so it'll be page 2
-                    if(this.getHighlight() && !this.highlightGot){
-                        this.highlightGot = true;
-                        this.openGallery(0);
-                    }
-                }
-                this.a11y.makeNavigable(this.elements.grid.querySelectorAll('.feed-item:not([data-keyboard-nav])'));
-                this.a11y.announceItems(items.length, append, this.state.hasMore);
-            }
-        };
-
-        // Start processing the first batch
-        if (items.length > 0) {
-            processBatch(0);
-        } else {
-            this.a11y.announceUpdate(0, append);
-        }
-    }
-
-    /**
-     * Load image for an item
-     */
-    loadImage(element) {
-        const img = element.querySelector('img');
-        if (!img) return;
-        const size = this.getImageSize();
-        img.src = img.dataset[size] || img.dataset.src;
-        delete img.dataset.src;
-
-        element.setAttribute('data-loaded', 'true');
-    }
-
-    /**
-     * Update image sizes based on screen width
-     */
-    updateImageSizes() {
-        const size = this.getImageSize();
-
-        // Update only visible images that aren't already loaded with the right size
-        const items = this.elements.grid.querySelectorAll('.feed-item[data-loaded="true"]');
-        items.forEach(item => {
-            const img = item.querySelector('img');
-            if (img && img.dataset[size] && img.src !== img.dataset[size]) {
-                img.src = img.dataset[size];
-            }
-        });
-    }
-
-    /**
-     * Create an element for a feed item
-     */
-    createItemElement(item) {
-
-        if(!this.rendered[item.icon]){
-            this.rendered[item.icon] = new Map();
-        }
-
-        if(this.rendered[item.icon].has(item.id)){
-            return this.rendered[item.icon].get(item.id);
-        }
-
-        const favourited = window.isFavourited(item.icon, item.id)??false;
-        const template = window.getTemplate('feed-item');
-
-        // Set unique attributes
-        template.id = `${item.icon}-${item.id}`;
-        template.classList.add(item.icon);
-
-		if (item.umami_view) {
-			this.buildUmamiData(template, item.umami_view);
-		}
-
-		let favouriteButton = template.querySelector('button.favourite');
-		[
-			favouriteButton.dataset.id,
-			favouriteButton.dataset.type,
-			favouriteButton.dataset.artist,
-			favouriteButton.title
-		] = [
-			item.id,
-			item.icon,
-			item.user_id,
-			(favourited) ? 'Remove from Favourites' : 'Add to Favourites'
-		];
-
-		let order = item.order;
-		let single = template.querySelector('.item');
-		let list = template.querySelector('.item-list');
-		let img = template.querySelector('.feed-images');
-		let summary = template.querySelector('summary');
-		let info = template.querySelector('.item-info');
-
-		for (let [index, id] of Object.entries(order)){
-			let target;
-			let config = item[id];
-			if (id === 'title') {
-				target = template.querySelector('h3 a');
-				if (item.title !== '') {
-					[
-						target.textContent,
-						target.href,
-						target.url
-					] = [
-						item.title,
-						item.url,
-						`Learn more about this ${item.icon}`
-					];
-					if (item.icon !== '') {
-						target.closest('h3').prepend(window.getIcon(item.icon));
-					}
-					if (item.umami_click) {
-						this.buildUmamiData(target, item.umami_click);
-					}
-				} else {
-					target.remove();
-				}
-			} else if (Object.hasOwn(config, 'terms')) {
-				//Taxonomy list
-				if (config.terms.length === 0) {
-					continue;
-				}
-				let taxonomy = list.cloneNode(true);
-				let label = taxonomy.querySelector('.label');
-				let termList = taxonomy.querySelector('ul');
-				let listItem = taxonomy.querySelector('li');
-
-				if (config.label) {
-					label.textContent = config.label;
-				}
-				if (config.icon) {
-					label.prepend(window.getIcon(config.icon));
-				}
-				if (!config.label && !config.icon){
-					label.remove();
-				}
-
-				config.terms.forEach(term => {
-					let termItem = listItem.cloneNode(true);
-					let link = termItem.querySelector('a');
-					[
-						link.href,
-						link.title,
-						link.textContent
-					] = [
-						term.url,
-						`Learn more about ${term.title}`,
-						term.title
-					];
-					if (term.umami_click.length > 0) {
-						this.buildUmamiData(link, term.umami_click);
-					}
-					termList.append(termItem);
-				});
-
-				listItem.remove();
-				info.appendChild(taxonomy);
-			} else if (Object.hasOwn(config, 'value') && config.value !== '') {
-				let itemInfo = single.cloneNode(true);
-				let label = itemInfo.querySelector('.label');
-				let link = itemInfo.querySelector('a');
-				let p = itemInfo.querySelector('p');
-				if (Object.hasOwn(config, 'label')) {
-					label.textContent = config.label;
-				}
-				if (Object.hasOwn(config, 'icon')) {
-					label.prepend(window.getIcon(config.icon));
-				}
-				if (!Object.hasOwn(config, 'icon') && !Object.hasOwn(config, 'label')) {
-					label.remove();
-				}
-				if (Object.hasOwn(config, 'url')) {
-					p.remove();
-					[
-						link.textContent,
-						link.href,
-						link.title
-					] = [
-						config.value,
-						config.url,
-						`Learn more about ${config.value}`
-					];
-				} else {
-					link.remove();
-					p.textContent = config.value;
-				}
-				info.appendChild(itemInfo);
-			} else if (id === 'image') {
-				let images = summary.querySelector('.feed-images');
-				let img = images.querySelector('a');
-
-				let main = img.cloneNode(true);
-				main.href = item.url;
-				main.classList.add('feed-image');
-				this.buildImageData(main.querySelector('img'), item.image);
-				images.append(main);
-
-				if (item.content?.length > 0) {
-					images.classList.add('multi');
-					item.content.forEach(c => {
-						let image = img.cloneNode(true);
-						image.href = c.url;
-						let itemImg = image.querySelector('img');
-						itemImg.src = c.image.small;
-						itemImg.alt = c.image.alt;
-						images.append(image);
-					});
-				}
-				img.remove();
-			}
-		}
-		single.remove();
-		list.remove();
-
-        this.rendered[item.icon].set(item.id, template);
-
-        return template;
-    }
-
-    buildImageData(img, data){
-		if (typeof data.tiny !== 'string') {
+class FeedBlockOld {
+	constructor() {
+		this.container = document.querySelector('section.feed-block');
+		if (!this.container) {
 			return;
 		}
-        [
-            img.src,
-            img.dataset.small,
-            img.dataset.medium,
-            img.dataset.large,
-            img.alt
-        ] =
-        [
-            data.tiny,
-            data.small,
-            data.medium,
-            data.large,
-            data.alt
-        ];
-    }
 
-    buildUmamiData(item, data){
-        for(let [key, value] of Object.entries(data)){
-            item.dataset[key] = value;
-        }
-    }
+		this.a11y = window.jvbA11y;
+		this.cache = new window.jvbCache('feed');
+		this.error = window.jvbError;
 
-    /**
-     * Show empty state
-     */
-    showEmptyState() {
-        const message = this.filters.favourites
-            ? `<div class="feed-empty-state">
-                <h3>♡ BLANK CANVAS ♡</h3>
-                <p>You haven't fallen in love with any pieces... yet!</p>
-                <p>Hit that heart icon when something stops your scroll.</p>
-                <p>Your dream collection is waiting to start.</p>
-            </div>`
-            : `<div class="feed-empty-state">
-                <h3>NOTHING HERE...</h3>
-                <p>Try tweaking those filters.</p>
-                <p>Edmonton's got talent - let's find it.</p>
-            </div>`;
+		this.config = {
+			source: '',
+			context: '',
+			highlight: null,
+			gallery: false,
+			view: this.cache.get('feedView') || 'grid',
+			... this.container.dataset
+		};
+		this.initElements();
+		this.initFilters();
 
-        this.elements.grid.innerHTML = message;
-        this.a11y.announceEmpty(this.filters.favourites);
-    }
 
-    /**
-     * Clear the grid
-     */
-    clearGrid() {
-        this.a11y.announce('Items cleared.');
-        window.removeChildren(this.elements.grid);
-        this.feed.loaded = 0;
-    }
+		this.loadWhenAble();
+	}
 
-    /**
-     * Get image size based on screen width
-     */
-    getImageSize() {
-        const width = window.innerWidth;
-        if (width > 1024) return 'medium';
-        if (width > 500) return 'medium';
-        return 'small';
-    }
+	loadWhenAble() {
+		if ('requestIdleCallback' in window) {
+			requestIdleCallback(() => {
+				this.initTaxonomies();
+				this.initStore();
+				this.initListeners();
+				this.initGallery();
+			}, { timeout: 2000 });
+		} else {
+			setTimeout(() => {
+				this.initTaxonomies();
+				this.initStore();
+				this.initListeners();
+				this.initGallery();
+			}, 100);
+		}
+	}
 
-    /**
-     * Get gallery items for the gallery modal
-     */
-    getGalleryItems() {
-        return Array.from(this.container.querySelectorAll('.feed-item'))
-            .map(item => {
-                const img = item.querySelector('img');
-                if (!img) return null;
+	initElements() {
+		this.currentTaxonomies = new Set();			// Allowed Taxonomies, grabbed from active buttons
+		this.taxonomyFilters = {};
+		this.elements = {
+			filterTrigger: '[data-filter]',
+			filters: {
+				content: '[data-filter="content"]',
+				orderby: '[data-filter="orderby"]',
+				order: '[data-filter="order"]',
+				match: '[data-filter="match"]',
+				favourites: '[data-filter="favourites"]',
+				taxonomy: '[data-filter^="taxonomy"]'
+			},
+			selectedTax: '.selected-items',
+			clearFilter: 'button.clear-filters',
+			loadMore: 'button.load-more',
+			filterContainer: '.filters',
+			grid: '.item-grid',
+		};
+		this.ui = window.uiFromSelectors(this.elements);
 
-                return {
-                    id: item.querySelector('button.favourite').dataset.id,
-                    small: img.dataset.small || img.src,
-                    large: img.dataset.medium || img.src,
-                    full: img.dataset.full || img.src,
-                    alt: img.alt || '',
-                    fav: item.querySelector('button.favourite')?.cloneNode(true),
-                    info: item.querySelector('.item-info')?.cloneNode(true)
-                };
-            })
-            .filter(Boolean);
-    }
 
-    /**
-     * Clean up resources when component is destroyed
-     */
-    addEvent(element, event, handler, options) {
-        if (!element) return;
+		this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
+		this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
+		if (this.ui.content && this.ui.content.length > 0) {
+			this.contentTypes = Array.from(
+				this.ui.content
+			).map(content => content.value);
+		} else {
+			this.contentTypes = [this.container.dataset['content']];
+		}
 
-        const boundHandler = handler.bind(this);
-        element.addEventListener(event, boundHandler, options);
+		if (this.ui.taxonomies.length>0) {
+			this.taxonomies = Array.from(
+				this.ui.taxonomies,
+			).map(content => content.dataset.taxonomy);
+		} else {
+			this.taxonomies = [];
+		}
 
-        // Store for cleanup
-        if (!this.eventHandlers.has(element)) {
-            this.eventHandlers.set(element, []);
-        }
-        this.eventHandlers.get(element).push({ event, handler: boundHandler });
-    }
-    destroy() {
-        // Clean up observers
-        if (this.imageObserver) {
-            this.imageObserver.disconnect();
-            this.imageObserver = null;
-        }
 
-        if (this.resizeObserver) {
-            this.resizeObserver.disconnect();
-            this.resizeObserver = null;
-        }
+	}
 
-        // Clean up all event listeners
-        this.eventHandlers.forEach((handlers, element) => {
-            handlers.forEach(({ event, handler }) => {
-                element.removeEventListener(event, handler);
-            });
-        });
-        this.eventHandlers.clear();
+	async initTaxonomies() {
+		this.selector = window.jvbSelector;
+		const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
 
-        // Clean up timers
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-        }
+		this.selector.isInitializing = true;
+		buttons.forEach((button) => {
+			const taxonomy = button.dataset.taxonomy;
+			this.currentTaxonomies.add(taxonomy);
 
-        if (this.timeoutId) {
-            clearTimeout(this.timeoutId);
-        }
+			this.selector.registerFilterButton(button, {
+				button: button,
+				buttonSelector: '[data-filter="taxonomy"]',
+				selected: this.ui.selectedTax
+			});
 
-        // Clear template cache and other state
-        this.feed.templates.clear();
-        this.feed.gallery = [];
-        this.feed.loaded = 0;
-    }
-    /** Extra Term Handling **/
+			// Add preload listeners
+			this.addTaxonomyPreloadListeners(button, taxonomy);
+		});
 
-    getSelectedTaxonomies(){
-        let taxonomies = {};
-        for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
-            taxonomies[taxonomy] = instance.selectedItems;
-        }
-        return taxonomies;
-    }
-    /**
-     * Get selected values for a taxonomy
-     */
-    getSelectedTerms(taxonomy) {
-        const selectedItems = this.elements.selected.querySelectorAll(
-            `.selected-item[data-taxonomy="${taxonomy}"]`
-        );
+		this.selector.isInitializing = false;
 
-        return Array.from(selectedItems).map(item => item.dataset.id);
-    }
+		this.selector.subscribe((event, data) => {
+			if (event === 'selected-terms') this.handleTaxonomyChange(data);
+		});
+	}
 
-    clearSelectedTaxonomies(){
-        window.removeChildren(this.elements.selected);
-        if(!isEmptyObject(this.selectorInstances)){
-            for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
-                instance.selectedItems = {};
-            }
-        }
+	addTaxonomyPreloadListeners(button, taxonomy) {
+		const preload = () => {
+			this.selector.preloadTaxonomy(taxonomy);
+		};
 
-        this.elements.matchAll.querySelector(input).checked = false;
+		// Desktop hover
+		button.addEventListener('mouseenter', preload, { once: true });
 
-        this.filters.taxonomies = {};
-        this.updateFilters();
-    }
-    setSelectedTerms(taxonomy){
-        if(this.selectorInstances[taxonomy]){
-            let selected = this.selectorInstances[taxonomy].selectedItems;
-            if(!isEmptyObject(selected)){
+		// Touch/keyboard (fires before click)
+		button.addEventListener('pointerdown', preload, { once: true });
 
-                this.filters.taxonomies[taxonomy] = selected;
-                for(var [id, name] of Object.entries(selected)){
-                    this.elements.selected.appendChild(this.createFilterTag(taxonomy, id, name));
-                }
-                this.updateFilters();
-                this.updateSelectedListeners();
-            }else{
-                delete this.filters.taxonomies[taxonomy];
-            }
+		// Keyboard focus
+		button.addEventListener('focus', preload, { once: true });
+	}
 
-        }
-    }
-    clearSelectedTerm(termId, taxonomy){
-        let container = this.container.querySelector('.filters');
-        let input = container.querySelector(`li[data-id="${termId}"] input`);
-        input.checked = false
-        delete this.selectorInstances[taxonomy].selectedItems[termId];
-    }
-    clearSelectedTerms(taxonomy){
+	handleTaxonomyChange(data) {
+		const { terms, taxonomy } = data;
 
-        if(!isEmptyObject(this.filters.taxonomies) && this.filters.taxonomies.hasOwnProperty(taxonomy)){
-            delete this.filters.taxonomies[taxonomy];
-        }
-        if(!isEmptyObject(this.selectorInstances) && this.selectorInstances.hasOwnProperty(taxonomy)){
-            this.selectorInstances[taxonomy].selectedItems = {};
-        }
+		// Update only the current taxonomy's terms
+		if (terms.size > 0) {
+			this.taxonomyFilters[taxonomy] = Array.from(terms.keys());
+		} else {
+			// Remove taxonomy if no terms selected
+			delete this.taxonomyFilters[taxonomy];
+		}
 
+		// Build filters object with all taxonomies
+		let filters = {
+			page: 1
+		};
 
-        const selectedItems = this.elements.selected.querySelectorAll(
-            `.selected-item[data-taxonomy="${taxonomy}"]`
-        );
+		// Add taxonomy filters if any exist
+		if (Object.keys(this.taxonomyFilters).length > 0) {
+			filters.taxonomy = this.taxonomyFilters;
+		}
 
-        if(selectedItems.length > 0){
-            selectedItems.forEach(item => {
-                item.remove();
-            });
-        }
+		this.updateFilter(filters);
+	}
 
-        // Update clear filters button visibility
-        this.updateClearFiltersButton();
-    }
-    updateClearFiltersButton(){
-        if (!this.elements.clearFilters) return;
+	clearAllTaxonomies() {
+		this.taxonomyFilters = {};
+		window.removeChildren(this.ui.selectedTax);
 
-        let filters = this.elements.selected.children.length;
+		this.updateFilter({
+			taxonomy: null,
+			page: 1
+		});
+	}
 
-        const hasFilters = filters > 0;
+	initFilters() {
+		//defaults
+		this.filters = {
+			content: 	this.contentTypes[0],
+			orderby: 	'date',
+			order: 		'desc',
+			page: 		1
+		};
+		if (this.config.context) this.filters.context = this.config.context;
+		if (this.config.source) this.filters.source = this.config.source;
 
-        const hasMultiple = filters > 1;
+		//check the cache
+		this.processCachedFilters();
+		//check url
+		this.processURLFilters();
 
-        this.elements.clearFilters.hidden = !hasFilters;
+		// Set initial UI state
+		this.syncUIToFilters();
+	}
+	syncUIToFilters() {
+		if (this.ui.filterContainer) {
+			// Check radio buttons
+			Object.entries(this.filters).forEach(([key, value]) => {
+				const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
+				if (input) {
+					input.checked = true;
+				}
+			});
+		}
 
-        this.elements.filters.classList.toggle('has-filters', hasFilters);
+		// Update content-specific visibility
+		this.updateContentFor(this.filters.content);
+	}
+	nextPage() {
+		this.store.setFilter('page', this.store.filters.page++);
+	}
 
-        this.elements.matchAll.hidden = !hasMultiple;
-    }
+	initStore() {
+		const store = 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: 'modified'},
+					{ name: 'title', keyPath: 'title'}
+				],
+				filters: this.filters,
+				TTL: 6 * 60 * 60 * 1000,
+				showLoading: true,
+				required: 'content',
+				delayFetch: true
+			}
+		);
+		this.store = store.feed;
 
+		this.store.subscribe((event, data) => {
+			switch (event) {
+				case 'data-loaded':
+					this.renderItems();
+					this.ui.loadMore.hidden = true;
+					if (this.store.lastResponse && this.store.lastResponse['has_more']) {
+						this.ui.loadMore.hidden = !this.store.lastResponse['has_more'];
+					}
+					break;
+			}
+		});
+	}
 
-    /**
-     * Create a filter tag element
-     */
-    createFilterTag(taxonomy, id, name) {
-        const tag = window.getTemplate('selectedTerm');
+	initGallery() {
+		this.gallery = (this.config.gallery) ? window.jvbGallery : false;
+		if (this.gallery) {
+			this.gallery.subscribe((event, data) => {
+				if (event === 'load-more' && this.store.lastResponse) {
+					if (this.store.lastResponse['has_more']) {
+						this.nextPage();
+					}
+				}
+			});
+		}
+	}
 
-        tag.dataset.taxonomy = taxonomy;
-        tag.dataset.id = id;
-        let icon = window.getIcon(taxonomy);
-        let span = tag.querySelector('span');
-        let button = tag.querySelector('button');
-        tag.prepend(icon);
-        span.classList.add('filter-name');
-        span.classList.remove('item-name');
-        [span.textContent, button.remove] =
-            [escapeHtml(name), `Remove ${escapeHtml(name)}`];
+	processCachedFilters() {
+		Object.keys(this.filters).forEach(filter => {
+			let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
+			if (cached && cached !== this.filters[filter]){
+				this.filters[filter] = cached;
+			}
+		});
+	}
 
+	processURLFilters() {
+		if (this.filters.page > 1) {
+			return false;
+		}
+		const params = new URLSearchParams(window.location.search);
 
+		if (!params.toString()) {
+			return false;
+		}
+		let filters = ['content', 'order', 'orderby', 'favourites', 'match'];
+		filters.forEach(filter => {
+			let value = params.get(`f_${filter}`);
+			if (value) {
+				this.filters[filter] = value;
+				let input = this.ui.filters[filter];
+				if (input) {
+					input.checked = true;
+				}
+			}
+		});
 
-        return tag;
-    }
+		let hasTaxonomy = false;
+		// Load taxonomy filters from URL
+		params.forEach((value, key) => {
+			if (key.startsWith('f_tax_')) {
+				hasTaxonomy = true;
+				const taxonomy = key.replace('f_tax_', '');
+				if (!this.taxonomyFilters[taxonomy]) {
+					this.taxonomyFilters[taxonomy] = [];
+				}
+				this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
+			}
+		});
+		if (this.ui.filterContainer && hasTaxonomy) {
+			for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
+				let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
+				if (button) {
+					if (button.dataset.fieldId) {
+						let field = this.selector.get(button.dataset.fieldId);
+						field.selectedTerms = new Set(ids);
+						this.selector.initFieldDisplay(button.dataset.fieldId);
+					} else {
+						this.selector.registerField(button, {
+							button: button,
+							buttonSelector: '[data-filter="taxonomy"]',
+							selected: this.ui.selectedTax,
+							selectedItems: ids
+						});
+					}
+				}
+			}
+		}
+		return true;
+	}
 
-    /**
-     * Gallery
-     **/
-    openGallery(index){
-        this.gallery.index = index;
-        this.gallery.modal.showModal();
-        this.hideBody();
+	/**
+	 * Update URL with current filters (for sharing/bookmarking)
+	 */
+	updateURL() {
+		const params = new URLSearchParams();
 
-        this.bindGalleryEvents();
-        //show current image
-        this.updateDisplay(index);
-        //preload adjacent images
-        this.preloadImages();
+		// Add simple filters
+		['content', 'order', 'orderby', 'match'].forEach(key => {
+			if (this.filters[key]) {
+				params.set(`f_${key}`, this.filters[key]);
+			}
+		});
 
-        // Announce initial state
-        this.a11y.announce(`Image ${this.gallery.index + 1} of ${this.gallery.items.length}. Use arrow keys to navigate.`)
-        this.a11y.trapFocus(this.gallery.modal);
+		// Add taxonomy filters
+		Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => {
+			if (terms.length > 0) {
+				params.set(`f_tax_${taxonomy}`, terms.join(','));
+			}
+		});
 
-    }
+		// Update URL without reload
+		const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+		window.history.pushState({ filters: this.filters }, '', newURL);
+	}
 
-    /**
-     * Create the modal element
-     */
-    createGalleryModal() {
-        const modal = document.createElement('dialog');
-        modal.className = 'gallery-modal';
-        modal.setAttribute('aria-modal', 'true');
-        modal.setAttribute('aria-label', 'Image Gallery');
+	renderItems() {
+		let items = this.store.getFiltered();
+		if (this.store.filters['page'] === 1) {
+			window.removeChildren(this.ui.grid);
+		}
 
-        modal.innerHTML = `
-        <button class="gallery-close" aria-label="Close gallery">
-          ${jvbSettings.icons.close}
-        </button>
+		if (items.length === 0) {
+			this.a11y.announceItems(0, this.store.filters['page'] > 0);
+			return;
+		}
 
-        <button class="gallery-nav gallery-prev" aria-label="Previous image">
-          ${jvbSettings.icons.prev}
-        </button>
+		const fragment = document.createDocumentFragment();
+		const batchSize = 10;
 
-        <button class="gallery-nav gallery-next" aria-label="Next image">
-             ${jvbSettings.icons.next}
-        </button>
+		const processBatch = (startIndex) => {
+			const endIndex = Math.min(startIndex + batchSize, items.length);
 
-        <div class="gallery-content">
-          <img src="" alt="" class="gallery-image">
-          <details>
-            <summary>DETAILS</summary>
-            <div class="item-info"></div>
-          </details>
-        </div>
+			for (let i = startIndex; i < endIndex; i++) {
+				const item = items[i];
+				const element = this.createItemElement(item);
+
+				fragment.appendChild(element);
+			}
 
-        <div class="gallery-favourite"></div>
-        <div class="gallery-counter"><span id="gallery-index">1</span> / <span class="total"></span></div>
-    `;
+			if (endIndex < items.length) {
+				requestAnimationFrame(() => processBatch(endIndex));
+			} else {
+				this.removePlaceholders();
+				this.ui.grid.append(fragment);
 
-        return modal;
-    }
+				if (this.config.gallery) {
+					this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
+				}
 
+				this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+				this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore);
+			}
+		};
 
-    /**
-     * Bind event handlers
-     */
-    bindGalleryEvents() {
-        // Close button
-        this.gallery.modal.querySelector('.gallery-close').addEventListener('click', () => this.closeGallery());
+		if (items.length > 0) {
+			processBatch(0);
+		} else {
+			this.a11y.announceItems(0, this.store.filters['page'] >1, false);
+		}
 
-        // Navigation buttons
-        const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
-        const nextBtn = this.gallery.modal.querySelector('.gallery-next');
+		if (this.ui.filters.match) {
+			this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
+		}
+		if (this.ui.clearFilter) {
+			this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
+		}
+	}
 
-        prevBtn.addEventListener('click', () => this.navigate(-1));
-        nextBtn.addEventListener('click', () => this.navigate(1));
+	/**
+	 *
+	 * @param {object} item
+	 */
+	createItemElement(item) {
+		let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
 
-        // Keyboard navigation
-        this.gallery.keyHandler = (e) => {
-            switch (e.key) {
-                case 'ArrowLeft':
-                    this.navigate(-1);
-                    break;
-                case 'ArrowRight':
-                    this.navigate(1);
-                    break;
-                case 'Escape':
-                    this.closeGallery();
-                    break;
-            }
-        };
-        document.addEventListener('keydown', this.gallery.keyHandler);
+		const isTimeline = Object.hasOwn(template.dataset, 'timeline');
 
-        // Touch events
-        this.gallery.modal.addEventListener('touchstart', (e) => {
-            this.gallery.touchStart = e.touches[0].clientX;
-        });
+		// Format fields using helpers
+		for (let [fieldName, value] of Object.entries(item.fields)) {
+			if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue;
+			let el = template.querySelector(`[data-field="${fieldName}"]`);
+			if (!el) continue;
 
-        this.gallery.modal.addEventListener('touchmove', (e) => {
-            this.gallery.touchEnd = e.touches[0].clientX;
-        });
+			if (value === '') {
+				el.remove();
+				continue;
+			}
 
-        this.gallery.modal.addEventListener('touchend', () => {
-            if (!this.gallery.touchStart || !this.gallery.touchEnd) return;
+			if (this.isImageField(item, value)) {
+				this.formatImageFields(el, value, item);
+			} else if (this.isTaxonomyField(item, fieldName)) {
+				this.formatTaxonomyField(el, item, fieldName, value);
+			} else if (this.isTimeField(el)) {
+				this.formatTimeField(el, value);
+			} else {
+				this.formatField(el, value);
+			}
+		}
 
-            const distance = this.gallery.touchStart - this.gallery.touchEnd;
-            const isLeftSwipe = distance > this.gallery.minSwipe;
-            const isRightSwipe = distance < -this.gallery.minSwipe;
+		// Handle link
+		let link = template.querySelector('a');
+		if (link && item.url !== '') {
+			[
+				link.href,
+				link.title
+			] = [
+				item.url,
+				`View ${item.fields['post_title']??'Item'}`
+			];
+		}
 
-            if (isLeftSwipe) {
-                this.navigate(1);
-            } else if (isRightSwipe) {
-                this.navigate(-1);
-            }
+		if (isTimeline) {
+			this.addTimelineElements(item, template);
+		}
 
-            this.gallery.touchStart = null;
-            this.gallery.touchEnd = null;
-        });
-    }
+		return template;
+	}
+	splitIDs(value) {
+		return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
+	}
+	isImageField(item, value) {
+		if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) {
+			return false;
+		}
+		let values = this.splitIDs(value);
 
-    /**
-     * Navigate to previous/next image
-     */
-    async navigate(direction) {
-        const newIndex = this.gallery.index + direction;
+		return values.some(v =>
+			Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v))
+		);
+	}
+	formatImageFields(element, value, item) {
+		let values = this.splitIDs(value); // Convert string to array first
+		if (values.length === 0) return;
 
-        // Check if out of bounds
-        if (newIndex < 0 || newIndex >= this.gallery.items.length) {
-            this.a11y.announceNavigation(newIndex, this.gallery.items.length,direction < 0, direction > 0)
-            return;
-        }
+		if (values.length > 1) {
+			let image = element.querySelector('img');
+			if (!image) return;
+			values.forEach(imgID => {
+				let img = image.cloneNode(true);
+				this.formatImageField(img, imgID, item);
+				element.append(img);
+			});
+			image.remove();
+		} else {
+			if (element.tagName !== 'IMG') {
+				element = element.querySelector('img');
+				if (!element) return;
+			}
+			this.formatImageField(element, values[0], item);
+		}
+	}
+	formatImageField(element, value, item) {
+		let imgData = item.images[value]??false;
+		if (!imgData) return;
+		[
+			element.src,
+			element.srcset,
+			element.alt
+		] = [
+			imgData.tiny,
+			`${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
+			imgData['image-alt-text']
+		]
+	}
+	isTaxonomyField(item, field) {
+		if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) {
+			return false;
+		}
 
-        // Update current index
-        this.gallery.index = newIndex;
+		return Object.keys(item.taxonomies).includes(field);
+	}
+	formatTaxonomyField(element, item, field, value) {
+		if (element.tagName !== 'UL' || !element.querySelector('li')) return;
+		let values = this.splitIDs(value);
+		if (values.length === 0) {
+			element.remove();
+		}
+		let listItem = element.querySelector('li');
+		for (let termID of values) {
+			let term = item.taxonomies[field][termID]??false;
+			if (!term) continue;
+			let termItem = listItem.cloneNode(true);
+			let link = termItem.querySelector('a');
+			if (!link) continue;
 
-        // Update display
-        this.updateDisplay(newIndex);
+			[
+				link.href,
+				link.title,
+				link.textContent
+			] = [
+				term.url,
+				`See more ${term.title}`,
+				term.title
+			];
+			element.append(termItem);
+		}
+		listItem.remove();
+	}
+	isTimeField(el) {
+		return el.tagName === 'TIME' || el.querySelector('time') !== null;
+	}
+	formatTimeField(element, value) {
+		if (element.tagName !== 'TIME') {
+			element = element.querySelector('time');
+			if (!element) return;
+		}
+		element.setAttribute('datetime', value);
+		element.textContent = window.formatTimeAgo(value, 'F Y');
+	}
+	formatField(element, value) {
+		element.textContent = value;
+	}
 
-        // Preload adjacent images
-        this.preloadImages();
+	addTimelineElements(item, template) {
+		let [
+			afterEl,
+			number,
+			started,
+			last
+		] = [
+			template.querySelector('span.after-text'),
+			template.querySelector('[data-field="number"] b'),
+			template.querySelector('[data-field="started"] time'),
+			template.querySelector('[data-field="updated"] time')
+		];
 
-        // Announce to screen readers
-        this.a11y.announceNavigation(this.gallery.index,this.gallery.items.length);
+		if (afterEl) {
+			afterEl.textContent = `After ${item.fields.number} Tx`;
+		}
+		if (number) {
+			number.textContent = item.fields.number;
+		}
+		if (started) {
+			this.formatTimeField(started, item.fields.timeline[0]['post_date']);
+		}
+		if (last) {
+			this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
+		}
+	}
 
-        //Load more if we're near the end
-        if (direction > 0 && newIndex >= (this.gallery.items.length - 3) && this.state.hasMore) {
-            await this.fetchFeed();
-            this.updateGalleryItems(this.getGalleryItems());
-        }
-    }
+	removePlaceholders() {
+		const placeholders = this.ui.grid.querySelectorAll('.placeholder');
+		if (placeholders.length > 0) {
+			placeholders.forEach(p => p.remove());
+		}
+	}
 
-    /**
-     * Preload adjacent images
-     */
-    preloadImages() {
-        // Preload current, previous and next images
-        [-1, 0, 1].forEach(offset => {
-            const index = this.gallery.index + offset;
-            if (index >= 0 && index < this.gallery.items.length) {
-                const img = new Image();
-                const item = this.gallery.items[index];
 
-                if (window.innerWidth < 1000) {
-                    img.src = item.large || item.src;
-                } else {
-                    img.src = item.full || item.src;
-                }
-            }
-        });
-    }
+	addPlaceholders() {
+		let total = this.contentTypes.length;
+		const fragment = document.createDocumentFragment();
+		for (let i = 0; i < 12; i++) {
+			let template = window.getTemplate('placeholderTemplate');
 
-    /**
-     * Update display with current image
-     */
-    updateDisplay(index) {
-        const item = this.gallery.items[index];
-        if (!item) return;
+			let rand = Math.floor(Math.random() * total);
+			let icon;
+			if (this.ui.content && this.ui.content.length > 0) {
+				icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
+			} else {
+				icon = window.getIcon(this.container.dataset.icon);
+			}
+			template.append(icon);
+			fragment.append(template);
+		}
+		this.ui.grid.append(fragment);
+	}
 
-        // Get elements
-        const favourite = this.gallery.modal.querySelector('.gallery-favourite');
-        const image = this.gallery.modal.querySelector('.gallery-image');
-        const counter = this.gallery.modal.querySelector('.gallery-counter');
-        const info = this.gallery.modal.querySelector('.item-info');
 
-        // Update image
-        image.src = window.innerWidth < 1000 ?
-            (item.large || item.src) :
-            (item.full || item.src);
 
-        image.alt = item.alt || '';
+	/**
+	 *
+	 * @param {object} filters {name: value}
+	 */
+	updateFilter(filters) {
+		//double check filters are what we're expecting
+		let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)];
 
-        // Update favourite button
-        if (favourite && item.fav) {
-            window.removeChildren(favourite);
-            favourite.appendChild(item.fav.cloneNode(true));
-        }
+		filters = Object.keys(filters)
+			.filter(key => allowed.includes(key))
+			.reduce((obj, key) => {
+				obj[key] = filters[key];
+				return obj;
+			}, {});
 
-        // Update info
-        if (info && item.info) {
-            window.removeChildren(info);
-            const clone = item.info.cloneNode(true);
-            info.appendChild(clone);
-        }
+		if (window.getDifferences.map(this.filters, filters)) {
+			this.filters = { ...this.filters, ...filters };  // Merge instead of replace
+			this.updateURL();
+			this.store.setFilters(filters);
+		}
+	}
+	/**
+	 * Update visible filters based on selected content type
+	 */
+	updateContentFor(contentType) {
+		// Update taxonomy filter visibility
+		const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]');
+		taxonomyButtons.forEach(button => {
+			const forTypes = button.dataset.for?.split(',') || [];
+			button.hidden = forTypes.length > 0 && !forTypes.includes(contentType);
+		});
 
-        // Update counter
-        counter.textContent = `${this.gallery.index + 1} / ${this.gallery.items.length}`;
+		// Update ordering options
+		const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]');
+		orderButtons.forEach(button => {
+			const forTypes = button.dataset.for?.split(',') || [];
+			if (forTypes.length > 0) {
+				button.hidden = !forTypes.includes(contentType);
+				// Uncheck if hiding
+				if (button.hidden && button.checked) {
+					button.checked = false;
+				}
+			}
+		});
 
-        // Update navigation buttons
-        this.updateNavigationButtons();
-    }
+		// Update order direction visibility based on selected orderby
+		const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked');
+		this.updateOrderDirectionVisibility(orderBy?.value);
+	}
 
-    /**
-     * Update navigation button visibility
-     */
-    updateNavigationButtons() {
-        const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
-        const nextBtn = this.gallery.modal.querySelector('.gallery-next');
+	/**
+	 * Show/hide order direction based on orderby selection
+	 */
+	updateOrderDirectionVisibility(orderBy) {
+		const orderDirection = this.ui.filterContainer.querySelector('.order-direction');
+		if (orderDirection) {
+			const forOrders = orderDirection.dataset.forOrder?.split(',') || [];
+			orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy);
+		}
+	}
+	/*********************************************************************
+	LISTENERS
+	 *********************************************************************/
+	initListeners() {
+		this.popStateHandler 	= this.handlePopState.bind(this);
+		this.clickHandler		= this.handleClick.bind(this);
+		this.changeHandler		= this.handleChange.bind(this);
+		this.imageObserver = null;
+		this.resizeObserver = null;
+		if ('IntersectionObserver' in window) {
+			this.imageObserver = new IntersectionObserver(entries => {
+				entries.forEach(entry => {
+					this.loadImage(entry.target);
+					this.imageObserver.unobserve(entry.target);
+				});
+			}, {
+				rootMargin: '100px',
+				threshold: .1
+			});
+		}
 
-        prevBtn.classList.toggle('end', this.gallery.index > 0 ? '' : 'none');
-        nextBtn.classList.toggle('end', this.gallery.index < this.gallery.items.length - 1 ? '' : 'none');
-    }
+		if ('ResizeObserver' in window) {
+			this.resizeObserver = new ResizeObserver(() => {
+				window.debouncer.schedule(
+					'feed-update-images',
+					() => this.updateImageSizes(),
+					250
+				);
+			});
+		} else {
+			window.addEventListener('resize', () => {
+				window.debouncer.schedule(
+					'feed-update-images',
+					() => this.updateImageSizes(),
+					250
+				);
+			});
+		}
 
-    /**
-     * Close the gallery
-     */
-    closeGallery() {
-        this.showBody();
-        // Remove event listeners
-        document.removeEventListener('keydown', this.gallery.keyHandler);
-        this.a11y.announce('Gallery closed.');
+		window.addEventListener('popstate', this.popStateHandler);
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+	}
 
-        this.gallery.modal.close();
+	handlePopState(e) {
+		if (e.state?.filters) {
+			if (this.processURLFilters()) {
+				this.store.setFilters(this.filters);
+				this.a11y.announce('Feed filters updated from browser history');
+			}
+		}
+	}
 
-        // Reset state
-        this.gallery.keyHandler = null;
-    }
+	handleClick(e) {
+		if (window.targetCheck(e, this.elements.loadMore)) {
+			this.nextPage();
+		} else if (window.targetCheck(e, this.elements.clearFilter)) {
+			this.clearAllTaxonomies();
+		} else if (window.targetCheck(e, '.remove-item')) {
+			this.handleRemoveSelectedTerm(e);
+		}
+	}
 
-    /**
-     * Update gallery items
-     * @param {Array} newItems - New gallery items
-     */
-    updateGalleryItems(newItems) {
-        // Store original current index and item
-        const currentItem = this.gallery.items[this.gallery.index];
+	handleRemoveSelectedTerm(e) {
+		const selectedItem = e.target.closest('.selected-item');
+		if (!selectedItem) return;
 
-        // Update items array
-        this.gallery.items = newItems;
+		const termId = parseInt(selectedItem.dataset.id);
+		const taxonomy = selectedItem.dataset.taxonomy;
 
-        // Try to keep the same item selected
-        if (currentItem) {
-            // Find the same item in the new array by matching source
-            const newIndex = this.gallery.items.findIndex(item =>
-                item.full === currentItem.full ||
-                item.large === currentItem.large
-            );
+		// Remove from filters
+		if (this.taxonomyFilters[taxonomy]) {
+			this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy]
+				.filter(id => id !== termId);
 
-            if (newIndex !== -1) {
-                this.gallery.index = newIndex;
-            }
-        }
+			if (this.taxonomyFilters[taxonomy].length === 0) {
+				delete this.taxonomyFilters[taxonomy];
+			}
+		}
 
-        // Update navigation buttons
-        this.updateNavigationButtons();
-    }
+		// Remove from UI
+		selectedItem.remove();
 
-    /**
-     * Ensure gallery is accessible
-     */
-    setupGalleryAccessibility() {
-        // Add ARIA attributes
-        this.gallery.modal.setAttribute('aria-modal', 'true');
-        this.gallery.modal.setAttribute('aria-label', 'Image Gallery');
-    }
+		// Update filters
+		this.updateFilter({
+			taxonomy: Object.keys(this.taxonomyFilters).length > 0
+				? this.taxonomyFilters
+				: null,
+			page: 1
+		});
+	}
 
-    hideBody(){
-        document.body.style.overflow = 'hidden';
-    }
-    showBody(){
-        document.body.style.overflow = '';
-    }
+	handleChange(e) {
+		let target = e.target;
+		if (Object.hasOwn(target.dataset, 'filter')) {
+			if (target.dataset.filter === 'content') {
+				this.updateContentFor(target.value);
+				this.updateFilter({ content: target.value, page: 1 });
+			} else if (target.dataset.filter === 'orderby') {
+				this.updateOrderDirectionVisibility(target.value);
+				this.updateFilter({ orderby: target.value, page: 1 });
+			} else if (target.dataset.filter === 'order') {
+				this.updateFilter({ order: target.value, page: 1 });
+			} else if (target.dataset.filter === 'match') {
+				this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 });
+			} else if (target.dataset.filter === 'favourites') {
+				this.updateFilter({ favourites: target.checked, page: 1 });
+			}
+		}
+	}
 }
 
-// Initialize feed blocks when DOM is loaded
-document.addEventListener('DOMContentLoaded', () => {
-    document.querySelectorAll('.feed-block').forEach(container => {
-        // Initialize with both the container and overlay
-        window.feedBlock = new FeedBlock(container);
-    });
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe(event => {
+		if (event === 'auth-loaded') {
+			window.feedBlock = new FeedBlock();
+		}
+	});
 });
-
-
-function isEmptyObject(obj) {
-    return Object.keys(obj).length === 0;
-}

--
Gitblit v1.10.0