← back to Handbag Auth Nextjs
auction-viewer/analytics-dashboard.html
915 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LUXVAULT Advanced Analytics Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 30px;
}
h1 {
color: #667eea;
font-size: 2.5em;
margin-bottom: 10px;
}
.subtitle {
color: #666;
font-size: 1.1em;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab {
background: white;
padding: 12px 24px;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 1em;
font-weight: 500;
transition: all 0.3s;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.tab:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.stat-label {
font-size: 0.9em;
color: #666;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
}
.stat-value {
font-size: 2.2em;
font-weight: bold;
color: #667eea;
}
.stat-subvalue {
font-size: 0.9em;
color: #888;
margin-top: 5px;
}
.card {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.8em;
}
.deal-item {
padding: 20px;
border: 2px solid #f0f0f0;
border-radius: 10px;
margin-bottom: 15px;
transition: all 0.3s;
}
.deal-item:hover {
border-color: #667eea;
transform: translateX(5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.deal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
}
.deal-title {
font-size: 1.2em;
font-weight: 600;
color: #333;
flex: 1;
min-width: 200px;
}
.deal-score {
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
font-size: 1.1em;
margin-left: 10px;
}
.grade-a-plus { background: #10b981; color: white; }
.grade-a { background: #3b82f6; color: white; }
.grade-b-plus { background: #8b5cf6; color: white; }
.grade-b { background: #f59e0b; color: white; }
.grade-c { background: #ef4444; color: white; }
.deal-details {
display: flex;
gap: 20px;
color: #666;
font-size: 0.95em;
flex-wrap: wrap;
}
.detail-item {
display: flex;
align-items: center;
gap: 5px;
}
.detail-label {
font-weight: 500;
}
.stars {
color: #fbbf24;
font-size: 1.2em;
}
.trend-item {
padding: 15px;
border-left: 4px solid #667eea;
background: #f8f9fa;
margin-bottom: 10px;
border-radius: 5px;
}
.trend-hot { border-color: #ef4444; background: #fee2e2; }
.trend-stable { border-color: #3b82f6; background: #dbeafe; }
.trend-cooling { border-color: #6b7280; background: #f3f4f6; }
.trend-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.trend-brand {
font-weight: 600;
font-size: 1.1em;
}
.trend-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85em;
font-weight: bold;
}
.badge-hot { background: #ef4444; color: white; }
.badge-stable { background: #3b82f6; color: white; }
.badge-cooling { background: #6b7280; color: white; }
.prediction-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
margin-bottom: 10px;
}
.prediction-brand {
font-weight: 600;
color: #333;
}
.prediction-values {
text-align: right;
}
.prediction-price {
font-size: 1.2em;
font-weight: bold;
color: #667eea;
}
.prediction-trend {
font-size: 0.9em;
color: #666;
}
.trend-positive { color: #10b981; }
.trend-negative { color: #ef4444; }
.filters {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.filter-label {
font-size: 0.9em;
color: #666;
font-weight: 500;
}
input, select {
padding: 10px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1em;
min-width: 200px;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1em;
font-weight: 500;
transition: transform 0.2s;
}
button:hover {
transform: scale(1.05);
}
.loading {
text-align: center;
padding: 40px;
color: #667eea;
font-size: 1.2em;
}
.error {
background: #fee2e2;
color: #991b1b;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #667eea;
}
tr:hover {
background: #f8f9fa;
}
@media (max-width: 768px) {
h1 {
font-size: 1.8em;
}
.stats-grid {
grid-template-columns: 1fr;
}
.deal-header {
flex-direction: column;
align-items: flex-start;
}
.deal-score {
margin-left: 0;
margin-top: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>LUXVAULT Advanced Analytics</h1>
<p class="subtitle">AI-Powered Insights for Luxury Handbag Auctions</p>
</header>
<div class="tabs">
<button class="tab active" onclick="switchTab('overview')">Overview</button>
<button class="tab" onclick="switchTab('predictions')">Price Predictions</button>
<button class="tab" onclick="switchTab('trends')">Trending Brands</button>
<button class="tab" onclick="switchTab('deals')">Top Deals</button>
<button class="tab" onclick="switchTab('undervalued')">Undervalued</button>
<button class="tab" onclick="switchTab('recommendations')">Recommendations</button>
<button class="tab" onclick="switchTab('comparison')">Brand Comparison</button>
</div>
<!-- Overview Tab -->
<div id="overview" class="tab-content active">
<div class="stats-grid" id="overview-stats"></div>
<div class="card">
<h2>Daily Report</h2>
<div id="daily-report"></div>
</div>
</div>
<!-- Predictions Tab -->
<div id="predictions" class="tab-content">
<div class="card">
<h2>Price Predictions (30-Day Forecast)</h2>
<div id="predictions-list"></div>
</div>
</div>
<!-- Trends Tab -->
<div id="trends" class="tab-content">
<div class="card">
<h2>Trending Brands (90-Day Analysis)</h2>
<div id="trends-list"></div>
</div>
<div class="card">
<h2>Auction House Performance</h2>
<div id="auction-houses"></div>
</div>
</div>
<!-- Top Deals Tab -->
<div id="deals" class="tab-content">
<div class="card">
<h2>Highest-Rated Deals</h2>
<div id="deals-list"></div>
</div>
</div>
<!-- Undervalued Tab -->
<div id="undervalued" class="tab-content">
<div class="card">
<h2>Undervalued Auctions</h2>
<p style="margin-bottom: 20px; color: #666;">Items selling significantly below historical average</p>
<div id="undervalued-list"></div>
</div>
</div>
<!-- Recommendations Tab -->
<div id="recommendations" class="tab-content">
<div class="card">
<h2>Personalized Recommendations</h2>
<div class="filters">
<div class="filter-group">
<label class="filter-label">Max Budget ($)</label>
<input type="number" id="budget-filter" placeholder="e.g., 5000">
</div>
<div class="filter-group">
<label class="filter-label">Preferred Brands</label>
<select id="brand-filter" multiple style="height: 100px;">
<!-- Populated dynamically -->
</select>
</div>
<div class="filter-group">
<label class="filter-label">Min Deal Score</label>
<input type="number" id="score-filter" value="50" min="0" max="100">
</div>
<div class="filter-group">
<label class="filter-label"> </label>
<button onclick="getRecommendations()">Get Recommendations</button>
</div>
</div>
<div id="recommendations-list"></div>
</div>
</div>
<!-- Brand Comparison Tab -->
<div id="comparison" class="tab-content">
<div class="card">
<h2>Brand Comparison Analysis</h2>
<div id="comparison-table"></div>
</div>
</div>
</div>
<script>
const API_BASE = 'http://45.61.58.125:7501/api/analytics';
// Tab switching
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tabName).classList.add('active');
// Load data for the tab
loadTabData(tabName);
}
// Load data based on active tab
async function loadTabData(tabName) {
switch(tabName) {
case 'overview':
await loadOverview();
break;
case 'predictions':
await loadPredictions();
break;
case 'trends':
await loadTrends();
break;
case 'deals':
await loadTopDeals();
break;
case 'undervalued':
await loadUndervalued();
break;
case 'recommendations':
await loadBrandsList();
break;
case 'comparison':
await loadComparison();
break;
}
}
// Format currency
function formatCurrency(value) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value);
}
// Format percentage
function formatPercent(value) {
return `${value >= 0 ? '+' : ''}${value.toFixed(1)}%`;
}
// Get grade class
function getGradeClass(grade) {
const gradeMap = {
'A+': 'grade-a-plus',
'A': 'grade-a',
'B+': 'grade-b-plus',
'B': 'grade-b',
'C+': 'grade-c',
'C': 'grade-c',
'D': 'grade-c'
};
return gradeMap[grade] || 'grade-c';
}
// Load overview data
async function loadOverview() {
const statsDiv = document.getElementById('overview-stats');
const reportDiv = document.getElementById('daily-report');
statsDiv.innerHTML = '<div class="loading">Loading analytics...</div>';
try {
const [statusRes, reportRes] = await Promise.all([
fetch(`${API_BASE}/status`),
fetch(`${API_BASE}/reports/daily`)
]);
const status = await statusRes.json();
const report = await reportRes.json();
if (status.success) {
statsDiv.innerHTML = `
<div class="stat-card">
<div class="stat-label">Total Auctions</div>
<div class="stat-value">${status.status.total_auctions.toLocaleString()}</div>
<div class="stat-subvalue">${status.status.brands} brands tracked</div>
</div>
<div class="stat-card">
<div class="stat-label">Price Range</div>
<div class="stat-value">${formatCurrency(status.status.price_range.min)}</div>
<div class="stat-subvalue">to ${formatCurrency(status.status.price_range.max)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Average Price</div>
<div class="stat-value">${formatCurrency(status.status.price_range.avg)}</div>
<div class="stat-subvalue">Across all brands</div>
</div>
<div class="stat-card">
<div class="stat-label">Date Range</div>
<div class="stat-value">${status.status.date_range.start}</div>
<div class="stat-subvalue">to ${status.status.date_range.end}</div>
</div>
`;
}
if (report.success) {
const r = report.report;
let topDealsHtml = '<h3>Top Recent Deals</h3>';
r.top_deals.slice(0, 5).forEach(deal => {
topDealsHtml += `
<div class="deal-item">
<div class="deal-header">
<div class="deal-title">${deal.title}</div>
<div>${formatPercent(deal.savings_percent)} savings</div>
</div>
<div class="deal-details">
<div class="detail-item">
<span class="detail-label">Price:</span> ${formatCurrency(deal.price)}
</div>
<div class="detail-item">
<span class="detail-label">House:</span> ${deal.auction_house}
</div>
</div>
</div>
`;
});
reportDiv.innerHTML = topDealsHtml;
}
} catch (error) {
statsDiv.innerHTML = `<div class="error">Error loading data: ${error.message}</div>`;
}
}
// Load predictions
async function loadPredictions() {
const listDiv = document.getElementById('predictions-list');
listDiv.innerHTML = '<div class="loading">Analyzing price trends...</div>';
try {
const response = await fetch(`${API_BASE}/predictions`);
const data = await response.json();
if (data.success) {
let html = '';
data.predictions.forEach(pred => {
const trendClass = pred.trend_percent_annual >= 0 ? 'trend-positive' : 'trend-negative';
html += `
<div class="prediction-item">
<div>
<div class="prediction-brand">${pred.brand}</div>
<div style="font-size: 0.9em; color: #666;">
Current avg: ${formatCurrency(pred.current_avg_price)}
(${pred.sample_size} auctions)
</div>
</div>
<div class="prediction-values">
<div class="prediction-price">${formatCurrency(pred.predicted_price)}</div>
<div class="prediction-trend ${trendClass}">
${formatPercent(pred.trend_percent_annual)} annually
</div>
<div style="font-size: 0.85em; color: #888;">
CI: ${formatCurrency(pred.confidence_lower)} - ${formatCurrency(pred.confidence_upper)}
</div>
</div>
</div>
`;
});
listDiv.innerHTML = html;
}
} catch (error) {
listDiv.innerHTML = `<div class="error">Error loading predictions: ${error.message}</div>`;
}
}
// Load trends
async function loadTrends() {
const trendsDiv = document.getElementById('trends-list');
const housesDiv = document.getElementById('auction-houses');
trendsDiv.innerHTML = '<div class="loading">Analyzing trends...</div>';
housesDiv.innerHTML = '<div class="loading">Loading auction houses...</div>';
try {
const [trendsRes, housesRes] = await Promise.all([
fetch(`${API_BASE}/trends?limit=15`),
fetch(`${API_BASE}/auction-houses`)
]);
const trends = await trendsRes.json();
const houses = await housesRes.json();
if (trends.success) {
let html = '';
trends.trends.forEach(trend => {
const trendClass = trend.trend === 'Hot' ? 'trend-hot' :
trend.trend === 'Stable' ? 'trend-stable' : 'trend-cooling';
const badgeClass = trend.trend === 'Hot' ? 'badge-hot' :
trend.trend === 'Stable' ? 'badge-stable' : 'badge-cooling';
html += `
<div class="trend-item ${trendClass}">
<div class="trend-header">
<div class="trend-brand">${trend.brand}</div>
<span class="trend-badge ${badgeClass}">${trend.trend}</span>
</div>
<div style="display: flex; gap: 20px; font-size: 0.95em; color: #666;">
<div>Recent auctions: ${trend.recent_auctions}</div>
<div>Avg price: ${formatCurrency(trend.recent_avg_price)}</div>
<div>Momentum: ${trend.momentum_score.toFixed(1)}</div>
</div>
</div>
`;
});
trendsDiv.innerHTML = html;
}
if (houses.success) {
let html = '<table><thead><tr><th>Auction House</th><th>Auctions</th><th>Avg Price</th><th>Avg Savings</th><th>Top Deals</th></tr></thead><tbody>';
houses.auction_houses.forEach(house => {
html += `
<tr>
<td><strong>${house.auction_house}</strong></td>
<td>${house.total_auctions}</td>
<td>${formatCurrency(house.avg_price)}</td>
<td>${formatPercent(house.avg_savings_percent)}</td>
<td>${house.deals_count}</td>
</tr>
`;
});
html += '</tbody></table>';
housesDiv.innerHTML = html;
}
} catch (error) {
trendsDiv.innerHTML = `<div class="error">Error loading trends: ${error.message}</div>`;
}
}
// Load top deals
async function loadTopDeals() {
const listDiv = document.getElementById('deals-list');
listDiv.innerHTML = '<div class="loading">Analyzing deals...</div>';
try {
const response = await fetch(`${API_BASE}/top-deals?limit=20`);
const data = await response.json();
if (data.success) {
let html = '';
data.deals.forEach(deal => {
const stars = '★'.repeat(deal.investment_stars) + '☆'.repeat(5 - deal.investment_stars);
html += `
<div class="deal-item">
<div class="deal-header">
<div class="deal-title">${deal.title}</div>
<div class="deal-score ${getGradeClass(deal.grade)}">${deal.grade} (${deal.deal_score})</div>
</div>
<div class="deal-details">
<div class="detail-item">
<span class="detail-label">Price:</span> ${formatCurrency(deal.price)}
</div>
<div class="detail-item">
<span class="detail-label">Savings:</span> ${formatPercent(deal.savings_percent)}
</div>
<div class="detail-item">
<span class="detail-label">Investment:</span>
<span class="stars">${stars}</span>
</div>
<div class="detail-item">
<span class="detail-label">House:</span> ${deal.auction_house}
</div>
</div>
</div>
`;
});
listDiv.innerHTML = html;
}
} catch (error) {
listDiv.innerHTML = `<div class="error">Error loading deals: ${error.message}</div>`;
}
}
// Load undervalued
async function loadUndervalued() {
const listDiv = document.getElementById('undervalued-list');
listDiv.innerHTML = '<div class="loading">Finding bargains...</div>';
try {
const response = await fetch(`${API_BASE}/undervalued?threshold=15&limit=30`);
const data = await response.json();
if (data.success) {
let html = '';
data.undervalued.forEach(item => {
html += `
<div class="deal-item">
<div class="deal-header">
<div class="deal-title">${item.title}</div>
<div style="color: #10b981; font-weight: bold;">${formatCurrency(item.discount_amount)} below avg</div>
</div>
<div class="deal-details">
<div class="detail-item">
<span class="detail-label">Current:</span> ${formatCurrency(item.current_price)}
</div>
<div class="detail-item">
<span class="detail-label">Avg:</span> ${formatCurrency(item.historical_avg)}
</div>
<div class="detail-item">
<span class="detail-label">Discount:</span> ${formatPercent(item.discount_percent)}
</div>
<div class="detail-item">
<span class="detail-label">House:</span> ${item.auction_house}
</div>
</div>
</div>
`;
});
listDiv.innerHTML = html;
}
} catch (error) {
listDiv.innerHTML = `<div class="error">Error loading undervalued items: ${error.message}</div>`;
}
}
// Load brands list for filter
async function loadBrandsList() {
try {
const response = await fetch(`${API_BASE}/status`);
const data = await response.json();
if (data.success) {
const select = document.getElementById('brand-filter');
const brands = ['Hermes Birkin', 'Hermes Kelly', 'Chanel Classic Flap', 'Chanel Boy', 'Louis Vuitton Neverfull',
'Dior Lady Dior', 'Gucci Dionysus', 'Prada Galleria', 'Bottega Veneta Cassette'];
select.innerHTML = brands.map(b => `<option value="${b}">${b}</option>`).join('');
}
} catch (error) {
console.error('Error loading brands:', error);
}
}
// Get recommendations
async function getRecommendations() {
const listDiv = document.getElementById('recommendations-list');
listDiv.innerHTML = '<div class="loading">Finding perfect matches...</div>';
try {
const budget = document.getElementById('budget-filter').value;
const selectedBrands = Array.from(document.getElementById('brand-filter').selectedOptions).map(o => o.value);
const minScore = document.getElementById('score-filter').value;
let url = `${API_BASE}/recommendations?min_score=${minScore}`;
if (budget) url += `&max_budget=${budget}`;
if (selectedBrands.length > 0) url += `&brands=${selectedBrands.join(',')}`;
const response = await fetch(url);
const data = await response.json();
if (data.success) {
if (data.recommendations.length === 0) {
listDiv.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No recommendations found. Try adjusting your filters.</div>';
return;
}
let html = '';
data.recommendations.forEach(rec => {
const stars = '★'.repeat(rec.investment_stars) + '☆'.repeat(5 - rec.investment_stars);
html += `
<div class="deal-item">
<div class="deal-header">
<div class="deal-title">${rec.title}</div>
<div class="deal-score ${getGradeClass(rec.grade)}">${rec.grade} (${rec.deal_score})</div>
</div>
<div class="deal-details">
<div class="detail-item">
<span class="detail-label">Price:</span> ${formatCurrency(rec.price)}
</div>
<div class="detail-item">
<span class="detail-label">Savings:</span> ${formatPercent(rec.savings_percent)}
</div>
<div class="detail-item">
<span class="detail-label">Investment:</span>
<span class="stars">${stars}</span> ${rec.investment_grade}
</div>
<div class="detail-item">
<span class="detail-label">House:</span> ${rec.auction_house}
</div>
</div>
</div>
`;
});
listDiv.innerHTML = html;
}
} catch (error) {
listDiv.innerHTML = `<div class="error">Error loading recommendations: ${error.message}</div>`;
}
}
// Load comparison
async function loadComparison() {
const tableDiv = document.getElementById('comparison-table');
tableDiv.innerHTML = '<div class="loading">Comparing brands...</div>';
try {
const response = await fetch(`${API_BASE}/reports/brand-comparison`);
const data = await response.json();
if (data.success) {
let html = '<table><thead><tr><th>Brand</th><th>Auctions</th><th>Avg Price</th><th>Median</th><th>Avg Savings</th><th>Price Range</th><th>Trend</th><th>Rarity</th></tr></thead><tbody>';
data.brand_comparison.forEach(brand => {
const trendClass = brand.predicted_trend >= 0 ? 'trend-positive' : 'trend-negative';
html += `
<tr>
<td><strong>${brand.brand}</strong></td>
<td>${brand.total_auctions}</td>
<td>${formatCurrency(brand.avg_price)}</td>
<td>${formatCurrency(brand.median_price)}</td>
<td>${formatPercent(brand.avg_savings)}</td>
<td>${formatCurrency(brand.price_range.min)} - ${formatCurrency(brand.price_range.max)}</td>
<td class="${trendClass}">${formatPercent(brand.predicted_trend)}</td>
<td>${brand.rarity_score}/15</td>
</tr>
`;
});
html += '</tbody></table>';
tableDiv.innerHTML = html;
}
} catch (error) {
tableDiv.innerHTML = `<div class="error">Error loading comparison: ${error.message}</div>`;
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadOverview();
});
</script>
</body>
</html>