/** * FrontendInteractions - Unified class for frontend user interactions * Handles: Favourites, Votes, and related user actions */ class UserInteractions { constructor() { if (!window.auth.getUser()) { return; // Don't initialize if not logged in } // Initialize favourites store this.favouritesStore = window.jvbStore.register( 'favourites', { storeName: 'favourites', endpoint: 'favourites', indexes: [ {name: 'content', keyPath: 'content'}, {name: 'listId', keyPath: 'listId'}, ], TTL: 6 * 60 * 1000, showLoading: false, filters: { user: window.auth.getUser(), content: 'all', order: 'desc', orderby: 'date', page: 1, all: true, } } ); // Initialize favourites lists store this.listsStore = window.jvbStore.register( 'favourites_lists', { storeName: 'lists', keyPath: 'listId', endpoint: 'favourites/lists', TTL: 6 * 60 * 1000, } ); // Initialize votes store this.votesStore = window.jvbStore.register( 'votes', { storeName: 'votes', endpoint: 'votes', useIndexedDB: true, TTL: 6 * 60 * 1000, showLoading: false } ); this.setupEventListeners(); this.favouritesStore.fetch(); } setupEventListeners() { // Subscribe to favourites updates this.favouritesStore.subscribe((event, data) => { switch (event) { case 'data-fetched': case 'data-cached': case 'items-updated': case 'item-stored': // Could handle UI updates here break; } }); } /** * Toggle favourite status * @param {HTMLElement} button - Button element with data attributes */ toggleFavourite(button) { if (!window.auth.getUser()) { window.location.href = jvbSettings.redirect + '&action=register&type=favourites'; return; } // Toggle UI immediately button.classList.toggle('favourited'); const action = button.classList.contains('favourited') ? 'add' : 'remove'; const message = button.classList.contains('favourited') ? `Added ${button.dataset.type} to favourites.` : `Removed ${button.dataset.type} from favourites.`; window.jvbA11y.announce(message); // Update button icon button.innerHTML = jvbSettings.icons[button.classList.contains('favourited') ? 'heart-filled' : 'heart']; // Save to store this.favouritesStore.setItem(button.dataset.id, { target_id: button.dataset.id, action: action, type: button.dataset.type, artist: button.dataset.artist, }); } /** * Handle vote action * @param {HTMLElement} button - Vote button element */ handleVote(button) { if (!window.auth.getUser()) { window.location.href = jvbSettings.redirect + '&action=register&type=vote'; return; } // Queue the vote operation window.jvbQueue.handleVote(button); const parent = button.closest('.vote'); const alreadyVoted = parent.querySelector('.voted'); // Handle previous vote if exists if (alreadyVoted) { const count = alreadyVoted.querySelector('.count'); if (alreadyVoted.classList.contains('up')) { count.textContent = parseInt(count.textContent) - 1; } else { count.textContent = parseInt(count.textContent) + 1; } alreadyVoted.classList.remove('voted'); } // Update current vote button.classList.add('voted'); const count = button.querySelector('.count'); if (button.classList.contains('up')) { count.textContent = parseInt(count.textContent) + 1; } else { count.textContent = parseInt(count.textContent) - 1; } } /** * Check if an item is favourited * @param {string} content - Content type * @param {string|number} id - Item ID * @returns {boolean} */ isFavourited(content, id) { if (!window.auth.getUser()) { return false; } if (typeof window.userFavourites === 'undefined') { return false; } if (typeof window.userFavourites[content] === 'undefined') { return false; } return window.userFavourites[content]?.has(id); } /** * Check if user has voted on an item * @param {string} content - Content type * @param {string|number} id - Item ID * @returns {string} - 'up', 'down', or '' */ checkVoteStatus(content, id) { if (!window.auth.getUser()) { return ''; } let status = ''; if (window.userVotes && window.userVotes[content]?.has(id)) { status = window.userVotes[content].get(id); } return status; } } // Lazy initialization using requestIdleCallback for better performance function initFrontendInteractions() { if (window.auth.getUser()) { window.jvbInteractions = new FrontendInteractions(); } } // Initialize after DOM is ready but without blocking render if ('requestIdleCallback' in window) { requestIdleCallback(async function() { window.auth.subscribe((event) => { if (event === 'auth-loaded') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initFrontendInteractions); } else { initFrontendInteractions(); } } }); }); } else { // Fallback for browsers without requestIdleCallback if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initFrontendInteractions); } else { setTimeout(initFrontendInteractions, 1); } } /** * Global helper functions for backwards compatibility */ window.toggleFavourite = function(button) { if (!window.jvbInteractions) { console.warn('FrontendInteractions not initialized'); return; } window.jvbInteractions.toggleFavourite(button); } window.handleVote = function(button) { if (!window.jvbInteractions) { console.warn('FrontendInteractions not initialized'); return; } window.jvbInteractions.handleVote(button); } window.isFavourited = function(content, id) { if (!window.jvbInteractions) { return false; } return window.jvbInteractions.isFavourited(content, id); } window.checkVoteStatus = function(content, id) { if (!window.jvbInteractions) { return ''; } return window.jvbInteractions.checkVoteStatus(content, id); } /** * Formats vote from template * @param item * @param status * @returns {Node|ActiveX.IXMLDOMNode|boolean} */ window.formatVote = function(item, status) { let vote = window.getTemplate('voteButton'); vote.dataset.itemId = item.id; vote.dataset.content = item.content; let up =vote.querySelector('button.up'); let down =vote.querySelector('button.down'); if(status === 'up'){ up.classList.add('voted'); } if(status === 'down'){ down.classList.add('voted'); } if(item.upvotes > 0){ up.querySelector('.count').textContent = item.upvotes; } if(item.downvotes > 0){ down.querySelector('.count').textContent = '-'+item.downvotes; } return vote; } /** * Tests if user has voted for this item * @param content * @param id * @returns {string} */ window.checkVoteStatus = function(content, id){ if(!window.auth.getUser()){ return ''; } let status = ''; if(window.userVotes && window.userVotes[content]?.has(id)){ status = window.userVotes[content].get(id); } return status; }