← back to Handbag Auth Nextjs
auction-viewer/index.html
1437 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LUXVAULT Auction Data Viewer</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #e0e0e0;
--shadow-sm: 0 2px 8px rgba(0,0,0,0.1);
--shadow-md: 0 10px 30px rgba(0,0,0,0.2);
--shadow-lg: 0 20px 60px rgba(0,0,0,0.3);
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
}
[data-theme="dark"] {
--bg-color: #1a1a2e;
--card-bg: #16213e;
--text-primary: #eee;
--text-secondary: #aaa;
--border-color: #2d3748;
--shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
--shadow-md: 0 10px 30px rgba(0,0,0,0.5);
--shadow-lg: 0 20px 60px rgba(0,0,0,0.7);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-color);
min-height: 100vh;
padding: 20px;
transition: background 0.3s ease;
}
body.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
max-width: 1400px;
margin: 0 auto;
}
/* Header Styles */
.header {
background: var(--card-bg);
padding: 30px;
border-radius: 20px;
box-shadow: var(--shadow-lg);
margin-bottom: 30px;
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--primary-gradient);
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.header h1 {
font-size: 2.5em;
color: var(--text-primary);
margin: 0;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.theme-toggle {
background: var(--card-bg);
border: 2px solid var(--border-color);
color: var(--text-primary);
width: 45px;
height: 45px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.theme-toggle:hover {
transform: rotate(180deg);
border-color: #667eea;
}
.header p {
color: var(--text-secondary);
font-size: 1.1em;
margin-bottom: 15px;
}
/* Search Bar */
.search-container {
position: relative;
margin-top: 20px;
}
.search-input {
width: 100%;
padding: 15px 50px 15px 20px;
border: 2px solid var(--border-color);
border-radius: 12px;
font-size: 16px;
background: var(--card-bg);
color: var(--text-primary);
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-icon {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
font-size: 18px;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: var(--card-bg);
padding: 25px;
border-radius: 15px;
box-shadow: var(--shadow-md);
text-align: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--primary-gradient);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.stat-card:hover::before {
transform: scaleX(1);
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
.stat-card .icon {
font-size: 2.5em;
margin-bottom: 10px;
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-card .number {
font-size: 2.5em;
font-weight: bold;
color: var(--text-primary);
margin-bottom: 10px;
font-variant-numeric: tabular-nums;
}
.stat-card .label {
color: var(--text-secondary);
font-size: 0.95em;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Loading Skeleton */
.skeleton {
background: linear-gradient(90deg, var(--border-color) 25%, rgba(102,126,234,0.1) 50%, var(--border-color) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8px;
}
.skeleton-text {
height: 20px;
margin-bottom: 10px;
}
.skeleton-title {
height: 30px;
width: 70%;
margin-bottom: 15px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Tabs */
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab {
background: var(--card-bg);
padding: 15px 30px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: var(--shadow-sm);
border: 2px solid transparent;
color: var(--text-primary);
text-decoration: none;
display: inline-block;
}
.tab:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: #667eea;
}
.tab.active {
background: var(--primary-gradient);
color: white;
}
.tab i {
margin-right: 8px;
}
/* Content Panels */
.content-panel {
background: var(--card-bg);
padding: 30px;
border-radius: 20px;
box-shadow: var(--shadow-lg);
display: none;
animation: fadeIn 0.3s ease;
}
.content-panel.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Table Styles */
.table-container {
overflow-x: auto;
max-height: 600px;
overflow-y: auto;
border-radius: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: var(--primary-gradient);
color: white;
padding: 15px;
text-align: left;
position: sticky;
top: 0;
z-index: 10;
font-weight: 600;
cursor: pointer;
user-select: none;
}
th:hover {
background: linear-gradient(135deg, #5568d3 0%, #653a8b 100%);
}
th i {
margin-left: 5px;
font-size: 0.8em;
}
td {
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
}
tr:hover {
background: rgba(102, 126, 234, 0.05);
}
.favorite-btn {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: var(--text-secondary);
transition: all 0.3s ease;
padding: 5px;
}
.favorite-btn:hover {
color: #f59e0b;
transform: scale(1.2);
}
.favorite-btn.active {
color: #f59e0b;
}
/* Brand List */
.brand-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.brand-item {
background: rgba(102, 126, 234, 0.05);
padding: 15px;
border-radius: 10px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.brand-item:hover {
transform: translateY(-3px);
border-color: #667eea;
box-shadow: var(--shadow-sm);
}
.brand-name {
font-weight: 600;
color: var(--text-primary);
}
.brand-count {
background: var(--primary-gradient);
color: white;
padding: 5px 12px;
border-radius: 15px;
font-size: 14px;
font-weight: 600;
}
/* Charts */
.chart-container {
position: relative;
height: 400px;
margin-top: 30px;
background: var(--card-bg);
padding: 20px;
border-radius: 15px;
box-shadow: var(--shadow-sm);
}
/* Toast Notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
max-width: 350px;
}
.toast {
background: var(--card-bg);
color: var(--text-primary);
padding: 16px 20px;
border-radius: 12px;
box-shadow: var(--shadow-lg);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 12px;
animation: slideIn 0.3s ease;
border-left: 4px solid #667eea;
}
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--danger); }
.toast.warning { border-left-color: var(--warning); }
.toast.info { border-left-color: var(--info); }
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.toast-icon {
font-size: 20px;
}
.toast.success .toast-icon { color: var(--success); }
.toast.error .toast-icon { color: var(--danger); }
.toast.warning .toast-icon { color: var(--warning); }
.toast.info .toast-icon { color: var(--info); }
/* Buttons */
.btn {
background: var(--primary-gradient);
color: white;
padding: 12px 24px;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
margin: 5px;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.btn:active {
transform: translateY(0);
}
.btn-secondary {
background: var(--card-bg);
color: var(--text-primary);
border: 2px solid var(--border-color);
}
/* Floating Action Buttons */
.fab-container {
position: fixed;
bottom: 30px;
right: 30px;
display: flex;
flex-direction: column;
gap: 15px;
z-index: 1000;
}
.fab {
width: 60px;
height: 60px;
border-radius: 50%;
background: var(--primary-gradient);
color: white;
border: none;
font-size: 24px;
cursor: pointer;
box-shadow: var(--shadow-lg);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.fab:hover {
transform: scale(1.1) rotate(90deg);
}
.fab-secondary {
background: var(--card-bg);
color: #667eea;
width: 50px;
height: 50px;
font-size: 20px;
}
/* Status Badge */
.status-badge {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-success {
background: var(--success);
color: white;
}
.status-warning {
background: var(--warning);
color: white;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
}
.empty-state-icon {
font-size: 80px;
margin-bottom: 20px;
opacity: 0.3;
}
.empty-state h3 {
font-size: 1.8em;
margin-bottom: 10px;
color: var(--text-primary);
}
/* Comparison Feature */
.compare-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--card-bg);
padding: 15px 20px;
box-shadow: 0 -5px 20px rgba(0,0,0,0.2);
transform: translateY(100%);
transition: transform 0.3s ease;
z-index: 999;
}
.compare-bar.active {
transform: translateY(0);
}
.compare-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.compare-items {
display: flex;
gap: 10px;
flex: 1;
overflow-x: auto;
}
.compare-item {
background: rgba(102, 126, 234, 0.1);
padding: 8px 15px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.compare-item button {
background: none;
border: none;
color: var(--danger);
cursor: pointer;
font-size: 16px;
}
/* Responsive Design */
@media (max-width: 768px) {
body {
padding: 10px;
}
.header h1 {
font-size: 1.8em;
}
.stats-grid {
grid-template-columns: 1fr;
}
.tabs {
flex-direction: column;
}
.tab {
width: 100%;
text-align: center;
}
.fab-container {
bottom: 20px;
right: 20px;
}
.chart-container {
height: 300px;
}
.header-actions {
width: 100%;
justify-content: flex-end;
}
}
/* Animations */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
}
.loading-spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid var(--border-color);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Log Container */
.log-container {
background: #1e1e1e;
color: #00ff00;
padding: 20px;
border-radius: 10px;
font-family: 'Courier New', monospace;
max-height: 600px;
overflow-y: auto;
font-size: 14px;
line-height: 1.6;
}
.log-line {
margin-bottom: 5px;
opacity: 0;
animation: fadeInLog 0.3s ease forwards;
}
@keyframes fadeInLog {
to { opacity: 1; }
}
</style>
</head>
<body class="gradient-bg">
<!-- Toast Container -->
<div class="toast-container" id="toastContainer"></div>
<!-- Compare Bar -->
<div class="compare-bar" id="compareBar">
<div class="compare-content">
<div class="compare-items" id="compareItems"></div>
<div>
<button class="btn" onclick="viewComparison()">
<i class="fas fa-columns"></i> Compare
</button>
<button class="btn btn-secondary" onclick="clearComparison()">
<i class="fas fa-times"></i> Clear
</button>
</div>
</div>
</div>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-top">
<div>
<h1><i class="fas fa-chart-line"></i> LUXVAULT</h1>
</div>
<div class="header-actions">
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle Theme">
<i class="fas fa-moon" id="themeIcon"></i>
</button>
</div>
</div>
<p>Real-time monitoring of luxury handbag auction scraper</p>
<div class="search-container">
<input type="text" class="search-input" id="searchInput" placeholder="Search auctions by title, brand, or auction house..." onkeyup="searchAuctions()">
<i class="fas fa-search search-icon"></i>
</div>
<div style="margin-top: 15px;">
<span class="status-badge status-success" id="systemStatus">SYSTEM OPERATIONAL</span>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card">
<div class="icon"><i class="fas fa-gavel"></i></div>
<div class="number" id="totalAuctions">0</div>
<div class="label">Total Auctions</div>
</div>
<div class="stat-card">
<div class="icon"><i class="fas fa-tags"></i></div>
<div class="number" id="brandsTracked">22</div>
<div class="label">Brands Tracked</div>
</div>
<div class="stat-card">
<div class="icon"><i class="fas fa-dollar-sign"></i></div>
<div class="number" id="avgPrice">$0</div>
<div class="label">Avg Price</div>
</div>
<div class="stat-card">
<div class="icon"><i class="fas fa-database"></i></div>
<div class="number" id="dbSize">0 KB</div>
<div class="label">Database Size</div>
</div>
<div class="stat-card">
<div class="icon"><i class="fas fa-star"></i></div>
<div class="number" id="favoriteCount">0</div>
<div class="label">Favorites</div>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<div class="tab active" onclick="switchTab('auctions')">
<i class="fas fa-table"></i> Auctions
</div>
<div class="tab" onclick="switchTab('stats')">
<i class="fas fa-chart-bar"></i> Statistics
</div>
<div class="tab" onclick="switchTab('favorites')">
<i class="fas fa-heart"></i> Favorites
</div>
<a href="/best-values.html" class="tab">
<i class="fas fa-gem"></i> Best Values
</a>
<div class="tab" onclick="switchTab('logs')">
<i class="fas fa-file-alt"></i> Logs
</div>
<div class="tab" onclick="switchTab('config')">
<i class="fas fa-cog"></i> Configuration
</div>
</div>
<!-- Auctions Panel -->
<div id="auctions" class="content-panel active">
<h2><i class="fas fa-gavel"></i> Auction Data</h2>
<div class="table-container" id="auctionsTable">
<div class="loading">
<div class="loading-spinner"></div>
<p>Loading auction data...</p>
</div>
</div>
</div>
<!-- Statistics Panel -->
<div id="stats" class="content-panel">
<h2><i class="fas fa-chart-bar"></i> Statistics & Analytics</h2>
<div id="statsContent">
<div class="loading">
<div class="loading-spinner"></div>
<p>Loading statistics...</p>
</div>
</div>
</div>
<!-- Favorites Panel -->
<div id="favorites" class="content-panel">
<h2><i class="fas fa-heart"></i> Favorite Auctions</h2>
<div id="favoritesContent">
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-heart"></i></div>
<h3>No favorites yet</h3>
<p>Click the star icon on any auction to add it to your favorites</p>
</div>
</div>
</div>
<!-- Logs Panel -->
<div id="logs" class="content-panel">
<h2><i class="fas fa-file-alt"></i> Scraper Logs</h2>
<div style="margin-bottom: 20px;">
<button class="btn" onclick="loadLogs()">
<i class="fas fa-sync-alt"></i> Refresh Logs
</button>
<button class="btn" onclick="downloadCSV()">
<i class="fas fa-download"></i> Download CSV
</button>
</div>
<div class="log-container" id="logsContent">
<div class="loading">Loading logs...</div>
</div>
</div>
<!-- Configuration Panel -->
<div id="config" class="content-panel">
<h2><i class="fas fa-cog"></i> System Configuration</h2>
<div id="configContent">
<div class="loading">
<div class="loading-spinner"></div>
<p>Loading configuration...</p>
</div>
</div>
</div>
</div>
<!-- Floating Action Buttons -->
<div class="fab-container">
<button class="fab fab-secondary" onclick="scrollToTop()" title="Scroll to Top">
<i class="fas fa-arrow-up"></i>
</button>
<button class="fab" onclick="loadAllData()" title="Refresh All Data">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<script>
const API_BASE = window.location.origin;
let allAuctions = [];
let filteredAuctions = [];
let favorites = JSON.parse(localStorage.getItem('luxvault_favorites') || '[]');
let compareList = JSON.parse(localStorage.getItem('luxvault_compare') || '[]');
// Theme Management
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('luxvault_theme', newTheme);
const icon = document.getElementById('themeIcon');
icon.className = newTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
showToast(newTheme === 'dark' ? 'Dark mode enabled' : 'Light mode enabled', 'success');
}
// Initialize theme
const savedTheme = localStorage.getItem('luxvault_theme') || 'light';
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
document.getElementById('themeIcon').className = 'fas fa-sun';
}
// Toast Notifications
function showToast(message, type = 'info', duration = 3000) {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
toast.innerHTML = `
<i class="fas ${icons[type]} toast-icon"></i>
<span>${message}</span>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideIn 0.3s ease reverse';
setTimeout(() => toast.remove(), 300);
}, duration);
}
// Tab Switching
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.content-panel').forEach(p => p.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tabName).classList.add('active');
if (tabName === 'favorites') {
loadFavorites();
}
}
// Search Functionality
function searchAuctions() {
const query = document.getElementById('searchInput').value.toLowerCase();
if (!query) {
filteredAuctions = allAuctions;
} else {
filteredAuctions = allAuctions.filter(auction => {
return (auction.title && auction.title.toLowerCase().includes(query)) ||
(auction.search_term && auction.search_term.toLowerCase().includes(query)) ||
(auction.auction_house && auction.auction_house.toLowerCase().includes(query));
});
}
renderAuctionsTable(filteredAuctions);
}
// Favorites Management
function toggleFavorite(auctionId) {
const index = favorites.indexOf(auctionId);
if (index > -1) {
favorites.splice(index, 1);
showToast('Removed from favorites', 'info');
} else {
favorites.push(auctionId);
showToast('Added to favorites', 'success');
}
localStorage.setItem('luxvault_favorites', JSON.stringify(favorites));
updateFavoriteCount();
// Update button state
const btn = document.querySelector(`[data-auction-id="${auctionId}"]`);
if (btn) {
btn.classList.toggle('active');
}
}
function updateFavoriteCount() {
document.getElementById('favoriteCount').textContent = favorites.length;
}
function loadFavorites() {
const favoriteAuctions = allAuctions.filter(a => favorites.includes(a.id));
if (favoriteAuctions.length === 0) {
document.getElementById('favoritesContent').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-heart"></i></div>
<h3>No favorites yet</h3>
<p>Click the star icon on any auction to add it to your favorites</p>
</div>
`;
return;
}
renderAuctionsTable(favoriteAuctions, 'favoritesContent');
}
// Comparison Feature
function addToCompare(auctionId) {
if (compareList.includes(auctionId)) {
showToast('Already in comparison list', 'warning');
return;
}
if (compareList.length >= 4) {
showToast('Maximum 4 items can be compared', 'warning');
return;
}
compareList.push(auctionId);
localStorage.setItem('luxvault_compare', JSON.stringify(compareList));
updateCompareBar();
showToast('Added to comparison', 'success');
}
function removeFromCompare(auctionId) {
compareList = compareList.filter(id => id !== auctionId);
localStorage.setItem('luxvault_compare', JSON.stringify(compareList));
updateCompareBar();
}
function updateCompareBar() {
const bar = document.getElementById('compareBar');
const items = document.getElementById('compareItems');
if (compareList.length === 0) {
bar.classList.remove('active');
return;
}
bar.classList.add('active');
items.innerHTML = compareList.map(id => {
const auction = allAuctions.find(a => a.id === id);
if (!auction) return '';
return `
<div class="compare-item">
<span>${auction.title ? auction.title.substring(0, 50) : 'Auction #' + id}</span>
<button onclick="removeFromCompare(${id})"><i class="fas fa-times"></i></button>
</div>
`;
}).join('');
}
function clearComparison() {
compareList = [];
localStorage.setItem('luxvault_compare', JSON.stringify([]));
updateCompareBar();
showToast('Comparison cleared', 'info');
}
function viewComparison() {
if (compareList.length < 2) {
showToast('Add at least 2 items to compare', 'warning');
return;
}
const compareAuctions = allAuctions.filter(a => compareList.includes(a.id));
let html = '<div style="overflow-x: auto;"><table><thead><tr><th>Property</th>';
compareAuctions.forEach((a, i) => {
html += `<th>Item ${i + 1}</th>`;
});
html += '</tr></thead><tbody>';
const fields = [
['Title', 'title'],
['Brand', 'search_term'],
['Current Price', 'current_price'],
['Estimate Low', 'estimate_low'],
['Estimate High', 'estimate_high'],
['Auction House', 'auction_house'],
['End Date', 'end_date']
];
fields.forEach(([label, field]) => {
html += `<tr><td><strong>${label}</strong></td>`;
compareAuctions.forEach(a => {
let value = a[field] || 'N/A';
if (field.includes('price') && value !== 'N/A') {
value = '$' + value.toLocaleString();
}
html += `<td>${value}</td>`;
});
html += '</tr>';
});
html += '</tbody></table></div>';
const panel = document.getElementById('auctionsTable');
panel.innerHTML = html;
switchTab('auctions');
showToast('Viewing comparison', 'info');
}
// Load Auctions
async function loadAuctions() {
try {
const res = await fetch(`${API_BASE}/api/auctions`);
const data = await res.json();
allAuctions = data.data || [];
filteredAuctions = allAuctions;
document.getElementById('totalAuctions').textContent = data.count;
if (data.count === 0) {
document.getElementById('auctionsTable').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-gavel"></i></div>
<h3>No auction data yet</h3>
<p>The scraper will run daily at 6:00 AM and populate this table.</p>
<p style="margin-top: 20px;">Database is initialized and ready to receive data.</p>
</div>
`;
return;
}
renderAuctionsTable(filteredAuctions);
updateFavoriteCount();
updateCompareBar();
} catch (error) {
document.getElementById('auctionsTable').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-exclamation-circle"></i></div>
<h3>Error loading data</h3>
<p>${error.message}</p>
</div>
`;
showToast('Failed to load auctions', 'error');
}
}
function renderAuctionsTable(auctions, containerId = 'auctionsTable') {
let html = `
<table>
<thead>
<tr>
<th><i class="fas fa-star"></i></th>
<th>ID</th>
<th>Title</th>
<th>Auction House</th>
<th>Price <i class="fas fa-sort"></i></th>
<th>Estimate</th>
<th>End Date</th>
<th>Brand</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
auctions.forEach(auction => {
const isFavorite = favorites.includes(auction.id);
html += `
<tr>
<td>
<button class="favorite-btn ${isFavorite ? 'active' : ''}"
data-auction-id="${auction.id}"
onclick="toggleFavorite(${auction.id})">
<i class="fas fa-star"></i>
</button>
</td>
<td>${auction.id}</td>
<td><a href="${auction.url}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 600;">${auction.title || 'N/A'}</a></td>
<td>${auction.auction_house || 'N/A'}</td>
<td style="font-weight: 600; color: #10b981;">${auction.current_price ? '$' + auction.current_price.toLocaleString() : 'N/A'}</td>
<td>${auction.estimate_low && auction.estimate_high ? '$' + auction.estimate_low.toLocaleString() + ' - $' + auction.estimate_high.toLocaleString() : 'N/A'}</td>
<td>${auction.end_date || 'N/A'}</td>
<td><span class="status-badge" style="background: rgba(102,126,234,0.2); color: #667eea;">${auction.search_term}</span></td>
<td>
<button class="btn btn-secondary" style="padding: 6px 12px; margin: 0;" onclick="addToCompare(${auction.id})">
<i class="fas fa-plus"></i> Compare
</button>
</td>
</tr>
`;
});
html += '</tbody></table>';
document.getElementById(containerId).innerHTML = html;
}
// Load Statistics
async function loadStats() {
try {
const res = await fetch(`${API_BASE}/api/stats`);
const data = await res.json();
if (data.success) {
const stats = data.stats;
document.getElementById('avgPrice').textContent = stats.avgPrice > 0
? '$' + Math.round(stats.avgPrice).toLocaleString()
: '$0';
let html = `
<h3><i class="fas fa-tags"></i> Auctions by Brand</h3>
<div class="brand-list">
`;
if (stats.byBrand.length > 0) {
stats.byBrand.forEach(brand => {
html += `
<div class="brand-item">
<span class="brand-name">${brand.search_term}</span>
<span class="brand-count">${brand.count}</span>
</div>
`;
});
} else {
html += '<p style="padding: 20px;">No data yet - waiting for first scrape run</p>';
}
html += '</div>';
// Add chart
html += '<div class="chart-container"><canvas id="priceChart"></canvas></div>';
if (stats.byAuctionHouse.length > 0) {
html += '<h3 style="margin-top: 30px;"><i class="fas fa-building"></i> Auctions by House</h3><div class="brand-list">';
stats.byAuctionHouse.forEach(house => {
html += `
<div class="brand-item">
<span class="brand-name">${house.auction_house}</span>
<span class="brand-count">${house.count}</span>
</div>
`;
});
html += '</div>';
}
if (stats.priceRange.min_price > 0) {
html += `
<h3 style="margin-top: 30px;"><i class="fas fa-dollar-sign"></i> Price Range</h3>
<div class="brand-list">
<div class="brand-item">
<span class="brand-name">Minimum Price</span>
<span class="brand-count">$${stats.priceRange.min_price.toLocaleString()}</span>
</div>
<div class="brand-item">
<span class="brand-name">Maximum Price</span>
<span class="brand-count">$${stats.priceRange.max_price.toLocaleString()}</span>
</div>
</div>
`;
}
document.getElementById('statsContent').innerHTML = html;
// Render chart if we have data
if (stats.byBrand.length > 0) {
setTimeout(() => renderChart(stats.byBrand), 100);
}
}
} catch (error) {
document.getElementById('statsContent').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-exclamation-circle"></i></div>
<h3>Error loading statistics</h3>
<p>${error.message}</p>
</div>
`;
showToast('Failed to load statistics', 'error');
}
}
// Render Chart
function renderChart(brandData) {
const ctx = document.getElementById('priceChart');
if (!ctx) return;
new Chart(ctx, {
type: 'bar',
data: {
labels: brandData.slice(0, 10).map(b => b.search_term),
datasets: [{
label: 'Number of Auctions',
data: brandData.slice(0, 10).map(b => b.count),
backgroundColor: 'rgba(102, 126, 234, 0.6)',
borderColor: '#667eea',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
title: {
display: true,
text: 'Top 10 Brands by Auction Count'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
}
// Load Logs
async function loadLogs() {
try {
const res = await fetch(`${API_BASE}/api/logs`);
const data = await res.json();
if (data.success) {
const html = data.logs.map((line, i) =>
`<div class="log-line" style="animation-delay: ${i * 0.01}s">${line}</div>`
).join('');
document.getElementById('logsContent').innerHTML = html || '<div class="empty-state">No logs yet</div>';
showToast('Logs refreshed', 'success');
}
} catch (error) {
document.getElementById('logsContent').innerHTML = `<div class="empty-state">Error: ${error.message}</div>`;
showToast('Failed to load logs', 'error');
}
}
// Load Configuration
async function loadConfig() {
const config = {
'Scraper Script': '/root/Projects/handbag-auth-nextjs/scripts/scrape-auction-data.py',
'Database': '/root/Projects/handbag-auth-nextjs/data/auction-history/auctions.db',
'CSV Export': '/root/Projects/handbag-auth-nextjs/data/auction-history/auction-history.csv',
'Logs': '/root/Projects/handbag-auth-nextjs/data/auction-history/scraper.log',
'Cron Schedule': '0 6 * * * (Daily at 6:00 AM)',
'Brands Monitored': '22 luxury handbag models',
'Rate Limit': '2 seconds between requests',
'Python Version': '3.10',
'Dependencies': 'BeautifulSoup4, requests, lxml, sqlite3'
};
try {
const res = await fetch(`${API_BASE}/api/status`);
const data = await res.json();
if (data.success) {
document.getElementById('dbSize').textContent = (data.status.dbSize / 1024).toFixed(2) + ' KB';
}
let html = '<h3><i class="fas fa-cog"></i> Configuration</h3><div class="brand-list">';
Object.keys(config).forEach(key => {
html += `
<div class="brand-item" style="grid-column: span 2;">
<span class="brand-name">${key}</span>
<span style="color: var(--text-secondary); font-size: 0.9em;">${config[key]}</span>
</div>
`;
});
html += '</div>';
html += `
<h3 style="margin-top: 30px;"><i class="fas fa-info-circle"></i> System Status</h3>
<div class="brand-list">
<div class="brand-item">
<span class="brand-name">Database File</span>
<span class="status-badge ${data.status.database ? 'status-success' : 'status-warning'}">${data.status.database ? 'EXISTS' : 'MISSING'}</span>
</div>
<div class="brand-item">
<span class="brand-name">CSV File</span>
<span class="status-badge ${data.status.csv ? 'status-success' : 'status-warning'}">${data.status.csv ? 'EXISTS' : 'MISSING'}</span>
</div>
<div class="brand-item">
<span class="brand-name">Log File</span>
<span class="status-badge ${data.status.logs ? 'status-success' : 'status-warning'}">${data.status.logs ? 'EXISTS' : 'MISSING'}</span>
</div>
<div class="brand-item">
<span class="brand-name">DB Size</span>
<span class="brand-count">${(data.status.dbSize / 1024).toFixed(2)} KB</span>
</div>
<div class="brand-item">
<span class="brand-name">CSV Size</span>
<span class="brand-count">${(data.status.csvSize / 1024).toFixed(2)} KB</span>
</div>
<div class="brand-item">
<span class="brand-name">Log Size</span>
<span class="brand-count">${(data.status.logSize / 1024).toFixed(2)} KB</span>
</div>
</div>
`;
document.getElementById('configContent').innerHTML = html;
} catch (error) {
document.getElementById('configContent').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-exclamation-circle"></i></div>
<h3>Error loading configuration</h3>
<p>${error.message}</p>
</div>
`;
}
}
// Download CSV
function downloadCSV() {
window.open(`${API_BASE}/api/download-csv`, '_blank');
showToast('Downloading CSV...', 'info');
}
// Scroll to Top
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// Load All Data
async function loadAllData() {
showToast('Refreshing data...', 'info');
await Promise.all([
loadAuctions(),
loadStats(),
loadLogs(),
loadConfig()
]);
showToast('Data refreshed successfully', 'success');
}
// Initialize
loadAllData();
setInterval(loadAllData, 60000); // Auto-refresh every 60 seconds
</script>
</body>
</html>