Add client-side spell column sorting and improve inventory search

- Implement client-side sorting for all columns including spell_names
- Add computed_spell_names CTE for server-side sort fallback
- Add resizable columns with localStorage persistence
- Add Cloak slot detection by name pattern
- Increase items limit to 50000 for full inventory loading
- Increase proxy timeout to 60s for large queries
- Remove pagination (all items loaded at once for sorting)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
erik 2026-01-28 15:32:54 +00:00
parent 25980edf99
commit 8cae94d87d
6 changed files with 294 additions and 111 deletions

View file

@ -265,6 +265,7 @@
border-collapse: collapse;
font-size: 10px;
color: #000;
table-layout: fixed;
}
.results-table th {
@ -276,11 +277,36 @@
position: sticky;
top: 0;
font-size: 9px;
position: relative;
user-select: none;
}
/* Resizable column handle */
.results-table th .resize-handle {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
background: transparent;
}
.results-table th .resize-handle:hover,
.results-table th .resize-handle.resizing {
background: #666;
}
.results-table th.resizing {
background: #b0b0b0;
}
.results-table td {
padding: 1px 3px;
border: 1px solid #ddd;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.results-table tbody tr:hover {
@ -542,6 +568,9 @@
<option value="Right Wrist">Right Wrist</option>
<option value="Trinket">Trinket</option>
</optgroup>
<optgroup label="Other Slots">
<option value="Cloak">Cloak</option>
</optgroup>
</select>
</div>
</div>
@ -944,6 +973,10 @@
<input type="checkbox" id="slot_trinket" value="Trinket">
<label for="slot_trinket">Trinket</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_cloak" value="Cloak">
<label for="slot_cloak">Cloak</label>
</div>
</div>
</div>
</div>

View file

@ -20,10 +20,9 @@ let currentSort = {
// Store current search results for client-side sorting
let currentResultsData = null;
// Pagination state
// Items limit - load all items for client-side sorting (backend max is 50000)
let currentPage = 1;
let itemsPerPage = 5000; // 5k items per page for good performance
let totalPages = 1;
let itemsPerPage = 50000;
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
@ -44,7 +43,7 @@ function initializeEventListeners() {
// Form submission
searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
await performSearch(true); // Reset to page 1 on new search
await performSearch();
});
// Clear button
@ -168,14 +167,13 @@ function clearAllFields() {
// Reset equipment type to armor
document.getElementById('armorOnly').checked = true;
// Reset slot filter
document.getElementById('slotFilter').value = '';
// Reset pagination
// Reset page
currentPage = 1;
totalPages = 1;
// Reset results and clear stored data
currentResultsData = null;
searchResults.innerHTML = '<div class="no-results">Enter search criteria above and click "Search Items" to find inventory items.</div>';
@ -205,12 +203,8 @@ function handleSlotFilterChange() {
/**
* Perform the search based on form inputs
*/
async function performSearch(resetPage = false) {
// Reset to page 1 if this is a new search (not pagination)
if (resetPage) {
currentPage = 1;
}
async function performSearch() {
currentPage = 1;
searchResults.innerHTML = '<div class="loading">🔍 Searching inventory...</div>';
try {
@ -227,10 +221,7 @@ async function performSearch(resetPage = false) {
// Store results for client-side re-sorting
currentResultsData = data;
// Update pagination state
updatePaginationState(data);
// Apply client-side slot filtering
applySlotFilter(data);
@ -396,12 +387,8 @@ function getSelectedProtections() {
*/
function getSelectedSlots() {
const selectedSlots = [];
// Get armor slots
document.querySelectorAll('#armor-slots input[type="checkbox"]:checked').forEach(cb => {
selectedSlots.push(cb.value);
});
// Get jewelry slots
document.querySelectorAll('#jewelry-slots input[type="checkbox"]:checked').forEach(cb => {
// Get all selected slot checkboxes from the all-slots container
document.querySelectorAll('#all-slots input[type="checkbox"]:checked').forEach(cb => {
selectedSlots.push(cb.value);
});
return selectedSlots;
@ -505,12 +492,16 @@ function displayResults(data) {
// Get item type for display
const itemType = item.item_type_name || '-';
// Format equipment set name
// Format equipment set name - prefer translated name
let setDisplay = '-';
if (item.item_set) {
// Remove redundant "Set" prefix if present
if (item.item_set_name) {
// Use the translated set name from backend
setDisplay = item.item_set_name;
// Remove redundant "Set" suffix if present for cleaner display
setDisplay = setDisplay.replace(/\s+Set$/i, '');
} else if (item.item_set) {
// Fallback to raw set ID if name not available
setDisplay = item.item_set.replace(/^Set\s+/i, '');
// Also handle if it ends with " Set"
setDisplay = setDisplay.replace(/\s+Set$/i, '');
}
@ -540,24 +531,8 @@ function displayResults(data) {
</table>
`;
// Add pagination controls if needed
if (totalPages > 1) {
const isFirstPage = currentPage === 1;
const isLastPage = currentPage === totalPages;
html += `
<div class="pagination-controls">
<button onclick="goToPage(1)" ${isFirstPage ? 'disabled' : ''}>First</button>
<button onclick="previousPage()" ${isFirstPage ? 'disabled' : ''}> Previous</button>
<span class="pagination-info">Page ${currentPage} of ${totalPages} (${data.total_count} total items)</span>
<button onclick="nextPage()" ${isLastPage ? 'disabled' : ''}>Next </button>
<button onclick="goToPage(${totalPages})" ${isLastPage ? 'disabled' : ''}>Last</button>
</div>
`;
}
searchResults.innerHTML = html;
// Add click event listeners to sortable headers
document.querySelectorAll('.sortable').forEach(header => {
header.addEventListener('click', () => {
@ -565,6 +540,98 @@ function displayResults(data) {
handleSort(sortField);
});
});
// Initialize resizable columns
initResizableColumns();
}
/**
* Initialize resizable columns for the results table
*/
function initResizableColumns() {
const table = document.querySelector('.results-table');
if (!table) return;
const headers = table.querySelectorAll('th');
headers.forEach((header, index) => {
// Skip last column (no need to resize)
if (index === headers.length - 1) return;
// Create resize handle
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
header.appendChild(resizeHandle);
let startX, startWidth;
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation(); // Prevent sorting when resizing
startX = e.pageX;
startWidth = header.offsetWidth;
header.classList.add('resizing');
resizeHandle.classList.add('resizing');
const onMouseMove = (e) => {
const diff = e.pageX - startX;
const newWidth = Math.max(30, startWidth + diff); // Minimum 30px width
header.style.width = newWidth + 'px';
header.style.minWidth = newWidth + 'px';
};
const onMouseUp = () => {
header.classList.remove('resizing');
resizeHandle.classList.remove('resizing');
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// Save column widths to localStorage
saveColumnWidths();
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
});
// Restore saved column widths
restoreColumnWidths();
}
/**
* Save column widths to localStorage
*/
function saveColumnWidths() {
const headers = document.querySelectorAll('.results-table th');
const widths = [];
headers.forEach(header => {
widths.push(header.style.width || '');
});
localStorage.setItem('inventoryColumnWidths', JSON.stringify(widths));
}
/**
* Restore column widths from localStorage
*/
function restoreColumnWidths() {
const saved = localStorage.getItem('inventoryColumnWidths');
if (!saved) return;
try {
const widths = JSON.parse(saved);
const headers = document.querySelectorAll('.results-table th');
headers.forEach((header, index) => {
if (widths[index]) {
header.style.width = widths[index];
header.style.minWidth = widths[index];
}
});
} catch (e) {
console.error('Failed to restore column widths:', e);
}
}
/**
* Apply client-side slot filtering
@ -636,7 +703,7 @@ function sortResults(data) {
}
/**
* Handle column sorting
* Handle column sorting - client-side sorting for all columns
*/
function handleSort(field) {
// If clicking the same field, toggle direction
@ -647,10 +714,17 @@ function handleSort(field) {
currentSort.field = field;
currentSort.direction = 'asc';
}
// Reset to page 1 and perform new search with updated sort
currentPage = 1;
performSearch();
// If we have cached results, sort them client-side and re-display
if (currentResultsData && currentResultsData.items) {
// Make a copy to preserve original order
const sortedData = JSON.parse(JSON.stringify(currentResultsData));
sortResults(sortedData);
displayResults(sortedData);
} else {
// No cached results, need to fetch first
performSearch();
}
}
/**
@ -794,40 +868,4 @@ function displaySetAnalysisResults(data) {
setAnalysisResults.innerHTML = html;
}
/**
* Update pagination state from API response
*/
function updatePaginationState(data) {
totalPages = data.total_pages || 1;
// Current page is already tracked in currentPage
}
/**
* Go to a specific page
*/
function goToPage(page) {
if (page < 1 || page > totalPages || page === currentPage) {
return;
}
currentPage = page;
performSearch();
}
/**
* Go to next page
*/
function nextPage() {
if (currentPage < totalPages) {
goToPage(currentPage + 1);
}
}
/**
* Go to previous page
*/
function previousPage() {
if (currentPage > 1) {
goToPage(currentPage - 1);
}
}
// Pagination functions removed - using client-side sorting with all results loaded