feat: Add clear filters functionality and improve product filter options in dashboard
This commit is contained in:
@@ -65,6 +65,13 @@ def get_stats():
|
|||||||
"""Get dashboard stats summary"""
|
"""Get dashboard stats summary"""
|
||||||
db = get_database()
|
db = get_database()
|
||||||
stats = db.get_dashboard_stats()
|
stats = db.get_dashboard_stats()
|
||||||
|
|
||||||
|
# If no check in database, fall back to scraper state's last_check
|
||||||
|
if not stats.get('last_check'):
|
||||||
|
state = scraper_state.get_state()
|
||||||
|
if state.get('last_check'):
|
||||||
|
stats['last_check'] = {'checked_at': state['last_check']}
|
||||||
|
|
||||||
return jsonify(stats)
|
return jsonify(stats)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -15,7 +15,12 @@ from flask_cors import CORS
|
|||||||
|
|
||||||
from src.database import get_database
|
from src.database import get_database
|
||||||
from src.favorites import get_favorites_manager
|
from src.favorites import get_favorites_manager
|
||||||
from .api import api_bp
|
|
||||||
|
# Handle both direct execution and package import
|
||||||
|
try:
|
||||||
|
from .api import api_bp
|
||||||
|
except ImportError:
|
||||||
|
from api import api_bp
|
||||||
|
|
||||||
# Create Flask app
|
# Create Flask app
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|||||||
+43
-12
@@ -86,6 +86,9 @@ function initEventListeners() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear filters button
|
||||||
|
document.getElementById('clearFiltersBtn')?.addEventListener('click', clearAllFilters);
|
||||||
|
|
||||||
// Add favorite button
|
// Add favorite button
|
||||||
document.getElementById('addFavoriteBtn')?.addEventListener('click', addFavorite);
|
document.getElementById('addFavoriteBtn')?.addEventListener('click', addFavorite);
|
||||||
|
|
||||||
@@ -96,6 +99,16 @@ function initEventListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear all product filters
|
||||||
|
function clearAllFilters() {
|
||||||
|
document.getElementById('filterSite').value = '';
|
||||||
|
document.getElementById('filterCategory').value = '';
|
||||||
|
document.getElementById('filterStock').value = '';
|
||||||
|
document.getElementById('filterFavorites').checked = false;
|
||||||
|
currentEventFilter = null;
|
||||||
|
loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
// API Helpers
|
// API Helpers
|
||||||
async function api(endpoint, options = {}) {
|
async function api(endpoint, options = {}) {
|
||||||
try {
|
try {
|
||||||
@@ -328,23 +341,23 @@ async function loadProducts() {
|
|||||||
const cleanName = cleanProductName(product.name);
|
const cleanName = cleanProductName(product.name);
|
||||||
const hasImage = product.image_url && !product.image_url.includes('data:') && product.image_url.length > 10;
|
const hasImage = product.image_url && !product.image_url.includes('data:') && product.image_url.length > 10;
|
||||||
|
|
||||||
// Build store logo fallback HTML
|
|
||||||
const storeLogoHtml = `
|
|
||||||
<div class="product-store-icon" style="background:${storeInfo.bgColor};">
|
|
||||||
<img src="${storeInfo.logo}" alt="${storeInfo.name}" class="product-store-logo"
|
|
||||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
|
||||||
<span class="product-store-fallback" style="display:none; color:${storeInfo.color};">${storeInfo.fallback}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="product-card ${product.in_stock ? 'in-stock' : 'out-of-stock'} ${product.is_favorite ? 'favorite' : ''}"
|
<div class="product-card ${product.in_stock ? 'in-stock' : 'out-of-stock'} ${product.is_favorite ? 'favorite' : ''}"
|
||||||
onclick="showProductDetails(${product.id})">
|
onclick="showProductDetails(${product.id})">
|
||||||
<div class="product-image-container">
|
<div class="product-image-container">
|
||||||
${hasImage
|
${hasImage
|
||||||
? `<img src="${product.image_url}" class="product-image" alt="" onerror="this.outerHTML=\`${storeLogoHtml.replace(/`/g, '\\`').replace(/\n/g, '')}\`">`
|
? `<img src="${product.image_url}" class="product-image" alt="" onerror="handleProductImageError(this, '${product.site}')">`
|
||||||
: storeLogoHtml
|
: `<div class="product-store-icon" style="background:${storeInfo.bgColor};">
|
||||||
|
<img src="${storeInfo.logo}" alt="${storeInfo.name}" class="product-store-logo"
|
||||||
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||||
|
<span class="product-store-fallback" style="display:none; color:${storeInfo.color};">${storeInfo.fallback}</span>
|
||||||
|
</div>`
|
||||||
}
|
}
|
||||||
|
<div class="product-store-badge" style="background:${storeInfo.bgColor};">
|
||||||
|
<img src="${storeInfo.logo}" alt="${storeInfo.name}" class="store-badge-logo"
|
||||||
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||||
|
<span class="store-badge-fallback" style="display:none; color:${storeInfo.color};">${storeInfo.fallback}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-name">${cleanName}</div>
|
<div class="product-name">${cleanName}</div>
|
||||||
<div class="product-meta">
|
<div class="product-meta">
|
||||||
@@ -356,7 +369,7 @@ async function loadProducts() {
|
|||||||
<div class="product-actions">
|
<div class="product-actions">
|
||||||
<span class="product-site">${storeInfo.name}</span>
|
<span class="product-site">${storeInfo.name}</span>
|
||||||
<button class="btn-favorite ${product.is_favorite ? 'active' : ''}"
|
<button class="btn-favorite ${product.is_favorite ? 'active' : ''}"
|
||||||
onclick="event.stopPropagation(); toggleFavorite('${product.url}', '${cleanName}', ${product.favorite_id || 'null'})">
|
onclick="event.stopPropagation(); toggleFavorite('${product.url}', '${cleanName.replace(/'/g, "\\'")}', ${product.favorite_id || 'null'})">
|
||||||
${product.is_favorite ? '★' : '☆'}
|
${product.is_favorite ? '★' : '☆'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -365,6 +378,24 @@ async function loadProducts() {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle product image errors by showing store icon
|
||||||
|
function handleProductImageError(img, site) {
|
||||||
|
const storeInfo = STORE_INFO[site] || { name: site, color: '#333', bgColor: '#666', logo: '', fallback: '?' };
|
||||||
|
const container = img.parentElement;
|
||||||
|
img.style.display = 'none';
|
||||||
|
|
||||||
|
// Create fallback element
|
||||||
|
const fallback = document.createElement('div');
|
||||||
|
fallback.className = 'product-store-icon';
|
||||||
|
fallback.style.background = storeInfo.bgColor;
|
||||||
|
fallback.innerHTML = `
|
||||||
|
<img src="${storeInfo.logo}" alt="${storeInfo.name}" class="product-store-logo"
|
||||||
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||||
|
<span class="product-store-fallback" style="display:none; color:${storeInfo.color};">${storeInfo.fallback}</span>
|
||||||
|
`;
|
||||||
|
container.insertBefore(fallback, img);
|
||||||
|
}
|
||||||
|
|
||||||
async function showProductDetails(productId) {
|
async function showProductDetails(productId) {
|
||||||
const product = await api(`/products/${productId}`);
|
const product = await api(`/products/${productId}`);
|
||||||
const history = await api(`/products/${productId}/history`);
|
const history = await api(`/products/${productId}/history`);
|
||||||
|
|||||||
@@ -502,6 +502,13 @@ h3 {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-clear-filters {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Products Grid */
|
/* Products Grid */
|
||||||
.products-grid {
|
.products-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -545,6 +552,36 @@ h3 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Store badge overlay on product images */
|
||||||
|
.product-store-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-badge-logo {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-badge-fallback {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
|
|||||||
@@ -80,8 +80,11 @@
|
|||||||
<div class="filters">
|
<div class="filters">
|
||||||
<select id="filterSite">
|
<select id="filterSite">
|
||||||
<option value="">All Sites</option>
|
<option value="">All Sites</option>
|
||||||
<option value="pokemoncenter">PokemonCenter</option>
|
<option value="pokemoncenter">Pokemon Center</option>
|
||||||
<option value="target">Target</option>
|
<option value="target">Target</option>
|
||||||
|
<option value="bestbuy">Best Buy</option>
|
||||||
|
<option value="gamestop">GameStop</option>
|
||||||
|
<option value="walmart">Walmart</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="filterCategory">
|
<select id="filterCategory">
|
||||||
<option value="">All Categories</option>
|
<option value="">All Categories</option>
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
<input type="checkbox" id="filterFavorites">
|
<input type="checkbox" id="filterFavorites">
|
||||||
Favorites Only
|
Favorites Only
|
||||||
</label>
|
</label>
|
||||||
|
<button id="clearFiltersBtn" class="btn-secondary btn-clear-filters">Clear Filters</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="products-grid" id="productsGrid">
|
<div class="products-grid" id="productsGrid">
|
||||||
|
|||||||
Reference in New Issue
Block a user