Jake Vanderwerf
2026-01-01 de699440ff94e9c0d8e8b5201ee93179c085bf60
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;
}