Commit 5fbd21e1 authored by Your Name's avatar Your Name

feat: add AJAX functionality for search, sorting, and pagination

- Debounced search input with clear functionality
- Clickable sortable headers with direction indicators
- Pagination controls with previous/next buttons and page size selector
- URL state management for bookmarking
- Loading states and error handling
parent 0a970415
...@@ -164,7 +164,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -164,7 +164,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<!-- Pagination Controls --> <!-- Pagination Controls -->
{% if pagination.total_pages > 1 %} {% if pagination.total_pages > 1 %}
<div style="margin-top: 20px; padding: 15px; background: #0f3460; border-radius: 8px;"> <div id="pagination-controls" style="margin-top: 20px; padding: 15px; background: #0f3460; border-radius: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;"> <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
<div style="color: #e0e0e0;"> <div style="color: #e0e0e0;">
Showing {{ pagination.start_item }}-{{ pagination.end_item }} of {{ pagination.total_users }} users Showing {{ pagination.start_item }}-{{ pagination.end_item }} of {{ pagination.total_users }} users
...@@ -237,7 +237,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -237,7 +237,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
<script> <script>
// Search functionality // Search functionality with AJAX
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn'); const searchBtn = document.getElementById('search-btn');
...@@ -246,6 +246,8 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -246,6 +246,8 @@ document.addEventListener('DOMContentLoaded', function() {
const prevBtn = document.getElementById('prev-btn'); const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn'); const nextBtn = document.getElementById('next-btn');
let searchTimeout = null;
// Get current URL parameters // Get current URL parameters
function getUrlParams() { function getUrlParams() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
...@@ -258,94 +260,158 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -258,94 +260,158 @@ document.addEventListener('DOMContentLoaded', function() {
}; };
} }
// Update URL with new parameters // Main function to update users list via AJAX
function updateUrl(params) { function updateUsers(params) {
const url = new URL(window.location.href); // Show loading state
const tableContainer = document.querySelector('table').parentElement;
const originalContent = tableContainer.innerHTML;
tableContainer.innerHTML = '<div style="text-align: center; padding: 20px; color: #e0e0e0;"><div class="spinner"></div> Loading...</div>';
// Build query parameters
const currentParams = new URLSearchParams(window.location.search);
Object.keys(params).forEach(key => { Object.keys(params).forEach(key => {
if (params[key]) { if (params[key] !== '' && params[key] !== null && params[key] !== undefined) {
url.searchParams.set(key, params[key]); currentParams.set(key, params[key]);
} else { } else {
url.searchParams.delete(key); currentParams.delete(key);
}
});
// Reset to page 1 if search/sort changed
if (params.search !== undefined || params.order_by !== undefined) {
currentParams.set('page', '1');
}
// Update URL without reload
const newURL = window.location.pathname + (currentParams.toString() ? '?' + currentParams.toString() : '');
window.history.pushState({}, '', newURL);
// Fetch updated content
fetch(newURL, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.text())
.then(html => {
// Parse and update just the table and pagination
const parser = new DOMParser();
const newDoc = parser.parseFromString(html, 'text/html');
const newTable = newDoc.querySelector('table');
const newPagination = newDoc.getElementById('pagination-controls');
tableContainer.innerHTML = originalContent;
document.querySelector('table').replaceWith(newTable);
const existingPagination = document.getElementById('pagination-controls');
if (existingPagination && newPagination) {
existingPagination.replaceWith(newPagination);
} else if (newPagination) {
document.querySelector('table').after(newPagination);
} }
// Re-attach event listeners to new elements
attachPaginationListeners();
attachSortingListeners();
})
.catch(error => {
console.error('Error updating users:', error);
tableContainer.innerHTML = originalContent;
showNotification('Error loading users', 'error');
}); });
window.location.href = url.toString();
} }
// Search button click // Attach pagination event listeners
if (searchBtn) { function attachPaginationListeners() {
searchBtn.addEventListener('click', function() { const newPageSize = document.getElementById('page-size');
const params = getUrlParams(); const newPrevBtn = document.getElementById('prev-btn');
params.search = searchInput.value.trim(); const newNextBtn = document.getElementById('next-btn');
params.page = 1; // Reset to first page on new search
updateUrl(params); if (newPageSize) {
newPageSize.addEventListener('change', function() {
updateUsers({ limit: this.value, page: 1 });
});
}
if (newPrevBtn) {
newPrevBtn.addEventListener('click', function() {
const params = getUrlParams();
updateUsers({ page: Math.max(1, params.page - 1) });
});
}
if (newNextBtn) {
newNextBtn.addEventListener('click', function() {
const params = getUrlParams();
updateUsers({ page: params.page + 1 });
});
}
}
// Attach sorting event listeners
function attachSortingListeners() {
document.querySelectorAll('.sortable').forEach(header => {
header.style.cursor = 'pointer';
header.addEventListener('click', function() {
const column = this.getAttribute('data-column');
const params = getUrlParams();
// Toggle direction if clicking same column, otherwise default to desc
if (params.order_by === column) {
params.direction = params.direction === 'asc' ? 'desc' : 'asc';
} else {
params.order_by = column;
params.direction = 'desc';
}
updateUsers({ order_by: params.order_by, direction: params.direction, page: 1 });
});
}); });
} }
// Search on Enter key // Debounced search input
if (searchInput) { if (searchInput) {
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
updateUsers({ search: searchInput.value.trim(), page: 1 });
}, 300);
});
// Search on Enter key (immediate)
searchInput.addEventListener('keypress', function(e) { searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
searchBtn.click(); clearTimeout(searchTimeout);
updateUsers({ search: searchInput.value.trim(), page: 1 });
} }
}); });
} }
// Clear button click // Search button click
if (clearBtn) { if (searchBtn) {
clearBtn.addEventListener('click', function() { searchBtn.addEventListener('click', function() {
const params = getUrlParams(); clearTimeout(searchTimeout);
params.search = ''; if (searchInput) {
params.page = 1; updateUsers({ search: searchInput.value.trim(), page: 1 });
updateUrl(params); }
}); });
} }
// Sortable headers // Clear button click
document.querySelectorAll('.sortable').forEach(header => { if (clearBtn) {
header.style.cursor = 'pointer'; clearBtn.addEventListener('click', function() {
header.addEventListener('click', function() { if (searchInput) {
const column = this.getAttribute('data-column'); searchInput.value = '';
const params = getUrlParams(); updateUsers({ search: '', page: 1 });
// Toggle direction if clicking same column, otherwise default to desc
if (params.order_by === column) {
params.direction = params.direction === 'asc' ? 'desc' : 'asc';
} else {
params.order_by = column;
params.direction = 'desc';
} }
params.page = 1; // Reset to first page on sort
updateUrl(params);
});
});
// Page size change
if (pageSize) {
pageSize.addEventListener('change', function() {
const params = getUrlParams();
params.limit = this.value;
params.page = 1; // Reset to first page on limit change
updateUrl(params);
}); });
} }
// Previous page button // Initial sorting listeners
if (prevBtn) { attachSortingListeners();
prevBtn.addEventListener('click', function() {
const params = getUrlParams();
params.page = Math.max(1, params.page - 1);
updateUrl(params);
});
}
// Next page button // Initial pagination listeners
if (nextBtn) { attachPaginationListeners();
nextBtn.addEventListener('click', function() {
const params = getUrlParams();
params.page = params.page + 1;
updateUrl(params);
});
}
}); });
function editUser(userId, username, email, role, isActive) { function editUser(userId, username, email, role, isActive) {
...@@ -507,6 +573,24 @@ th { ...@@ -507,6 +573,24 @@ th {
color: #16c79a; color: #16c79a;
} }
/* Loading spinner */
.spinner {
border: 3px solid #0f3460;
border-top: 3px solid #4ade80;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 10px;
vertical-align: middle;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Notification animations */ /* Notification animations */
@keyframes slideIn { @keyframes slideIn {
from { from {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment