/** * Alpine.js Pagination Component * * Provides client-side pagination for large lists. */ document.addEventListener('alpine:init', () => { Alpine.data('remotePaginatedList', (options = {}) => ({ items: [], currentPage: 1, totalPages: 0, totalItems: 0, perPage: options.perPage || 50, endpoint: options.endpoint || '', isReviewed: options.isReviewed || false, instanceId: options.instanceId || Math.random().toString(36).substring(2, 9), isLoading: false, error: null, init() { if (this.endpoint) { this.fetchPage(1); } }, get paginatedItems() { return this.items; }, get showingStart() { if (this.totalItems === 0) return 0; return (this.currentPage - 1) * this.perPage + 1; }, get showingEnd() { if (this.totalItems === 0) return 0; return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems); }, async fetchPage(page) { if (!this.endpoint) { return; } this.isLoading = true; this.error = null; try { const url = new URL(this.endpoint, window.location.origin); // Preserve existing query parameters and add pagination params url.searchParams.set('page', page.toString()); url.searchParams.set('page_size', this.perPage.toString()); const response = await fetch(url.toString(), { headers: { Accept: 'application/json' }, credentials: 'same-origin' }); if (!response.ok) { throw new Error(`Failed to load ${this.endpoint} (status ${response.status})`); } const data = await response.json(); this.items = data.items || []; this.totalItems = data.total_items || 0; this.totalPages = data.total_pages || 0; this.currentPage = data.page || page; this.perPage = data.page_size || this.perPage; // Dispatch event for parent components (e.g., tab count updates) this.$dispatch('items-loaded', { totalItems: this.totalItems }); } catch (err) { console.error(err); this.error = `Unable to load ${this.endpoint}. Please try again.`; } finally { this.isLoading = false; } }, nextPage() { if (this.currentPage < this.totalPages) { this.fetchPage(this.currentPage + 1); } }, prevPage() { if (this.currentPage > 1) { this.fetchPage(this.currentPage - 1); } }, goToPage(page) { if (page >= 1 && page <= this.totalPages) { this.fetchPage(page); } }, get pageNumbers() { const pages = []; const total = this.totalPages; const current = this.currentPage; if (total === 0) { return pages; } if (total <= 7) { for (let i = 1; i <= total; i++) { pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` }); } } else { pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` }); let rangeStart; let rangeEnd; if (current <= 4) { rangeStart = 2; rangeEnd = 5; } else if (current >= total - 3) { rangeStart = total - 4; rangeEnd = total - 1; } else { rangeStart = current - 1; rangeEnd = current + 1; } if (rangeStart > 2) { pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` }); } for (let i = rangeStart; i <= rangeEnd; i++) { pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` }); } if (rangeEnd < total - 1) { pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` }); } pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` }); } return pages; } })); });