diff --git a/groups/Tapops/css/documents.css b/groups/Tapops/css/documents.css index 19ae003..799ac4d 100644 --- a/groups/Tapops/css/documents.css +++ b/groups/Tapops/css/documents.css @@ -765,6 +765,104 @@ a.btn-action-outline:hover { } } +/* =========================================== + Responsive — Phone (≤480px) + =========================================== */ + +/* =========================================== + Pagination + =========================================== */ + +.doc-pagination { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32px 0 0; + gap: 16px; + flex-wrap: wrap; +} + +.pagination-info { + font-size: 0.75rem; + font-weight: 800; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.pagination-nav { + display: flex; + align-items: center; + gap: 6px; +} + +.pagination-btn { + min-width: 40px; + height: 40px; + padding: 0 10px; + border: 1px solid #e2e8f0; + background: white; + color: var(--primary-blue); + font-size: 0.85rem; + font-weight: 700; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + font-family: inherit; +} + +.pagination-btn:hover:not(.disabled):not(.active) { + border-color: var(--secondary-gold); + background: #fffdf5; +} + +.pagination-btn.active { + background: var(--primary-blue); + border-color: var(--primary-blue); + color: white; +} + +.pagination-btn.disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +.pagination-ellipsis { + min-width: 32px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + color: #94a3b8; + font-weight: 700; + font-size: 1rem; + letter-spacing: 1px; +} + +@media (max-width: 768px) { + .doc-pagination { + flex-direction: column; + align-items: center; + gap: 12px; + } +} + +@media (max-width: 480px) { + .pagination-btn { + min-width: 36px; + height: 36px; + font-size: 0.8rem; + } + + .pagination-nav { + gap: 4px; + } +} + /* =========================================== Responsive — Phone (≤480px) =========================================== */ diff --git a/groups/Tapops/js/main.js b/groups/Tapops/js/main.js index 9896cc8..1f6668f 100644 --- a/groups/Tapops/js/main.js +++ b/groups/Tapops/js/main.js @@ -416,6 +416,8 @@ class CustomDropdown { DocumentRepository =========================================== */ class DocumentRepository { + static PAGE_SIZE = 6; + constructor() { this.gridContainer = document.getElementById('document-grid'); this.filterWrapper = document.getElementById('tag-filter-wrapper'); @@ -430,18 +432,21 @@ class DocumentRepository { this.statUpdatesEl = document.getElementById('stat-updates-value'); this.cards = []; + this.paginationContainer = null; this.state = { filter: 'all', // 'all' or a specific tag string search: '', startDate: '', endDate: '', sort: 'newest', + page: 1, }; } async init() { if (!this.gridContainer) return; this._bindControls(); + this._initPagination(); try { const items = await DirectusClient.fetchPublished('berkas'); this._renderCards(items); @@ -456,6 +461,14 @@ class DocumentRepository { } } + _initPagination() { + const pag = document.createElement('div'); + pag.id = 'doc-pagination'; + pag.className = 'doc-pagination'; + this.gridContainer.insertAdjacentElement('afterend', pag); + this.paginationContainer = pag; + } + // Days within which an upload counts as a "recent update". static RECENT_DAYS = 30; @@ -551,6 +564,7 @@ class DocumentRepository { */ _setFilter(key) { this.state.filter = key; + this.state.page = 1; // Sync buttons. if (this._filterBtnGroup) { @@ -567,21 +581,35 @@ class DocumentRepository { } _bindControls() { - // Tag filter buttons are built dynamically after API fetch - // (see _buildTagFilters). Only bind search/date/sort here. + // Limit date inputs to a 4-digit year (max 9999). + [this.dateStartInput, this.dateEndInput].forEach(input => { + if (!input) return; + input.setAttribute('max', '9999-12-31'); + input.setAttribute('min', '1000-01-01'); + // Clamp year on change as a fallback for browsers that don't enforce max. + input.addEventListener('change', () => { + if (!input.value) return; + const [yearStr, month, day] = input.value.split('-'); + const year = parseInt(yearStr, 10); + if (year > 9999) input.value = `9999-${month}-${day}`; + }); + }); this.searchInput?.addEventListener('input', (e) => { this.state.search = e.target.value.toLowerCase().trim(); + this.state.page = 1; this._applyFilters(); }); this.dateStartInput?.addEventListener('change', (e) => { this.state.startDate = e.target.value; + this.state.page = 1; this._applyFilters(); }); this.dateEndInput?.addEventListener('change', (e) => { this.state.endDate = e.target.value; + this.state.page = 1; this._applyFilters(); }); @@ -599,6 +627,7 @@ class DocumentRepository { prefixIcon: 'fas fa-sort-amount-down', onChange: (value) => { this.state.sort = value; + this.state.page = 1; this._applySort(); this._applyFilters(); } @@ -668,19 +697,159 @@ class DocumentRepository { } _applyFilters() { - this.cards.forEach(({ view, element }) => { - if (this._cardMatches(view)) { - element.style.display = 'flex'; + const matched = this.cards.filter(({ view }) => this._cardMatches(view)); + const total = matched.length; + + // Handle zero-results state when cards exist but none match the filter. + this._toggleNoResultsState(total === 0 && this.cards.length > 0); + + if (total === 0) { + this.cards.forEach(({ element }) => { element.style.display = 'none'; }); + this._renderPagination(0, 0); + return; + } + + const totalPages = Math.ceil(total / DocumentRepository.PAGE_SIZE); + + // Clamp page to valid range after filter changes. + this.state.page = Math.min(Math.max(this.state.page, 1), totalPages); + + const start = (this.state.page - 1) * DocumentRepository.PAGE_SIZE; + const pageSet = new Set( + matched.slice(start, start + DocumentRepository.PAGE_SIZE).map(c => c.element) + ); + + this.cards.forEach(({ element }) => { + const show = pageSet.has(element); + element.style.display = show ? 'flex' : 'none'; + if (show) { setTimeout(() => { element.style.opacity = '1'; element.style.transform = 'translateY(0)'; }, 10); } else { - element.style.display = 'none'; element.style.opacity = '0'; element.style.transform = 'translateY(10px)'; } }); + + this._renderPagination(totalPages, total); + } + + _toggleNoResultsState(show) { + const existing = this.gridContainer.querySelector('.repo-no-results'); + if (show && !existing) { + const wrapper = document.createElement('div'); + wrapper.className = 'repo-no-results'; + Object.assign(wrapper.style, { + gridColumn: '1 / -1', + textAlign: 'center', + padding: '60px 20px', + color: '#64748b', + fontWeight: '700', + fontSize: '1rem', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '15px', + }); + const icon = document.createElement('i'); + icon.className = 'fas fa-search'; + Object.assign(icon.style, { fontSize: '3rem', color: '#cbd5e1' }); + const span = document.createElement('span'); + span.setAttribute('data-tp-i18n', 'state_no_results'); + span.textContent = (window.tpT?.('state_no_results')) || 'Tidak ada berkas yang cocok dengan filter saat ini.'; + wrapper.appendChild(icon); + wrapper.appendChild(span); + this.gridContainer.appendChild(wrapper); + } else if (!show && existing) { + existing.remove(); + } + } + + _renderPagination(totalPages, total) { + if (!this.paginationContainer) return; + this.paginationContainer.innerHTML = ''; + if (total === 0 || totalPages <= 1) return; + + const current = this.state.page; + const start = (current - 1) * DocumentRepository.PAGE_SIZE + 1; + const end = Math.min(current * DocumentRepository.PAGE_SIZE, total); + + const info = document.createElement('span'); + info.className = 'pagination-info'; + info.textContent = `${start}–${end} dari ${total} berkas`; + this.paginationContainer.appendChild(info); + + const nav = document.createElement('nav'); + nav.className = 'pagination-nav'; + nav.setAttribute('aria-label', 'Navigasi halaman dokumen'); + + const prev = document.createElement('button'); + prev.className = 'pagination-btn' + (current <= 1 ? ' disabled' : ''); + prev.disabled = current <= 1; + prev.setAttribute('aria-label', 'Halaman sebelumnya'); + const prevIcon = document.createElement('i'); + prevIcon.className = 'fas fa-chevron-left'; + prev.appendChild(prevIcon); + prev.addEventListener('click', () => { + if (this.state.page > 1) { this.state.page--; this._applyFilters(); this._scrollToGrid(); } + }); + nav.appendChild(prev); + + this._paginationRange(current, totalPages).forEach(p => { + if (p === '...') { + const el = document.createElement('span'); + el.className = 'pagination-ellipsis'; + el.setAttribute('aria-hidden', 'true'); + el.textContent = '…'; + nav.appendChild(el); + } else { + const btn = document.createElement('button'); + btn.className = 'pagination-btn pagination-number' + (p === current ? ' active' : ''); + btn.textContent = p; + btn.setAttribute('aria-label', `Halaman ${p}`); + if (p === current) btn.setAttribute('aria-current', 'page'); + btn.addEventListener('click', () => { + this.state.page = p; this._applyFilters(); this._scrollToGrid(); + }); + nav.appendChild(btn); + } + }); + + const next = document.createElement('button'); + next.className = 'pagination-btn' + (current >= totalPages ? ' disabled' : ''); + next.disabled = current >= totalPages; + next.setAttribute('aria-label', 'Halaman berikutnya'); + const nextIcon = document.createElement('i'); + nextIcon.className = 'fas fa-chevron-right'; + next.appendChild(nextIcon); + next.addEventListener('click', () => { + if (this.state.page < totalPages) { this.state.page++; this._applyFilters(); this._scrollToGrid(); } + }); + nav.appendChild(next); + + this.paginationContainer.appendChild(nav); + } + + // Returns page numbers and '...' ellipsis placeholders for the given range. + _paginationRange(current, total) { + if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1); + + const pages = [1]; + const lo = Math.max(2, current - 1); + const hi = Math.min(total - 1, current + 1); + + if (lo > 2) pages.push('...'); + for (let i = lo; i <= hi; i++) pages.push(i); + if (hi < total - 1) pages.push('...'); + pages.push(total); + + return pages; + } + + _scrollToGrid() { + document.getElementById('repository')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } _cardMatches(view) {