Add product pagination and enable BestBuy

Implement client-side pagination for the products grid: add PRODUCTS_PER_PAGE, currentProductPage, offset in /products requests, renderPagination(), and changeProductPage() (with smooth scroll). Reset page when filters or event filters change and render empty-state pagination. Add pagination container to the template and styles for pagination buttons and info. Also enable "bestbuy" in SITES_ENABLED in config.py and add a taskkill line to .claude/settings.local.json for closing stray python.exe processes.
This commit is contained in:
2026-03-28 16:57:03 -04:00
parent f01bde54a5
commit d33639fdc7
5 changed files with 79 additions and 3 deletions
+2 -1
View File
@@ -14,7 +14,8 @@
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(''''Last sync:'''', d[''''last_sync'''']\\); print\\(''''Products:'''', len\\(d[''''products'''']\\)\\); print\\(''''Sample names:''''\\); [print\\('''' -'''', p[''''name''''][:80]\\) for p in d[''''products''''][:5]]\")", "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(''''Last sync:'''', d[''''last_sync'''']\\); print\\(''''Products:'''', len\\(d[''''products'''']\\)\\); print\\(''''Sample names:''''\\); [print\\('''' -'''', p[''''name''''][:80]\\) for p in d[''''products''''][:5]]\")",
"Bash(PYTHON=\"/c/Users/hocke/Documents/GitHub/pokemon-stock-checker/.venv/Scripts/python.exe\")", "Bash(PYTHON=\"/c/Users/hocke/Documents/GitHub/pokemon-stock-checker/.venv/Scripts/python.exe\")",
"Bash(\"$PYTHON\" -c \":*)", "Bash(\"$PYTHON\" -c \":*)",
"Bash(1 grep:*)" "Bash(1 grep:*)",
"Bash(taskkill /F /IM python.exe)"
] ]
} }
} }
+1 -1
View File
@@ -36,7 +36,7 @@ SORT_BY_NEWEST = True
# Which sites to monitor # Which sites to monitor
# Note: PokemonCenter has strong bot protection (Imperva) - may need manual workarounds # Note: PokemonCenter has strong bot protection (Imperva) - may need manual workarounds
# Note: Walmart has aggressive bot protection (PerimeterX) - may need stealth mode # Note: Walmart has aggressive bot protection (PerimeterX) - may need stealth mode
SITES_ENABLED = ["target", "gamestop"] # Options: "pokemoncenter", "target", "gamestop", "bestbuy" (disabled - needs rework), "walmart" SITES_ENABLED = ["target", "gamestop", "bestbuy"] # Options: "pokemoncenter", "target", "gamestop", "bestbuy" (disabled - needs rework), "walmart"
# PokemonCenter URLs to monitor # PokemonCenter URLs to monitor
POKEMON_CENTER_URLS = [ POKEMON_CENTER_URLS = [
+39 -1
View File
@@ -6,6 +6,8 @@ const API_BASE = '/api';
let currentPage = 'dashboard'; let currentPage = 'dashboard';
let charts = {}; let charts = {};
let currentEventFilter = null; // { type: 'new_drop'|'restock', period: 'today'|'week' } let currentEventFilter = null; // { type: 'new_drop'|'restock', period: 'today'|'week' }
let currentProductPage = 1;
const PRODUCTS_PER_PAGE = 50;
// Initialize // Initialize
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -81,6 +83,7 @@ function initEventListeners() {
if (el) { if (el) {
el.addEventListener('change', () => { el.addEventListener('change', () => {
currentEventFilter = null; // Clear event filter when using regular filters currentEventFilter = null; // Clear event filter when using regular filters
currentProductPage = 1;
loadProducts(); loadProducts();
}); });
} }
@@ -106,6 +109,7 @@ function clearAllFilters() {
document.getElementById('filterStock').value = ''; document.getElementById('filterStock').value = '';
document.getElementById('filterFavorites').checked = false; document.getElementById('filterFavorites').checked = false;
currentEventFilter = null; currentEventFilter = null;
currentProductPage = 1;
loadProducts(); loadProducts();
} }
@@ -296,12 +300,14 @@ async function loadActivityFeed() {
// Navigate to products filtered by event type // Navigate to products filtered by event type
function goToFilteredProducts(eventType, period) { function goToFilteredProducts(eventType, period) {
currentEventFilter = { type: eventType, period: period }; currentEventFilter = { type: eventType, period: period };
currentProductPage = 1;
navigateTo('products'); navigateTo('products');
} }
// Clear event filter // Clear event filter
function clearEventFilter() { function clearEventFilter() {
currentEventFilter = null; currentEventFilter = null;
currentProductPage = 1;
loadProducts(); loadProducts();
} }
@@ -311,8 +317,9 @@ async function loadProducts() {
const category = document.getElementById('filterCategory')?.value || ''; const category = document.getElementById('filterCategory')?.value || '';
const inStock = document.getElementById('filterStock')?.value || ''; const inStock = document.getElementById('filterStock')?.value || '';
const favoritesOnly = document.getElementById('filterFavorites')?.checked || false; const favoritesOnly = document.getElementById('filterFavorites')?.checked || false;
const offset = (currentProductPage - 1) * PRODUCTS_PER_PAGE;
let url = '/products?limit=50'; let url = `/products?limit=${PRODUCTS_PER_PAGE}&offset=${offset}`;
if (site) url += `&site=${site}`; if (site) url += `&site=${site}`;
if (category) url += `&category=${encodeURIComponent(category)}`; if (category) url += `&category=${encodeURIComponent(category)}`;
if (inStock) url += `&in_stock=${inStock}`; if (inStock) url += `&in_stock=${inStock}`;
@@ -333,6 +340,7 @@ async function loadProducts() {
if (data.products.length === 0) { if (data.products.length === 0) {
grid.innerHTML = '<div class="loading">No products found</div>'; grid.innerHTML = '<div class="loading">No products found</div>';
renderPagination(0);
return; return;
} }
@@ -376,6 +384,36 @@ async function loadProducts() {
</div> </div>
`; `;
}).join(''); }).join('');
renderPagination(data.total || 0);
}
function renderPagination(total) {
const container = document.getElementById('productsPagination');
if (!container) return;
const totalPages = Math.ceil(total / PRODUCTS_PER_PAGE);
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
const start = (currentProductPage - 1) * PRODUCTS_PER_PAGE + 1;
const end = Math.min(currentProductPage * PRODUCTS_PER_PAGE, total);
container.innerHTML = `
<div class="pagination">
<button class="btn-page" onclick="changeProductPage(${currentProductPage - 1})" ${currentProductPage === 1 ? 'disabled' : ''}>&laquo; Prev</button>
<span class="page-info">Page ${currentProductPage} of ${totalPages} &nbsp;(${start}${end} of ${total})</span>
<button class="btn-page" onclick="changeProductPage(${currentProductPage + 1})" ${currentProductPage === totalPages ? 'disabled' : ''}>Next &raquo;</button>
</div>
`;
}
function changeProductPage(page) {
currentProductPage = page;
loadProducts();
document.getElementById('page-products').scrollIntoView({ behavior: 'smooth' });
} }
// Handle product image errors by showing store icon // Handle product image errors by showing store icon
+36
View File
@@ -859,6 +859,42 @@ h3 {
background: var(--border-color); background: var(--border-color);
} }
/* Pagination */
#productsPagination {
margin-top: 20px;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.btn-page {
padding: 8px 18px;
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.btn-page:hover:not(:disabled) {
background: var(--border-color);
}
.btn-page:disabled {
opacity: 0.4;
cursor: default;
}
.page-info {
color: var(--text-secondary);
font-size: 14px;
}
/* Settings */ /* Settings */
.settings-section { .settings-section {
max-width: 600px; max-width: 600px;
+1
View File
@@ -110,6 +110,7 @@
<div class="products-grid" id="productsGrid"> <div class="products-grid" id="productsGrid">
<div class="loading">Loading products...</div> <div class="loading">Loading products...</div>
</div> </div>
<div id="productsPagination"></div>
</section> </section>
<!-- Analytics Page --> <!-- Analytics Page -->