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:
10
README.md
10
README.md
@@ -58,10 +58,20 @@ Advanced inventory search and filtering interface.
|
|||||||
- Filter by expiry date range (start date and end date)
|
- Filter by expiry date range (start date and end date)
|
||||||
- Real-time result display with item cards
|
- Real-time result display with item cards
|
||||||
- Visual expiry status indicators (Fresh, Expiring Soon, Expired)
|
- 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
|
- Reset button to clear all filters
|
||||||
- Keyboard support (press Enter to search)
|
- Keyboard support (press Enter to search)
|
||||||
- Date range validation (start date ≤ end date)
|
- 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:**
|
**Expiry Date Features:**
|
||||||
- Each inventory item includes an expiry date
|
- Each inventory item includes an expiry date
|
||||||
- Visual badges show expiry status:
|
- Visual badges show expiry status:
|
||||||
|
|||||||
152
search.html
152
search.html
@@ -128,6 +128,87 @@
|
|||||||
background-color: #ffebee;
|
background-color: #ffebee;
|
||||||
color: #c62828;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -212,13 +293,30 @@
|
|||||||
|
|
||||||
<div id="results-container">
|
<div id="results-container">
|
||||||
<div class="results-info" id="results-info"></div>
|
<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 id="results-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="navbar.js"></script>
|
<script src="navbar.js"></script>
|
||||||
<script type="module">
|
<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';
|
import createItemComponent from './item-component.js';
|
||||||
|
|
||||||
const searchForm = document.getElementById('search-form');
|
const searchForm = document.getElementById('search-form');
|
||||||
@@ -232,6 +330,13 @@
|
|||||||
const resetBtn = document.getElementById('reset-btn');
|
const resetBtn = document.getElementById('reset-btn');
|
||||||
const resultsGrid = document.getElementById('results-grid');
|
const resultsGrid = document.getElementById('results-grid');
|
||||||
const resultsInfo = document.getElementById('results-info');
|
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
|
// Populate location dropdown
|
||||||
function populateLocations() {
|
function populateLocations() {
|
||||||
@@ -250,16 +355,22 @@
|
|||||||
return `<div class="item-expiry-badge ${badgeClass}">${formatDate(expiryDate)}</div>`;
|
return `<div class="item-expiry-badge ${badgeClass}">${formatDate(expiryDate)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display search results
|
// Display search results with pagination
|
||||||
function displayResults(results) {
|
function displayResults(results) {
|
||||||
|
lastSearchResults = results;
|
||||||
|
const paginationData = getPaginatedResults(results);
|
||||||
|
|
||||||
resultsGrid.innerHTML = '';
|
resultsGrid.innerHTML = '';
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
resultsGrid.innerHTML = '<div class="no-results">No items found matching your search criteria.</div>';
|
resultsGrid.innerHTML = '<div class="no-results">No items found matching your search criteria.</div>';
|
||||||
resultsInfo.textContent = 'No results';
|
resultsInfo.textContent = 'No results';
|
||||||
|
pageInfo.textContent = 'Page 1 of 1';
|
||||||
|
prevPageBtn.disabled = true;
|
||||||
|
nextPageBtn.disabled = true;
|
||||||
} else {
|
} else {
|
||||||
resultsGrid.className = 'item-component-grid';
|
resultsGrid.className = 'item-component-grid';
|
||||||
results.forEach(item => {
|
paginationData.items.forEach(item => {
|
||||||
const comp = createItemComponent({
|
const comp = createItemComponent({
|
||||||
imgSrc: item.img,
|
imgSrc: item.img,
|
||||||
imgAlt: item.name,
|
imgAlt: item.name,
|
||||||
@@ -269,14 +380,18 @@
|
|||||||
editable: false
|
editable: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add expiry badge
|
|
||||||
const badge = document.createElement('div');
|
const badge = document.createElement('div');
|
||||||
badge.innerHTML = getExpiryBadgeHtml(item.expiryDate);
|
badge.innerHTML = getExpiryBadgeHtml(item.expiryDate);
|
||||||
comp.appendChild(badge.firstChild);
|
comp.appendChild(badge.firstChild);
|
||||||
|
|
||||||
resultsGrid.appendChild(comp);
|
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 minExpiry = minExpiryDateInput.value;
|
||||||
const maxExpiry = maxExpiryDateInput.value;
|
const maxExpiry = maxExpiryDateInput.value;
|
||||||
|
|
||||||
// Validate date range
|
|
||||||
if (minExpiry && maxExpiry && minExpiry > maxExpiry) {
|
if (minExpiry && maxExpiry && minExpiry > maxExpiry) {
|
||||||
alert('Start date cannot be after end date');
|
alert('Start date cannot be after end date');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPage = 1;
|
||||||
|
setCurrentPage(1);
|
||||||
const results = searchInventory(searchName, location, minQty, maxQty, minExpiry, maxExpiry);
|
const results = searchInventory(searchName, location, minQty, maxQty, minExpiry, maxExpiry);
|
||||||
displayResults(results);
|
displayResults(results);
|
||||||
}
|
}
|
||||||
@@ -309,12 +425,36 @@
|
|||||||
maxExpiryDateInput.value = '';
|
maxExpiryDateInput.value = '';
|
||||||
resultsGrid.innerHTML = '';
|
resultsGrid.innerHTML = '';
|
||||||
resultsInfo.textContent = '';
|
resultsInfo.textContent = '';
|
||||||
|
currentPage = 1;
|
||||||
|
setCurrentPage(1);
|
||||||
|
pageInfo.textContent = 'Page 1 of 1';
|
||||||
|
pageSizeSelect.value = '15';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
searchBtn.addEventListener('click', performSearch);
|
searchBtn.addEventListener('click', performSearch);
|
||||||
resetBtn.addEventListener('click', resetForm);
|
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
|
// Allow Enter key to search
|
||||||
searchNameInput.addEventListener('keypress', (e) => {
|
searchNameInput.addEventListener('keypress', (e) => {
|
||||||
if (e.key === 'Enter') performSearch();
|
if (e.key === 'Enter') performSearch();
|
||||||
|
|||||||
57
search.js
57
search.js
@@ -1,5 +1,9 @@
|
|||||||
// search.js - Search module with inventory data and filtering logic
|
// 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
|
// Helper function to create a date string
|
||||||
function getDate(daysFromNow) {
|
function getDate(daysFromNow) {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
@@ -116,3 +120,56 @@ export function getLocations() {
|
|||||||
const locations = [...new Set(inventoryData.map(item => item.location))];
|
const locations = [...new Set(inventoryData.map(item => item.location))];
|
||||||
return ['All', ...locations.sort()];
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user