Skip to content

Commit 91d4d4e

Browse files
authored
Merge pull request #209 from Osalotioman/enhance/paginate-table
enhance: Paginate Table
2 parents 52092c7 + 121fda1 commit 91d4d4e

4 files changed

Lines changed: 230 additions & 13 deletions

File tree

docs/app.js

Lines changed: 127 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,94 @@ let allProblems = [];
88
let filteredProblems = [];
99
let currentSort = { column: 'number', direction: 'asc' };
1010

11+
// Pagination
12+
const DEFAULT_PAGE_SIZE = 100;
13+
let currentPage = 1;
14+
let pageSize = DEFAULT_PAGE_SIZE;
15+
16+
function getPageSizeFromUI() {
17+
const input = document.getElementById('page-size');
18+
if (!input) return pageSize;
19+
20+
// Keep the last valid value while the user is typing.
21+
const value = input.value.trim();
22+
if (!/^[1-9]\d*$/.test(value)) return pageSize;
23+
24+
// Regex guarantees a positive integer.
25+
return Number(value);
26+
}
27+
28+
function setPageSizeInUI(size) {
29+
const input = document.getElementById('page-size');
30+
if (!input) return;
31+
input.value = String(size);
32+
}
33+
34+
function resetToFirstPageAndUpdate() {
35+
currentPage = 1;
36+
updateTable();
37+
}
38+
39+
function updatePaginationUI(totalPages) {
40+
const info = document.getElementById('pagination-info');
41+
const prevBtn = document.getElementById('pagination-prev');
42+
const nextBtn = document.getElementById('pagination-next');
43+
44+
if (info) {
45+
info.textContent = `Page ${currentPage.toLocaleString()} / ${totalPages.toLocaleString()}`;
46+
}
47+
if (prevBtn) {
48+
prevBtn.disabled = currentPage <= 1;
49+
}
50+
if (nextBtn) {
51+
nextBtn.disabled = currentPage >= totalPages;
52+
}
53+
}
54+
55+
function initializePaginationListeners() {
56+
const prevBtn = document.getElementById('pagination-prev');
57+
const nextBtn = document.getElementById('pagination-next');
58+
const pageSizeInput = document.getElementById('page-size');
59+
60+
if (prevBtn) {
61+
prevBtn.addEventListener('click', () => {
62+
currentPage = Math.max(1, currentPage - 1);
63+
updateTable();
64+
});
65+
}
66+
67+
if (nextBtn) {
68+
nextBtn.addEventListener('click', () => {
69+
currentPage += 1;
70+
updateTable();
71+
});
72+
}
73+
74+
if (pageSizeInput) {
75+
let inputTimeout;
76+
77+
const applyPageSize = () => {
78+
const newSize = getPageSizeFromUI();
79+
pageSize = newSize;
80+
// Changing page size should reset to page 1
81+
currentPage = 1;
82+
updateTable(); // updateTable will save state to URL
83+
};
84+
85+
// Update as the user types, but debounce so we don't re-render on every keystroke.
86+
pageSizeInput.addEventListener('input', () => {
87+
clearTimeout(inputTimeout);
88+
inputTimeout = setTimeout(applyPageSize, 250);
89+
});
90+
91+
// Commit immediately on blur/enter (change fires on commit)
92+
pageSizeInput.addEventListener('change', () => {
93+
clearTimeout(inputTimeout);
94+
applyPageSize();
95+
});
96+
}
97+
}
98+
1199
/**
12100
* Load problems from YAML file
13101
* @returns {Promise<Array<Object>>} Array of problem objects
@@ -101,7 +189,8 @@ function renderTable(problems) {
101189

102190
if (problems.length === 0) {
103191
tableBody.innerHTML = '<tr><td colspan="7" class="loading-cell">No problems match the current filters.</td></tr>';
104-
updateStats(0, allProblems.length);
192+
// With pagination, show filtered vs total(allProblems.length) counts (range is empty here)
193+
updateStats();
105194
return;
106195
}
107196

@@ -131,18 +220,27 @@ function renderTable(problems) {
131220
tableBody.innerHTML = rows;
132221

133222
// Update stats
134-
updateStats(problems.length, allProblems.length);
223+
updateStats();
135224
}
136225

137226
/**
138227
* Update statistics display
139-
* @param {number} showing - Number of problems currently shown
140-
* @param {number} total - Total number of problems
141228
*/
142-
function updateStats(showing, total) {
229+
function updateStats() {
143230
const showingCount = document.getElementById('showing-count');
144231
if (showingCount) {
145-
showingCount.textContent = `Showing ${showing.toLocaleString()} of ${total.toLocaleString()} problems`;
232+
const filteredTotal = Array.isArray(filteredProblems) ? filteredProblems.length : 0;
233+
const start = filteredTotal === 0 ? 0 : ((currentPage - 1) * pageSize + 1);
234+
const end = filteredTotal === 0 ? 0 : Math.min(currentPage * pageSize, filteredTotal);
235+
236+
// N.B: total = allProblems.length i.e total number of problems
237+
// Example: "Showing 201–300 of 1,742 (total 2,100) problems"
238+
// When no filters are active, filteredTotal === total.
239+
if (filteredTotal > 0) {
240+
showingCount.textContent = `Showing ${start.toLocaleString()}${end.toLocaleString()} of ${filteredTotal.toLocaleString()} (total ${allProblems.length.toLocaleString()}) problems`;
241+
} else {
242+
showingCount.textContent = `Showing 0 of 0 (total ${allProblems.length.toLocaleString()}) problems`;
243+
}
146244
}
147245
}
148246

@@ -167,11 +265,11 @@ function handleSortClick(event) {
167265
// Update visual indicators
168266
updateSortIndicators(currentSort.column, currentSort.direction);
169267

268+
// Sorting should reset pagination
269+
currentPage = 1;
270+
170271
// Re-render table
171272
updateTable();
172-
173-
// Save state to URL
174-
saveStateToURL(getCurrentState());
175273
}
176274

177275
/**
@@ -194,6 +292,14 @@ function updateTable() {
194292
// Store filtered results
195293
filteredProblems = results;
196294

295+
// Pagination: slice the final, sorted results
296+
pageSize = getPageSizeFromUI();
297+
const totalPages = Math.max(1, Math.ceil(results.length / pageSize));
298+
if (!Number.isFinite(currentPage) || currentPage < 1) currentPage = 1;
299+
if (currentPage > totalPages) currentPage = totalPages;
300+
const startIndex = (currentPage - 1) * pageSize;
301+
const pagedResults = results.slice(startIndex, startIndex + pageSize);
302+
197303
// Update tag and dropdown displays with filtered counts
198304
const nonTagFiltersActive = hasNonTagFilters();
199305

@@ -216,7 +322,10 @@ function updateTable() {
216322
updateAllDropdownDisplays(allProblems, hasAnyFilters);
217323

218324
// Render
219-
renderTable(results);
325+
renderTable(pagedResults);
326+
327+
// Update pagination controls
328+
updatePaginationUI(totalPages);
220329

221330
// Save state to URL
222331
saveStateToURL(getCurrentState());
@@ -263,7 +372,8 @@ async function initialize() {
263372
}
264373

265374
// Set filter change handler FIRST (before creating any event listeners)
266-
setFilterChangeHandler(updateTable);
375+
// Any query change should reset pagination.
376+
setFilterChangeHandler(resetToFirstPageAndUpdate);
267377

268378
// Extract tag counts and tags
269379
const tagCounts = extractTagCounts(allProblems);
@@ -290,6 +400,11 @@ async function initialize() {
290400
currentSort.column = urlState.sortColumn;
291401
currentSort.direction = urlState.sortDirection;
292402

403+
// Restore pagination from URL
404+
currentPage = urlState.page || 1;
405+
pageSize = urlState.pageSize || DEFAULT_PAGE_SIZE;
406+
setPageSizeInUI(pageSize);
407+
293408
// Get initial tag sort preference from URL
294409
const initialTagSort = urlState.tagSort || 'count';
295410

@@ -303,6 +418,7 @@ async function initialize() {
303418
// Initialize event listeners
304419
initializeSortListeners();
305420
initializeFilterListeners();
421+
initializePaginationListeners();
306422

307423
// Initial render
308424
updateTable();

docs/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,19 @@ <h1>Erdős Problems Database</h1>
129129
<span id="loading-indicator" class="loading">Loading data...</span>
130130
</div>
131131

132+
<div class="pagination-bar" aria-label="Pagination controls">
133+
<div class="pagination-controls">
134+
<button id="pagination-prev" class="pagination-btn" type="button">Prev</button>
135+
<span id="pagination-info" class="pagination-info">Page 1 / 1</span>
136+
<button id="pagination-next" class="pagination-btn" type="button">Next</button>
137+
</div>
138+
139+
<div class="page-size-controls">
140+
<label for="page-size" class="page-size-label">Per page:</label>
141+
<input id="page-size" class="page-size-input" type="number" inputmode="numeric" min="1" step="1" value="100" />
142+
</div>
143+
</div>
144+
132145
<div class="table-container">
133146
<table id="problems-table">
134147
<thead>

docs/styles.css

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,75 @@ main {
396396
font-style: italic;
397397
}
398398

399+
/* Pagination Bar (minimal) */
400+
.pagination-bar {
401+
display: flex;
402+
justify-content: space-between;
403+
align-items: center;
404+
gap: 1rem;
405+
padding: 0.75rem 1rem;
406+
background-color: var(--bg-secondary);
407+
border: 1px solid var(--border-color);
408+
border-radius: 4px;
409+
margin-bottom: 1rem;
410+
font-size: 0.95rem;
411+
}
412+
413+
.pagination-controls {
414+
display: flex;
415+
align-items: center;
416+
gap: 0.75rem;
417+
}
418+
419+
.pagination-btn {
420+
background-color: var(--button-bg);
421+
color: var(--button-text);
422+
border: none;
423+
border-radius: 4px;
424+
padding: 0.4rem 0.75rem;
425+
cursor: pointer;
426+
font-size: 0.9rem;
427+
}
428+
429+
.pagination-btn:hover:not(:disabled) {
430+
background-color: var(--button-hover);
431+
}
432+
433+
.pagination-btn:disabled {
434+
opacity: 0.5;
435+
cursor: not-allowed;
436+
}
437+
438+
.pagination-info {
439+
color: var(--text-secondary);
440+
font-weight: 600;
441+
}
442+
443+
.page-size-controls {
444+
display: flex;
445+
align-items: center;
446+
gap: 0.5rem;
447+
}
448+
449+
.page-size-label {
450+
font-weight: 600;
451+
}
452+
453+
.page-size-input {
454+
width: 6rem;
455+
padding: 0.35rem 0.5rem;
456+
border: 1px solid var(--input-border);
457+
border-radius: 4px;
458+
background-color: var(--bg-primary);
459+
color: var(--text-primary);
460+
}
461+
462+
.page-size-input:focus {
463+
outline: none;
464+
border-color: var(--input-focus);
465+
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
466+
}
467+
399468
/* Table Container */
400469
.table-container {
401470
overflow-x: auto;

docs/url-state.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ function saveStateToURL(state) {
6060
params.set('tagSort', state.tagSort);
6161
}
6262

63+
// Pagination
64+
if (state.page && state.page !== 1) {
65+
params.set('page', String(state.page));
66+
}
67+
68+
if (state.pageSize && state.pageSize !== 100) {
69+
params.set('pageSize', String(state.pageSize));
70+
}
71+
6372
// Update URL without reload using History API
6473
const queryString = params.toString();
6574
const newURL = queryString
@@ -76,6 +85,12 @@ function saveStateToURL(state) {
7685
function loadStateFromURL() {
7786
const params = new URLSearchParams(window.location.search);
7887

88+
const pageRaw = params.get('page') || '1';
89+
const page = /^[1-9]\d*$/.test(pageRaw) ? Number(pageRaw) : 1;
90+
91+
const pageSizeRaw = params.get('pageSize') || '100';
92+
const pageSize = /^[1-9]\d*$/.test(pageSizeRaw) ? Number(pageSizeRaw) : 100;
93+
7994
return {
8095
sortColumn: params.get('sort') || 'number',
8196
sortDirection: params.get('dir') || 'asc',
@@ -86,7 +101,9 @@ function loadStateFromURL() {
86101
oeisFilter: params.get('oeis') || '',
87102
selectedTags: params.get('tags') ? params.get('tags').split(',').filter(tag => tag.trim() !== '') : [],
88103
tagLogic: params.get('tagLogic') || 'any',
89-
tagSort: params.get('tagSort') || 'count'
104+
tagSort: params.get('tagSort') || 'count',
105+
page,
106+
pageSize
90107
};
91108
}
92109

@@ -242,6 +259,8 @@ function getCurrentState() {
242259
oeisFilter: oeisFilter ? oeisFilter.value : '',
243260
selectedTags,
244261
tagLogic,
245-
tagSort
262+
tagSort,
263+
page: (typeof currentPage !== 'undefined' && Number.isFinite(currentPage)) ? currentPage : 1,
264+
pageSize: (typeof pageSize !== 'undefined' && Number.isFinite(pageSize)) ? pageSize : 100
246265
};
247266
}

0 commit comments

Comments
 (0)