Add pagination to search page with configurable items per page

- Added pagination state (currentPage, pageSize) to search.js
- Implemented getPaginatedResults() function to slice filtered results
- Added getTotalPages() and helper functions for pagination math
- Added pagination UI controls to search.html:
  * Items per page dropdown (15, 30, 60 options)
  * Previous/Next navigation buttons
  * Page info display (Page X of Y)
  * Result summary showing item range
- Integrated pagination with search filtering:
  * Resets to page 1 on new search
  * Disables nav buttons at boundaries
  * Updates page info after each action
- Added comprehensive CSS styling for responsive pagination controls
- Updated README with pagination features and usage

Features:
✓ Filter results first, then paginate
✓ Smart button disabling at boundaries
✓ Auto-reset to page 1 on search
✓ Dynamic page count updates
✓ Responsive design on mobile devices

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-09 11:48:21 +00:00
parent ffa007082a
commit bdacf52b28
3 changed files with 213 additions and 6 deletions

View File

@@ -58,10 +58,20 @@ Advanced inventory search and filtering interface.
- Filter by expiry date range (start date and end date)
- Real-time result display with item cards
- Visual expiry status indicators (Fresh, Expiring Soon, Expired)
- **Pagination** — Results displayed with configurable items per page (15, 30, or 60)
- Reset button to clear all filters
- Keyboard support (press Enter to search)
- Date range validation (start date ≤ end date)
**Pagination Features:**
- **Items Per Page Dropdown** — Choose between 15, 30, or 60 items per page
- **Previous/Next Navigation** — Browse through pages of results
- **Page Information** — Shows current page and total pages (e.g., "Page 2 of 5")
- **Result Summary** — Displays showing item count (e.g., "Found 47 items (16-30)")
- **Smart Boundaries** — Previous button disabled on page 1, Next disabled on last page
- **Auto-Reset** — Returns to page 1 when search filters change
- **Dynamic Page Count** — Automatically updates total pages when page size changes
**Expiry Date Features:**
- Each inventory item includes an expiry date
- Visual badges show expiry status:

View File

@@ -128,6 +128,87 @@
background-color: #ffebee;
color: #c62828;
}
.pagination-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin: 30px 0;
flex-wrap: wrap;
}
.pagination-size {
display: flex;
align-items: center;
gap: 10px;
}
.pagination-size label {
font-weight: bold;
color: #333;
}
.pagination-size select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background-color: white;
cursor: pointer;
}
.pagination-size select:focus {
outline: none;
border-color: #4CAF50;
}
.pagination-nav {
display: flex;
align-items: center;
gap: 10px;
}
.pagination-nav button {
padding: 8px 12px;
border: 1px solid #ddd;
background-color: #f5f5f5;
color: #333;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s;
}
.pagination-nav button:hover:not(:disabled) {
background-color: #4CAF50;
color: white;
border-color: #4CAF50;
}
.pagination-nav button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
font-size: 14px;
color: #666;
min-width: 120px;
text-align: center;
}
@media (max-width: 600px) {
.pagination-controls {
flex-direction: column;
gap: 15px;
}
.pagination-size,
.pagination-nav {
justify-content: center;
}
}
</style>
</head>
<body>
@@ -212,13 +293,30 @@
<div id="results-container">
<div class="results-info" id="results-info"></div>
<div class="pagination-controls">
<div class="pagination-size">
<label for="page-size-select">Items per page:</label>
<select id="page-size-select">
<option value="15">15</option>
<option value="30">30</option>
<option value="60">60</option>
</select>
</div>
<div class="pagination-nav">
<button type="button" id="prev-page-btn" class="btn">← Previous</button>
<div class="pagination-info" id="page-info">Page 1 of 1</div>
<button type="button" id="next-page-btn" class="btn">Next →</button>
</div>
</div>
<div id="results-grid"></div>
</div>
</div>
<script src="navbar.js"></script>
<script type="module">
import { searchInventory, getLocations, formatDate, getExpiryStatus } from './search.js';
import { searchInventory, getLocations, formatDate, getExpiryStatus, setPageSize, setCurrentPage, getPaginatedResults } from './search.js';
import createItemComponent from './item-component.js';
const searchForm = document.getElementById('search-form');
@@ -232,6 +330,13 @@
const resetBtn = document.getElementById('reset-btn');
const resultsGrid = document.getElementById('results-grid');
const resultsInfo = document.getElementById('results-info');
const pageSizeSelect = document.getElementById('page-size-select');
const prevPageBtn = document.getElementById('prev-page-btn');
const nextPageBtn = document.getElementById('next-page-btn');
const pageInfo = document.getElementById('page-info');
let lastSearchResults = [];
let currentPage = 1;
// Populate location dropdown
function populateLocations() {
@@ -250,16 +355,22 @@
return `<div class="item-expiry-badge ${badgeClass}">${formatDate(expiryDate)}</div>`;
}
// Display search results
// Display search results with pagination
function displayResults(results) {
lastSearchResults = results;
const paginationData = getPaginatedResults(results);
resultsGrid.innerHTML = '';
if (results.length === 0) {
resultsGrid.innerHTML = '<div class="no-results">No items found matching your search criteria.</div>';
resultsInfo.textContent = 'No results';
pageInfo.textContent = 'Page 1 of 1';
prevPageBtn.disabled = true;
nextPageBtn.disabled = true;
} else {
resultsGrid.className = 'item-component-grid';
results.forEach(item => {
paginationData.items.forEach(item => {
const comp = createItemComponent({
imgSrc: item.img,
imgAlt: item.name,
@@ -269,14 +380,18 @@
editable: false
});
// Add expiry badge
const badge = document.createElement('div');
badge.innerHTML = getExpiryBadgeHtml(item.expiryDate);
comp.appendChild(badge.firstChild);
resultsGrid.appendChild(comp);
});
resultsInfo.textContent = `Found ${results.length} item${results.length !== 1 ? 's' : ''}`;
resultsInfo.textContent = `Found ${results.length} item${results.length !== 1 ? 's' : ''} (showing ${paginationData.startIndex}-${paginationData.endIndex})`;
pageInfo.textContent = `Page ${paginationData.currentPage} of ${paginationData.totalPages}`;
prevPageBtn.disabled = paginationData.currentPage === 1;
nextPageBtn.disabled = paginationData.currentPage === paginationData.totalPages;
}
}
@@ -289,12 +404,13 @@
const minExpiry = minExpiryDateInput.value;
const maxExpiry = maxExpiryDateInput.value;
// Validate date range
if (minExpiry && maxExpiry && minExpiry > maxExpiry) {
alert('Start date cannot be after end date');
return;
}
currentPage = 1;
setCurrentPage(1);
const results = searchInventory(searchName, location, minQty, maxQty, minExpiry, maxExpiry);
displayResults(results);
}
@@ -309,11 +425,35 @@
maxExpiryDateInput.value = '';
resultsGrid.innerHTML = '';
resultsInfo.textContent = '';
currentPage = 1;
setCurrentPage(1);
pageInfo.textContent = 'Page 1 of 1';
pageSizeSelect.value = '15';
}
// Event listeners
searchBtn.addEventListener('click', performSearch);
resetBtn.addEventListener('click', resetForm);
// Pagination event listeners
pageSizeSelect.addEventListener('change', (e) => {
currentPage = 1;
setPageSize(parseInt(e.target.value));
setCurrentPage(1);
displayResults(lastSearchResults);
});
prevPageBtn.addEventListener('click', () => {
currentPage = Math.max(1, currentPage - 1);
setCurrentPage(currentPage);
displayResults(lastSearchResults);
});
nextPageBtn.addEventListener('click', () => {
currentPage++;
setCurrentPage(currentPage);
displayResults(lastSearchResults);
});
// Allow Enter key to search
searchNameInput.addEventListener('keypress', (e) => {

View File

@@ -1,5 +1,9 @@
// search.js - Search module with inventory data and filtering logic
// Pagination state
export let currentPage = 1;
export let pageSize = 15;
// Helper function to create a date string
function getDate(daysFromNow) {
const date = new Date();
@@ -116,3 +120,56 @@ export function getLocations() {
const locations = [...new Set(inventoryData.map(item => item.location))];
return ['All', ...locations.sort()];
}
/**
* Set the page size for pagination
* @param {number} size - Items per page (15, 30, or 60)
*/
export function setPageSize(size) {
pageSize = size;
currentPage = 1;
}
/**
* Set the current page
* @param {number} page - Page number
*/
export function setCurrentPage(page) {
currentPage = page;
}
/**
* Get total number of pages
* @param {number} totalItems - Total items to paginate
* @returns {number} Number of pages
*/
export function getTotalPages(totalItems) {
return Math.ceil(totalItems / pageSize);
}
/**
* Get paginated results from filtered items
* @param {Array} filteredItems - Filtered inventory items
* @returns {Object} Object with paginated items and pagination info
*/
export function getPaginatedResults(filteredItems) {
const totalItems = filteredItems.length;
const totalPages = getTotalPages(totalItems);
if (currentPage < 1) currentPage = 1;
if (currentPage > totalPages && totalPages > 0) currentPage = totalPages;
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedItems = filteredItems.slice(startIndex, endIndex);
return {
items: paginatedItems,
currentPage,
pageSize,
totalPages,
totalItems,
startIndex: totalItems > 0 ? startIndex + 1 : 0,
endIndex: Math.min(endIndex, totalItems)
};
}