class NewsManager { constructor(){ this.queue = window.jvbQueue; this.loading = window.jvbLoading; this.cache = window.jvbCache; this.a11y = window.jvbA11y; this.error = window.jvbError; this.activeTab = 'all'; this.tabs = new window.jvbTabs(document.querySelector('.replace'), { 'news': () => { this.activeTab = 'all'; this.resetFilters(); this.loadItems(true).then(()=>{}); }, 'mine': () => { console.log('switching to mine tab'); this.activeTab = 'own'; this.resetFilters(); this.filters.artist = window.auth.getUser(); this.loadItems(true).then(()=>{}); }, 'watching': () => { this.activeTab = 'watching'; this.resetFilters(); this.filters.watched = true; this.loadItems(true).then(()=>{}); } }); this.isLoading = false; this.alreadyHandling = false; this.template = new Map(); this.endpoints = { news: 'news', vote: 'news/vote' }; this.resetFilters(); this.state = { hasMore: true, pages: 1, items: 0 }; this.initElements(); this.initEvents(); this.loadItems(); } resetFilters(){ this.filters = { page: 1, order: 'DESC', orderby: 'date', shop: null, type: null, artist: null, watched: false, } } initElements(){ this.container = document.querySelector('.replace'); this.grid = this.container.querySelector('.item-grid'); this.addButton = this.container.querySelector('.add-item-btn'); this.addModal = new window.jvbModal( this.container.querySelector('.create-modal'), { render: this.renderModal.bind(this), open: this.addButton, content: 'news', openMessage: 'Opened modal to create a news post.', onSave: this.saveModal.bind(this), }); this.filterForm = this.container.querySelector('form'); this.dateRangeFilter = new window.jvbModal( this.container.querySelector('dialog.date-range'), { open: false, }); this.clearFilters = this.container.querySelector('.clear-filters'); this.replyModal = new window.jvbModal( this.container.querySelector('.create-response'), { open: false, content: 'response', openMessage: 'Opened Response modal', onSave: this.saveCreatedResponse.bind(this) }); } initEvents(){ this.filterForm.addEventListener('change', (e) => { let value = e.target.value; if(e.target.closest('.date-range')){ return; } if(value === 'custom'){ this.handleCustomDateRange(); }else{ let name = e.target.name; if(name){ this.filters[name] = value; }else{ this.resetFilters(); } this.loadItems(true); } }); document.addEventListener('click', (e) => { if(e.target === this.clearFilters){ this.filterForm.reset(); this.resetFilters(); this.loadItems(true); } if(e.target.closest('button.reply')){ //TODO: //1) get content of what we are responding to, and add it to .original //2) get the ID of the item we are responding to let button = e.target.closest('button'); let itemID = button.closest('.item').dataset.id; let original = ''; if(button.dataset.type === 'news'){ original = button.closest('.item').querySelector('.item-info').innerHTML; }else{ original = button.closest('.response').querySelector('.content').innerHTML; this.replyModal.modal.dataset.parent_id = button.id.replace('reply-to', ''); } this.replyModal.modal.dataset.id = itemID; this.replyModal.modal.dataset.type = button.dataset.type; this.replyModal.modal.querySelector('.original').innerHTML = '
Replying to:
'+original; this.replyModal.handleOpen(); } }); } renderModal(){ } handleCustomDateRange(){ this.dateRangeFilter.handleOpen(); let dateStart = this.dateRangeFilter.modal.querySelector('input.date-start'); let dateEnd = this.dateRangeFilter.modal.querySelector('input.date-end'); let custom = this.dateRangeFilter.modal.querySelector('select'); let inputs = this.dateRangeFilter.modal.querySelectorAll('input, select').forEach(input => { input.addEventListener('change', (e) => { //We need both beginning and end filters in order to parse the request if((input === dateStart && dateEnd.value !== '') || (input === dateEnd && dateStart.value !== '')){ this.filters.dateFrom = dateStart.value; this.filters.dateTo = dateEnd.value; this.dateRangeFilter.handleClose(); this.loadItems(true); }else if (input === custom){ //Or one of the preset months this.filters.customDate = custom.value; this.dateRangeFilter.handleClose(); this.loadItems(true); } }); }); // this.alreadyHandling = false; } async saveModal(form){ const formData = new FormData(this.addModal.modal.querySelector('form')); formData.append('user', window.auth.getUser()); this.queue.addToQueue({ type: 'new_news', data: formData, }); } /** * Load favourites from the server * @returns {Promise} Response data */ async loadItems(reset = true) { if(this.isLoading) return; try { this.isLoading = true; this.loading.show(); if(reset){ this.filters.page = 1; removeChildren(this.grid); this.grid.classList.remove('empty'); } const params = this.buildFilters(); const data = await this.cache.fetchWithCache( `${jvbSettings.api}${this.endpoints.news}?${params.toString()}`, { method: 'GET', headers: { 'X-WP-Nonce': window.auth.getNonce(), 'action_nonce': window.auth.getNonce('dash'), } },{ context: 'news', forceRefresh: true, //TODO: set false } ); // Process and render the favourites this.renderItems(data.items || [], this.filters.page > 1); // Update pagination info if (data.pagination) { this.state = { hasMore: data.has_more, items: data.items, pages: data.pages }; } return data; } catch (error) { this.handleError(error, 'loading news'); throw error; } finally { this.isLoading = false; this.loading.hide(); } } buildFilters(){ //Clone to avoid modifying original const filters = JSON.parse(JSON.stringify(this.filters)); let temp = {}; for(var[name, value] of Object.entries(filters)){ if(value !== false && value !== null){ temp[name] = value; } } return new URLSearchParams(temp); } renderItems(items, append = false){ if(!append){ removeChildren(this.grid); } if(items.length === 0){ this.a11y.announceItems(0, append); this.showEmptyState(); return; } // Use DocumentFragment for better performance const fragment = document.createDocumentFragment(); // Process items in batches for better performance const batchSize = 10; const processBatch = (startIndex) => { const endIndex = Math.min(startIndex + batchSize, items.length); // Process this batch for (let i = startIndex; i < endIndex; i++) { const item = items[i]; const element = this.createItemElement(item); fragment.appendChild(element); } // 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.grid.appendChild(fragment); this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])')); this.a11y.announceItems(items.length, append, this.state.hasMore); } }; // Start processing the first batch if (items.length > 0) { processBatch(0); } else { this.a11y.announceItems(0, append); } } createItemElement(item){ const itemEl = window.getTemplate(`template-${this.activeTab}`); itemEl.id = `news-${item.id}`; itemEl.dataset.id = item.id; const [title] = itemEl.getElementsByTagName('h3'); const [published] =itemEl.getElementsByClassName('published'); const [artist] = itemEl.getElementsByClassName('artist'); const [shop] = itemEl.getElementsByClassName('shop'); const [tldr] = itemEl.getElementsByClassName('tldr'); const [content] = itemEl.getElementsByClassName('item-info'); const [img] = itemEl.getElementsByClassName('image'); [title.textContent, published.textContent, artist.href, artist.textContent,tldr.textContent, content.innerHTML] = [item.title, formatTimeAgo(item.date), item.artist.url, item.artist.name, item.tldr, item['post_content']]; if(item.shop){ [shop.href, shop.innerHTML] = [item.shop.url, jvbSettings.icons.shop + item.shop.name]; }else{ shop.hidden = true; } const [favourite] = itemEl.getElementsByClassName('favourite-button'); if(this.activeTab !== 'own'){ [favourite.dataset.id, favourite.dataset.artist] = [item.id, item.artist.id]; if(window.userFavourites.news?.includes(parseInt(item.id))){ removeChildren(favourite); favourite.append(getIcon('star-fi')); }else{ removeChildren(favourite); favourite.append(getIcon('star')); } }else{ favourite.hidden = true; const [select] = itemEl.getElementsByClassName('select-checkbox'); const [selectLabel] = itemEl.getElementsByTagName('label'); [select.id, select.value, selectLabel.for] = [`item-${item.id}`, item.id, `item-${item.id}`]; } let status = ''; if(window.userVotes?.news?.has(item.id)){ status = window.userVotes.news.get(item.id); } console.log(item); itemEl.querySelector('.summary').appendChild(formatVote(item, status)); let commentButton = window.getTemplate('commentsButton'); commentButton.href = (`#responses-to-${item.id}`); commentButton.querySelector('.count').textContent = item.comments.items.length; let responses = window.getTemplate('responses'); responses.id = `responses-to-${item.id}`; let summary =responses.querySelector('summary'); summary.textContent += ' { '+item.comments.items.length + ' }'; let reply = window.getTemplate('replyButton'); reply.id = 'reply-to-'+item.id; reply.dataset.type = 'news'; reply.dataset.action = 'reply'; itemEl.appendChild(reply); if(item.comments.items.length > 0){ item.comments.items.forEach(comment => { responses.appendChild(this.formatComment(comment)); }); } itemEl.appendChild(responses); itemEl.querySelector('.vote').prepend(commentButton,itemEl.querySelector('.vote button')); const imageHTML = (item.image) ? item.image.replace(/src="([^"]+)"/, 'data-src="$1"') : ''; return itemEl; } formatComment(comment, parent = null){ let response = window.getTemplate('response'); response.id = 'response-'+comment.id; let summary = response.querySelector('summary'); summary.querySelector('.content').innerHTML = comment.response; summary.querySelector('.created').textContent = formatTimeAgo(comment.created_at); //Add voting buttons let status = checkVoteStatus('response',comment.id); comment.content = 'response'; response.querySelector('.footer').appendChild(formatVote(comment, status)); console.log(comment); let reply = window.getTemplate('replyButton'); reply.id = 'reply-to-'+comment.id; if(parent){ reply.dataset.parent_id = parent; } reply.dataset.action = 'reply'; reply.dataset.type = comment.content; summary.querySelector('.vote').prepend(reply, summary.querySelector('.vote').firstElementChild); let artist = summary.querySelector('.artist'); let shop = summary.querySelector('.shop'); console.log(comment); if(comment.artist){ if(!comment.artist.shop){ shop.remove(); } [artist.href, artist.textContent, shop.href, shop.textContent] = [comment.artist.url, comment.artist.name, comment.artist.shop.url, comment.artist.shop.name]; }else{ artist.remove(); shop.remove(); } //Add any replies if(comment.children.items.length>0){ let responses = window.getTemplate('responses'); responses.id = 'replies-to-'+comment.id; responses.querySelector('summary').textContent = 'See Responses {'+comment.children.items.length+'}'; comment.children.items.forEach(item=>{ responses.appendChild(this.formatComment(item, comment.id)); }); response.appendChild(responses); } return response; } renderResponseCreate(){ } saveCreatedResponse(){ console.log('Saving create response'); console.log(this.replyModal.modal.id) const modal = this.replyModal.modal; let data = { user: window.auth.getUser(), item_id: modal.dataset.id, response: modal.querySelector('.ql-editor').innerHTML, content: modal.dataset.type, action: 'create', }; if(modal.dataset.parent_id){ data.parent_id = modal.dataset.parent_id; } console.log(data); this.queue.addToQueue({ type: 'new_response', data: data, }); } showEmptyState(){ const empty = document.createElement('div'); empty.className = 'no-news'; empty.innerHTML = `

Nothing here

No updates here.

Add some gap fillers from the main favourites tab.

`; this.grid.appendChild(empty); this.grid.classList.add('empty'); this.a11y.announce('No favourites to show!'); } hideEmptyState(){ let empty = this.grid.querySelector('.no-news'); if(empty) { empty.remove(); } } /** * Handle errors * @param {Error} error - Error object * @param {string} action - Action being performed when error occurred */ handleError(error, action) { console.error(`News error (${action}):`, error); // Log with error handler if available if (window.jvbError) { window.jvbError.log(error, { component: 'NewsManager', action: action }); } // Announce to screen readers if (window.jvbA11y) { window.jvbA11y.announce(`Error ${action}. ${error.message || 'Please try again.'}`); } } } window.newsManager = NewsManager;