fix(tapops): batasi input tahun 4 digit dan tambah paginasi 6 dokumen per halaman
- Tambah atribut max/min pada date input agar tahun dibatasi 1000-9999, plus clamping via JS sebagai fallback lintas browser - Tambah paginasi 6 dokumen per halaman dengan navigasi prev/next dan nomor halaman (ellipsis untuk banyak halaman) - Reset ke halaman 1 saat filter, sort, search, atau rentang tanggal berubah - Clamp halaman aktif ke rentang valid jika jumlah hasil berkurang setelah filter - Tambah pesan "tidak ada hasil" saat filter aktif tidak menemukan berkas - Semua DOM pagination dibangun via safe DOM API (tanpa innerHTML)
This commit is contained in:
@@ -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)
|
Responsive — Phone (≤480px)
|
||||||
=========================================== */
|
=========================================== */
|
||||||
|
|||||||
+175
-6
@@ -416,6 +416,8 @@ class CustomDropdown {
|
|||||||
DocumentRepository
|
DocumentRepository
|
||||||
=========================================== */
|
=========================================== */
|
||||||
class DocumentRepository {
|
class DocumentRepository {
|
||||||
|
static PAGE_SIZE = 6;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.gridContainer = document.getElementById('document-grid');
|
this.gridContainer = document.getElementById('document-grid');
|
||||||
this.filterWrapper = document.getElementById('tag-filter-wrapper');
|
this.filterWrapper = document.getElementById('tag-filter-wrapper');
|
||||||
@@ -430,18 +432,21 @@ class DocumentRepository {
|
|||||||
this.statUpdatesEl = document.getElementById('stat-updates-value');
|
this.statUpdatesEl = document.getElementById('stat-updates-value');
|
||||||
|
|
||||||
this.cards = [];
|
this.cards = [];
|
||||||
|
this.paginationContainer = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
filter: 'all', // 'all' or a specific tag string
|
filter: 'all', // 'all' or a specific tag string
|
||||||
search: '',
|
search: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
sort: 'newest',
|
sort: 'newest',
|
||||||
|
page: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (!this.gridContainer) return;
|
if (!this.gridContainer) return;
|
||||||
this._bindControls();
|
this._bindControls();
|
||||||
|
this._initPagination();
|
||||||
try {
|
try {
|
||||||
const items = await DirectusClient.fetchPublished('berkas');
|
const items = await DirectusClient.fetchPublished('berkas');
|
||||||
this._renderCards(items);
|
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".
|
// Days within which an upload counts as a "recent update".
|
||||||
static RECENT_DAYS = 30;
|
static RECENT_DAYS = 30;
|
||||||
|
|
||||||
@@ -551,6 +564,7 @@ class DocumentRepository {
|
|||||||
*/
|
*/
|
||||||
_setFilter(key) {
|
_setFilter(key) {
|
||||||
this.state.filter = key;
|
this.state.filter = key;
|
||||||
|
this.state.page = 1;
|
||||||
|
|
||||||
// Sync buttons.
|
// Sync buttons.
|
||||||
if (this._filterBtnGroup) {
|
if (this._filterBtnGroup) {
|
||||||
@@ -567,21 +581,35 @@ class DocumentRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_bindControls() {
|
_bindControls() {
|
||||||
// Tag filter buttons are built dynamically after API fetch
|
// Limit date inputs to a 4-digit year (max 9999).
|
||||||
// (see _buildTagFilters). Only bind search/date/sort here.
|
[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.searchInput?.addEventListener('input', (e) => {
|
||||||
this.state.search = e.target.value.toLowerCase().trim();
|
this.state.search = e.target.value.toLowerCase().trim();
|
||||||
|
this.state.page = 1;
|
||||||
this._applyFilters();
|
this._applyFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dateStartInput?.addEventListener('change', (e) => {
|
this.dateStartInput?.addEventListener('change', (e) => {
|
||||||
this.state.startDate = e.target.value;
|
this.state.startDate = e.target.value;
|
||||||
|
this.state.page = 1;
|
||||||
this._applyFilters();
|
this._applyFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dateEndInput?.addEventListener('change', (e) => {
|
this.dateEndInput?.addEventListener('change', (e) => {
|
||||||
this.state.endDate = e.target.value;
|
this.state.endDate = e.target.value;
|
||||||
|
this.state.page = 1;
|
||||||
this._applyFilters();
|
this._applyFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -599,6 +627,7 @@ class DocumentRepository {
|
|||||||
prefixIcon: 'fas fa-sort-amount-down',
|
prefixIcon: 'fas fa-sort-amount-down',
|
||||||
onChange: (value) => {
|
onChange: (value) => {
|
||||||
this.state.sort = value;
|
this.state.sort = value;
|
||||||
|
this.state.page = 1;
|
||||||
this._applySort();
|
this._applySort();
|
||||||
this._applyFilters();
|
this._applyFilters();
|
||||||
}
|
}
|
||||||
@@ -668,19 +697,159 @@ class DocumentRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_applyFilters() {
|
_applyFilters() {
|
||||||
this.cards.forEach(({ view, element }) => {
|
const matched = this.cards.filter(({ view }) => this._cardMatches(view));
|
||||||
if (this._cardMatches(view)) {
|
const total = matched.length;
|
||||||
element.style.display = 'flex';
|
|
||||||
|
// 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(() => {
|
setTimeout(() => {
|
||||||
element.style.opacity = '1';
|
element.style.opacity = '1';
|
||||||
element.style.transform = 'translateY(0)';
|
element.style.transform = 'translateY(0)';
|
||||||
}, 10);
|
}, 10);
|
||||||
} else {
|
} else {
|
||||||
element.style.display = 'none';
|
|
||||||
element.style.opacity = '0';
|
element.style.opacity = '0';
|
||||||
element.style.transform = 'translateY(10px)';
|
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) {
|
_cardMatches(view) {
|
||||||
|
|||||||
Reference in New Issue
Block a user