From 07282da9671de8fb2601e9e641decb2655439ad8 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 01 Jan 2026 23:20:54 +0000
Subject: [PATCH] =FeedRoutes.php: fixed the extractTaxonomies method

---
 src/feed/viewOld.js | 1996 +++++++++++++++------------------------------------------
 1 files changed, 542 insertions(+), 1,454 deletions(-)

diff --git a/src/feed/viewOld.js b/src/feed/viewOld.js
index 347e997..29859f9 100644
--- a/src/feed/viewOld.js
+++ b/src/feed/viewOld.js
@@ -1,968 +1,529 @@
 class FeedBlock {
-    static LOADING_QUIPS = JSON.parse(feedSettings.quips);
-    constructor(container){
+	constructor() {
+		this.cache = window.jvbCache;
+		this.a11y = window.jvbA11y;
+		this.loading = window.jvbLoading;
+		this.error = window.jvbError;
 
-        this.cache = window.jvbCache;
-        this.eventHandlers = new Map();
 
-        this.rendered = {};
+		this.container = document.querySelector('section.feed-block');
+		if (!this.container) {
+			return;
+		}
 
-        // Store container references
-        this.container = container;
-        this.a11y = window.jvbA11y;
+		this.openGallery = false;
 
+		this.initElements();
+		this.addPlaceholders();
+		this.config = {
+			api: feedSettings.apiUrl,
+			nonce: feedSettings.nonce,
+			user: jvbSettings.currentUser || null,
+			source: '',
+			context: '',
+			highlight: null,
+			gallery: false,
+			showAuthor: true,
+			showDate: false,
+			view: localStorage.getItem('feedViewMode') || 'grid',
+			... this.container.dataset
+		};
+		this.taxonomies = {};
+		this.rendered = {};
 
-        //For tracking cache and current requests
-        this.currentRequest = null;
-        this.timeoutId = null;
+		this.feed = {
+			imageLoadThreshold: 5,
+			lazyLoadOffset: '100px',
+			gallery: [],
+			loaded: 0,
+			intsersectionObserver: null,
+			templates: new Map()
+		};
 
-        // Initialize Config
-        this.initConfig();
+		this.isLoading = false;
+		this.hasMore = true;
+		this.retries = {
+			count: 0,
+			max: 3,
+			delay: 1000
+		};
+		this.page = 1;
+		this.order = 'DESC';
+		this.orderby = 'date';
 
+		this.gallery = (this.config.gallery) ? new window.jvbGallery(document.querySelector('dialog.gallery'), {
+			imageWrapper: '.item',
+			loadMore: ()=>this.fetchFeed.bind(this)
+		}) : false;
+		this.initListeners();
+		if (this.page === 1) {
+			this.processURLFilters();
+		} else {
+			this.updateFilters();
+		}
 
-        // 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);
-				}
+	}
+	initElements() {
+		this.filterSelector = 'form.feed-filters';
+		this.filterForm = this.container.querySelector(this.filterSelector);
+		this.grid = this.container.querySelector('.item-grid');
+		this.loadMore = this.container.querySelector('.load-more');
+		this.filterControls = this.container.querySelector('.filter-actions');
+		this.contentTypes = Array.from(this.filterForm.querySelectorAll('input[name="content"]')).map(
+			content => {
+				return content.value;
 			});
-        }
-        return this.config.highlight;
-    }
+		this.selectedTerms = this.container.querySelector('.selected-items-section .selected-items');
+	}
 
-    /**
-     * 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);
-                }
-
-            }
+	initListeners() {
+		window.addEventListener('popstate', this.handlePopState.bind(this));
+		document.addEventListener('click', this.handleClick.bind(this));
+		document.addEventListener('change', this.handleChange.bind(this));
 
 
-            this.a11y.announce(`Checking for more ${tax} ${content}...`);
-        } else {
-            this.hideLoading();
-        }
+		// 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: '100px',
+				threshold: 0.1
+			});
+		}
+		// Resize observer for responsive images
+		if ('ResizeObserver' in window) {
+			this.resizeObserver = new ResizeObserver(window.debounce(() => {
+				this.updateImageSizes();
+			}, 250));
 
-        // Update loading spinner
-        this.elements.spinner.hidden = !loading;
+			// Observe the container
+			this.resizeObserver.observe(this.container);
+		} else {
+			// Fallback to window resize
+			window.addEventListener('resize', window.debounce(() => {
+				this.updateImageSizes();
+			}, 250));
+		}
 
-        // Update load more button
-        this.elements.loadMore.disabled = loading;
-
-    }
+		this.taxonomies = {};
+		this.container.querySelectorAll('.jvb-selector:not([hidden])').forEach(selector => {
+			let taxonomy = selector.dataset.taxonomy;
+			if (!Object.hasOwn(this.taxonomies, taxonomy)) {
+				this.taxonomies[taxonomy] = new window.jvbTaxonomySelector(
+					selector,
+					{
+						multiple: true,
+						feed: true,
+						selected: {},
+						onClose: () => this.setSelectedTerms(taxonomy),
+					}
+				);
+			}
+		});
+	}
 
 
-    /**
-     * Show the loading overlay
-     */
-    showLoading() {
-        this.hideBody();
-        this.elements.loading.classList.add('active');
-        this.startQuipCycle();
-        document.body.classList.add('loading');
-    }
+	/**
+	 * Handle browser history navigation
+	 */
+	handlePopState(e) {
+		if (e.state && e.state.filters) {
+			if(this.processURLFilters()){
+				// Load items with updated filters
+				this.resetPage();
+				this.fetchFeed();
 
-    /**
-     * Hide the loading overlay
-     */
-    hideLoading() {
-        this.showBody();
-        this.container.classList.remove('active');
-        this.stopQuipCycle();
-        document.body.classList.remove('loading');
-    }
+				// Announce to screen readers
+				this.a11y.announce('Feed filters updated from browser history.');
+			}
+		}
+	}
+
+	processURLFilters() {
+		const params = new URLSearchParams(window.location.search);
+		//No parameters to process
+		if (!params.toString()) {
+			this.updateFilters();
+			return;
+		}
+
+		let filters = ['content', 'order', 'orderby', 'favourites','match'];
+
+		filters.forEach(filter => {
+			let value = params.get('f_'+filter);
+			params.delete('f_'+filter);
+			if (value && this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`)) {
+				this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`).checked = true;
+			}
+		});
+
+		let unprocessed = {};
+		for (var [key, value] of Object.entries(Object.fromEntries(params))) {
+
+			key = key.replace('f_','');
+			if (this.contentTypes.includes(key)) {
+				this.openGallery = value;
+			} else {
+				this.taxonomies[key].addTermsFromURL(value);
+				this.setSelectedTerms(key);
+			}
+		}
+
+		this.updateFilters();
+	}
+
+	handleClick(e) {
+		if (e.target.classList.contains('load-more') || e.target.closest('.load-more')) {
+			this.fetchFeed(false);
+			e.target.disabled = true;
+		} else if (e.target.classList.contains('clear-filters') || e.target.closest('.clear-filters')) {
+			this.resetFilters();
+		} else if (this.config.gallery && e.target.closest('.feed-image')) {
+			this.gallery.handleGalleryOpen(e);
+		} else if (e.target.classList.contains('.remove-item') || e.target.closest('.remove-item')) {
+			let tag = e.target.closest('.selected-item');
+			let taxonomy = tag.dataset.taxonomy;
+			this.taxonomies[taxonomy].removeSelectedTerm(tag.dataset.id);
+			this.setSelectedTerms(taxonomy);
+			this.updateFilters();
+		}
+	}
+	handleChange(e) {
+		if (e.target.closest(this.filterSelector)) {
+			this.resetPage();
+			window.removeChildren(this.grid);
+			this.addPlaceholders();
+			//update filters
+			this.updateFilters();
+		}
+	}
+
+	updateFilters() {
+		this.page = 1;
+		const params = new URLSearchParams(window.location.search);
+
+		let filters = Object.fromEntries(new FormData(this.filterForm));
+
+		let contents = [];
+		for (let [key, value] of Object.entries(filters)) {
+			let set = false;
+			switch (key) {
+				case 'content':
+					if (value !== this.contentTypes[0]) {
+						set = true;
+					} else {
+						params.delete('f_'+key);
+					}
+					break;
+				case 'orderby':
+					if (value !== 'date') {
+						set = true;
+					}
+					break;
+				case 'order':
+					if (value !== 'desc') {
+						set = true;
+					}
+					break;
+				default:
+					set = true;
+			}
+			if (!set) {
+				params.delete('f_'+key);
+			}
 
 
-    /**
-     * Start cycling through loading messages
-     */
-    startQuipCycle() {
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-        }
+			if (set && value !== false && value !== '') {
+				params.set('f_'+key, value);
+			}
+			if (value !== '') {
+				contents.push(value);
+			}
 
-        if (!this.quips.length) return;
+			const newURL = `${window.location.pathname}?${params.toString()}`;
+			history.pushState(filters, '', newURL);
 
-        // Set initial message
-        this.updateMessage(this.quips[0]);
-        this.container.classList.remove('changing');
+		}
 
-        this.quipInterval = setInterval(() => {
-            this.container.classList.add('changing');
+		this.filters = filters;
+		this.updateContentFor(filters.content);
 
-            setTimeout(() => {
-                this.loadingIndex = (this.loadingIndex + 1) % this.quips.length;
-                this.updateMessage(this.quips[this.loadingIndex]);
+		this.updateFilterControls();
 
-                setTimeout(() => {
-                    this.container.classList.remove('changing');
-                }, 50);
-            }, 350);
-        }, this.loadingOptions.cycleInterval);
-    }
+		this.loading.setContent(contents);
+		this.fetchFeed(true);
+	}
 
-    /**
-     * Stop cycling through loading messages
-     */
-    stopQuipCycle() {
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-            this.quipInterval = null;
-        }
-    }
+	updateFilterControls() {
+		this.filterControls.hidden = this.selectedTerms.children.length < 2;
+	}
 
-    /**
-     * Update the loading message
-     */
-    updateMessage(quipData) {
-        if (!this.loadingMessage) return;
+	/**
+	 * Toggles taxonomy selectors and certain order/orderby options
+	 *  depending on current content
+	 * @param content
+	 */
+	updateContentFor(content) {
+		this.filterForm.querySelectorAll('.jvb-selector').forEach(tax => {
+			let hasContent = tax.dataset.for.includes(content);
+			tax.hidden = !hasContent;
+			if (!hasContent) {
+				let t = tax.dataset.taxonomy;
+				this.clearSelectedTerms(t);
+			}
+		});
+		this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => {
+			toggle.hidden = !toggle.dataset.for.includes(content);
+		});
+		this.filterForm.querySelectorAll('input[name="order"]').forEach(order => {
+			order.hidden = this.filters.order === 'random';
+		});
+	}
 
-        const icon = feedSettings?.icons?.[quipData.icon] || '';
-        this.loadingMessage.innerHTML = `${icon}<p>${quipData.quip}</p>`;
-    }
+	clearSelectedTerms(taxonomy) {
+		this.filterForm.querySelector(`input[name="${taxonomy}"]`).value = '';
+		if (Object.hasOwn(this.taxonomies, taxonomy)) {
+			this.taxonomies[taxonomy].selectedItems = {};
+		}
+	}
 
-    /**
-     * 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
-                        });
-                    });
-                }
-            });
-        }
+	setSelectedTerms(taxonomy) {
+		let input = this.filterForm.querySelector(`input[name="${taxonomy}"]`);
+		input.value = '';
+		let selected = this.taxonomies[taxonomy].selectedTerms;
+		if (!window.isEmptyObject(selected)) {
+			let ids = Object.keys(selected);
+			input.value = ids.join(',');
+		}
+		this.updateFilters();
+	}
 
 
-        // Shuffle the quips array
-        return this.shuffleArray(allQuips);
-    }
+	nextPage() {
+		if (this.hasMore) {
+			this.page++;
+		}
+	}
+	resetPage() {
+		this.page = 1;
+		this.hasMore = true;
+	}
+	resetState() {
+		this.resetPage(true);
+		this.isLoading = false;
+		this.retries = {
+			count: 0,
+			max: 3,
+			delay: 1000
+		};
+	}
 
-    /**
-     * Feed Grid
-     */
+	resetFilters() {
+		this.filterForm.reset();
+		//check the first content
+		this.filterForm.querySelector('input[name="content"]').checked = true;
+		this.filterForm.querySelector('input[name="orderby"][value="date"]').checked = true;
+		this.page = 1;
+		this.updateFilters();
+	}
 
-    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);
-        }
+	buildFilterRequest() {
 
-        // Bail early if no items
-        if (items.length === 0) {
-            this.a11y.announceUpdate(0, append);
-            return;
-        }
+		let filters = {};
 
-        // Use DocumentFragment for better performance
-        const fragment = document.createDocumentFragment();
+		for (let [filter, value] of Object.entries(this.filters)) {
+			if (value !== false && value !== '') {
+				filters[filter] = value;
+			}
+		}
+		filters.page = parseInt(this.page);
+		if (this.container.dataset.context) {
+			filters.context = this.container.dataset.context;
+		}
+		if (this.container.dataset.source) {
+			filters.source = this.container.dataset.source;
+		}
+		return new URLSearchParams(filters).toString();
+	}
 
-        // Process items in batches for better performance
-        const batchSize = 10;
-        const processBatch = (startIndex) => {
-            const endIndex = Math.min(startIndex + batchSize, items.length);
+	async fetchFeed(reset = false, force = false) {
+		if (this.isLoading) {
+			return false;
+		}
+		this.loading.showLoading(this.filters);
+		try {
+			if (this.page === 1) {
+				window.removeChildren(this.grid);
+				this.addPlaceholders();
+			}
 
-            // Process this batch
-            for (let i = startIndex; i < endIndex; i++) {
-                const item = items[i];
-                const element = this.createItemElement(item);
-                fragment.appendChild(element);
+			const data = await this.cache.fetchWithCache(
+				`${this.config.api}feed?${this.buildFilterRequest()}`,
+				{
+					method: 'GET',
+				},
+				{
+					context: 'feed',
+					forceRefresh: true
+					// forceRefresh: force
+				}
+			);
 
-                // Lazy load images beyond threshold
-                if (this.feed.loaded >= this.feed.imageLoadThreshold && this.imageObserver) {
-                    this.imageObserver.observe(element);
-                } else {
-                    this.loadImage(element);
-                }
+			//Handle empty results
+			if (!data || !data.items || data.items.length === 0) {
+				if (this.page === 1) {
+					this.showEmptyState();
+				}
+				this.hasMore = false;
+				return false;
+			} else {
+				this.hasMore = data['has_more'];
 
-                this.feed.loaded++;
-            }
+				this.renderItems(data.items, this.page > 1);
 
-            // 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);
-            }
-        };
+				if (this.hasMore) {
+					this.nextPage();
+				}
+				return true;
+			}
+		} catch (error) {
+			this.handleError(error);
+		} finally {
+			this.loading.hideLoading();
+			if (this.openGallery !== false) {
+				this.gallery.openWhenReady = this.openGallery;
+				this.openGallery = false;
+			}
+			this.loadMore.disabled = false;
+			this.loadMore.hidden = !this.hasMore;
+		}
+	}
 
-        // Start processing the first batch
-        if (items.length > 0) {
-            processBatch(0);
-        } else {
-            this.a11y.announceUpdate(0, append);
-        }
-    }
+	removePlaceholders() {
+		if (this.grid.querySelector('.placeholder')) {
+			window.removeChildren(this.grid);
+		}
+	}
+	showEmptyState() {
+		window.removeChildren(this.grid);
+		let template = window.getTemplate('emptyState');
+		let isFavourite = Object.hasOwn(this.filters, 'favourites') && this.filters.favourites === true;
+		if (isFavourite) {
+			[
+				template.querySelector('h3').textContent,
+				template.querySelector('p:first-of-type').textContent,
+				template.querySelector('p:last-of-type').textContent,
+			] = [
+				'♡ BLANK CANVAS ♡',
+				'You haven\'t fallen in love with any pieces... yet!',
+				'Hit that heart icon when something stops your scroll — your dream collection is waiting to start.'
+			];
+		}
+		this.grid.append(template);
+		this.a11y.announceEmpty(isFavourite);
 
-    /**
-     * 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;
+	}
+	handleError(error){
+		return this.error.handleApiError(
+			error,
+			{
+				component: 'Feed Block',
+				action: 'loaditems'
+			},
+			() => this.fetchFeed()
+		);
+	}
 
-        element.setAttribute('data-loaded', 'true');
-    }
+	addPlaceholders() {
+		let total = this.contentTypes.length - 1;
+		for (let i = 0; i < 9; i++) {
+			let template = window.getTemplate('placeholderTemplate');
+			let rand =  Math.floor(Math.random()*total+1);
+			let icon = window.getIcon(this.contentTypes[rand]).cloneNode(true);
 
-    /**
-     * Update image sizes based on screen width
-     */
-    updateImageSizes() {
-        const size = this.getImageSize();
+			template.append(icon);
+			this.grid.append(template);
+		}
+	}
+	renderItems(items, append = false) {
+		//Clear the grid if we aren't appending
+		if (!append) {
+			window.removeChildren(this.grid);
+			this.addPlaceholders();
+		}
 
-        // 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) {
+		//Bail early if no items
+		if (items.length === 0) {
+			this.a11y.announceUpdate(0, append);
+			return;
+		}
 
-        if(!this.rendered[item.icon]){
-            this.rendered[item.icon] = new Map();
-        }
+		//Use DocumentFragment for better performance
+		const fragment = document.createDocumentFragment();
 
-        if(this.rendered[item.icon].has(item.id)){
-            return this.rendered[item.icon].get(item.id);
-        }
+		const batchSize = 10;
+		const processBatch = (startIndex) => {
+			const endIndex = Math.min(startIndex + batchSize, items.length);
 
-        const favourited = window.isFavourited(item.icon, item.id)??false;
-        const template = window.getTemplate('feed-item');
+			for (let i = startIndex; i < endIndex; i++) {
+				const item = items[i];
+				const element = this.createItemElement(item);
+				fragment.appendChild(element);
 
-        // Set unique attributes
-        template.id = `${item.icon}-${item.id}`;
-        template.classList.add(item.icon);
+				this.imageObserver.observe(element);
+			}
 
-		if (item.umami_view) {
-			this.buildUmamiData(template, item.umami_view);
+			if (endIndex < items.length) {
+				requestAnimationFrame(() => {
+					processBatch(endIndex);
+				});
+			} else {
+				this.removePlaceholders();
+				//all batches are processed, append fragment
+				this.grid.appendChild(fragment);
+				if (this.config.gallery) {
+					this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
+				}
+				this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+				this.a11y.announceItems(items.length, append, this.hasMore);
+			}
+		};
+
+		if (items.length > 0) {
+			processBatch(0);
+		} else {
+			this.a11y.announceUpdate(0, append);
+		}
+	}
+
+	/**
+	 * Creates a feed-item. Used by RenderItems
+	 */
+	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');
+
+		template.id = `${item.icon}-${item.id}`;
+		template.dataset.id = item.id;
+		template.classList.add(item.icon);
+
+		if (item['umami_view']) {
+			this.buildUmamiData(template, item['umami_view']);
 		}
 
 		let favouriteButton = template.querySelector('button.favourite');
@@ -974,7 +535,7 @@
 		] = [
 			item.id,
 			item.icon,
-			item.user_id,
+			item['user_id'],
 			(favourited) ? 'Remove from Favourites' : 'Add to Favourites'
 		];
 
@@ -985,7 +546,7 @@
 		let summary = template.querySelector('summary');
 		let info = template.querySelector('.item-info');
 
-		for (let [index, id] of Object.entries(order)){
+		for (let [index, id] of Object.entries(order)) {
 			let target;
 			let config = item[id];
 			if (id === 'title') {
@@ -1009,7 +570,7 @@
 				} else {
 					target.remove();
 				}
-			} else if (Object.hasOwn(config, 'terms')) {
+			}  else if (Object.hasOwn(config, 'terms')) {
 				//Taxonomy list
 				if (config.terms.length === 0) {
 					continue;
@@ -1084,7 +645,10 @@
 				let img = images.querySelector('a');
 
 				let main = img.cloneNode(true);
-				main.href = item.url;
+				if (!this.config.gallery) {
+					main.href = item.url;
+				}
+
 				main.classList.add('feed-image');
 				this.buildImageData(main.querySelector('img'), item.image);
 				images.append(main);
@@ -1093,7 +657,9 @@
 					images.classList.add('multi');
 					item.content.forEach(c => {
 						let image = img.cloneNode(true);
-						image.href = c.url;
+						if (!this.config.gallery) {
+							image.href = c.url;
+						}
 						let itemImg = image.querySelector('img');
 						itemImg.src = c.image.small;
 						itemImg.alt = c.image.alt;
@@ -1106,552 +672,74 @@
 		single.remove();
 		list.remove();
 
-        this.rendered[item.icon].set(item.id, template);
+		this.rendered[item.icon].set(item.id, template);
 
-        return template;
-    }
+		return template;
+	}
 
-    buildImageData(img, data){
+	buildImageData(img, data){
 		if (typeof data.tiny !== 'string') {
 			return;
 		}
-        [
-            img.src,
-            img.dataset.small,
-            img.dataset.medium,
-            img.dataset.large,
-            img.alt
-        ] =
-        [
-            data.tiny,
-            data.small,
-            data.medium,
-            data.large,
-            data.alt
-        ];
-    }
+		[
+			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;
-        }
-    }
+	buildUmamiData(item, data){
+		for(let [key, value] of Object.entries(data)){
+			item.dataset[key] = value;
+		}
+	}
 
-    /**
-     * 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>`;
+	/**
+	 * Load Image, used by renderItems
+	 * @param element
+	 */
+	loadImage(element) {
+		const img = element.querySelector('img');
+		if (!img) return;
+		const size = this.getImageSize();
 
-        this.elements.grid.innerHTML = message;
-        this.a11y.announceEmpty(this.filters.favourites);
-    }
+		img.src = img.dataset[size] || img.dataset.src;
+		element.setAttribute('data-loaded', 'true');
+	}
 
-    /**
-     * Clear the grid
-     */
-    clearGrid() {
-        this.a11y.announce('Items cleared.');
-        window.removeChildren(this.elements.grid);
-        this.feed.loaded = 0;
-    }
-
-    /**
-     * Get image size based on screen width
-     */
-    getImageSize() {
-        const width = window.innerWidth;
-        if (width > 1024) return 'medium';
-        if (width > 500) return 'medium';
-        return 'small';
-    }
-
-    /**
-     * 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;
-
-                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;
-
-        const boundHandler = handler.bind(this);
-        element.addEventListener(event, boundHandler, options);
-
-        // 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();
-
-        // Clean up timers
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-        }
-
-        if (this.timeoutId) {
-            clearTimeout(this.timeoutId);
-        }
-
-        // Clear template cache and other state
-        this.feed.templates.clear();
-        this.feed.gallery = [];
-        this.feed.loaded = 0;
-    }
-    /** Extra Term Handling **/
-
-    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}"]`
-        );
-
-        return Array.from(selectedItems).map(item => item.dataset.id);
-    }
-
-    clearSelectedTaxonomies(){
-        window.removeChildren(this.elements.selected);
-        if(!isEmptyObject(this.selectorInstances)){
-            for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
-                instance.selectedItems = {};
-            }
-        }
-
-        this.elements.matchAll.querySelector(input).checked = false;
-
-        this.filters.taxonomies = {};
-        this.updateFilters();
-    }
-    setSelectedTerms(taxonomy){
-        if(this.selectorInstances[taxonomy]){
-            let selected = this.selectorInstances[taxonomy].selectedItems;
-            if(!isEmptyObject(selected)){
-
-                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];
-            }
-
-        }
-    }
-    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){
-
-        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 = {};
-        }
-
-
-        const selectedItems = this.elements.selected.querySelectorAll(
-            `.selected-item[data-taxonomy="${taxonomy}"]`
-        );
-
-        if(selectedItems.length > 0){
-            selectedItems.forEach(item => {
-                item.remove();
-            });
-        }
-
-        // Update clear filters button visibility
-        this.updateClearFiltersButton();
-    }
-    updateClearFiltersButton(){
-        if (!this.elements.clearFilters) return;
-
-        let filters = this.elements.selected.children.length;
-
-        const hasFilters = filters > 0;
-
-        const hasMultiple = filters > 1;
-
-        this.elements.clearFilters.hidden = !hasFilters;
-
-        this.elements.filters.classList.toggle('has-filters', hasFilters);
-
-        this.elements.matchAll.hidden = !hasMultiple;
-    }
-
-
-    /**
-     * Create a filter tag element
-     */
-    createFilterTag(taxonomy, id, name) {
-        const tag = window.getTemplate('selectedTerm');
-
-        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)}`];
-
-
-
-        return tag;
-    }
-
-    /**
-     * Gallery
-     **/
-    openGallery(index){
-        this.gallery.index = index;
-        this.gallery.modal.showModal();
-        this.hideBody();
-
-        this.bindGalleryEvents();
-        //show current image
-        this.updateDisplay(index);
-        //preload adjacent images
-        this.preloadImages();
-
-        // 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);
-
-    }
-
-    /**
-     * 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');
-
-        modal.innerHTML = `
-        <button class="gallery-close" aria-label="Close gallery">
-          ${jvbSettings.icons.close}
-        </button>
-
-        <button class="gallery-nav gallery-prev" aria-label="Previous image">
-          ${jvbSettings.icons.prev}
-        </button>
-
-        <button class="gallery-nav gallery-next" aria-label="Next image">
-             ${jvbSettings.icons.next}
-        </button>
-
-        <div class="gallery-content">
-          <img src="" alt="" class="gallery-image">
-          <details>
-            <summary>DETAILS</summary>
-            <div class="item-info"></div>
-          </details>
-        </div>
-
-        <div class="gallery-favourite"></div>
-        <div class="gallery-counter"><span id="gallery-index">1</span> / <span class="total"></span></div>
-    `;
-
-        return modal;
-    }
-
-
-    /**
-     * Bind event handlers
-     */
-    bindGalleryEvents() {
-        // Close button
-        this.gallery.modal.querySelector('.gallery-close').addEventListener('click', () => this.closeGallery());
-
-        // Navigation buttons
-        const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
-        const nextBtn = this.gallery.modal.querySelector('.gallery-next');
-
-        prevBtn.addEventListener('click', () => this.navigate(-1));
-        nextBtn.addEventListener('click', () => this.navigate(1));
-
-        // 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);
-
-        // Touch events
-        this.gallery.modal.addEventListener('touchstart', (e) => {
-            this.gallery.touchStart = e.touches[0].clientX;
-        });
-
-        this.gallery.modal.addEventListener('touchmove', (e) => {
-            this.gallery.touchEnd = e.touches[0].clientX;
-        });
-
-        this.gallery.modal.addEventListener('touchend', () => {
-            if (!this.gallery.touchStart || !this.gallery.touchEnd) return;
-
-            const distance = this.gallery.touchStart - this.gallery.touchEnd;
-            const isLeftSwipe = distance > this.gallery.minSwipe;
-            const isRightSwipe = distance < -this.gallery.minSwipe;
-
-            if (isLeftSwipe) {
-                this.navigate(1);
-            } else if (isRightSwipe) {
-                this.navigate(-1);
-            }
-
-            this.gallery.touchStart = null;
-            this.gallery.touchEnd = null;
-        });
-    }
-
-    /**
-     * Navigate to previous/next image
-     */
-    async navigate(direction) {
-        const newIndex = this.gallery.index + direction;
-
-        // 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;
-        }
-
-        // Update current index
-        this.gallery.index = newIndex;
-
-        // Update display
-        this.updateDisplay(newIndex);
-
-        // Preload adjacent images
-        this.preloadImages();
-
-        // Announce to screen readers
-        this.a11y.announceNavigation(this.gallery.index,this.gallery.items.length);
-
-        //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());
-        }
-    }
-
-    /**
-     * 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;
-                }
-            }
-        });
-    }
-
-    /**
-     * Update display with current image
-     */
-    updateDisplay(index) {
-        const item = this.gallery.items[index];
-        if (!item) return;
-
-        // 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 || '';
-
-        // Update favourite button
-        if (favourite && item.fav) {
-            window.removeChildren(favourite);
-            favourite.appendChild(item.fav.cloneNode(true));
-        }
-
-        // Update info
-        if (info && item.info) {
-            window.removeChildren(info);
-            const clone = item.info.cloneNode(true);
-            info.appendChild(clone);
-        }
-
-        // Update counter
-        counter.textContent = `${this.gallery.index + 1} / ${this.gallery.items.length}`;
-
-        // Update navigation buttons
-        this.updateNavigationButtons();
-    }
-
-    /**
-     * Update navigation button visibility
-     */
-    updateNavigationButtons() {
-        const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
-        const nextBtn = this.gallery.modal.querySelector('.gallery-next');
-
-        prevBtn.classList.toggle('end', this.gallery.index > 0 ? '' : 'none');
-        nextBtn.classList.toggle('end', this.gallery.index < this.gallery.items.length - 1 ? '' : 'none');
-    }
-
-    /**
-     * Close the gallery
-     */
-    closeGallery() {
-        this.showBody();
-        // Remove event listeners
-        document.removeEventListener('keydown', this.gallery.keyHandler);
-        this.a11y.announce('Gallery closed.');
-
-        this.gallery.modal.close();
-
-        // Reset state
-        this.gallery.keyHandler = null;
-    }
-
-    /**
-     * 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];
-
-        // Update items array
-        this.gallery.items = newItems;
-
-        // 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
-            );
-
-            if (newIndex !== -1) {
-                this.gallery.index = newIndex;
-            }
-        }
-
-        // Update navigation buttons
-        this.updateNavigationButtons();
-    }
-
-    /**
-     * Ensure gallery is accessible
-     */
-    setupGalleryAccessibility() {
-        // Add ARIA attributes
-        this.gallery.modal.setAttribute('aria-modal', 'true');
-        this.gallery.modal.setAttribute('aria-label', 'Image Gallery');
-    }
+	/**
+	 * Updates the image size according to screen size
+	 */
+	updateImageSizes() {
+		const size = this.getImageSize();
+		const items = this.grid.querySelectorAll('.item');
+		items.forEach(item => {
+			const img = item.querySelector('img');
+			if (img && img.dataset[size] && img.src !== img.dataset[size]) {
+				img.src = img.dataset[size];
+			}
+		});
+	}
+	/**
+	 * Get image size based on screen width
+	 */
+	getImageSize() {
+		const width = window.innerWidth;
+		if (width > 1024) return 'medium';
+		if (width > 500) return 'medium';
+		return 'small';
+	}
 
-    hideBody(){
-        document.body.style.overflow = 'hidden';
-    }
-    showBody(){
-        document.body.style.overflow = '';
-    }
 }
-
-// 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);
-    });
+	window.feedBlock = new FeedBlock();
 });
-
-
-function isEmptyObject(obj) {
-    return Object.keys(obj).length === 0;
-}

--
Gitblit v1.10.0