← back to Handbag Authentication
public/birkin-chart.html
572 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hermès Birkin Price Analysis - Japan vs US</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
color: white;
margin-bottom: 40px;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-label {
font-size: 0.875rem;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #1a1a1a;
}
.stat-subtext {
font-size: 0.875rem;
color: #888;
margin-top: 4px;
}
.chart-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.chart-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 20px;
color: #1a1a1a;
}
.listings-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.listing-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.listing-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.listing-image {
width: 100%;
height: 200px;
object-fit: cover;
background: #f5f5f5;
}
.listing-content {
padding: 16px;
}
.listing-title {
font-size: 0.95rem;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 8px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.listing-price {
font-size: 1.5rem;
font-weight: 700;
color: #667eea;
margin-bottom: 4px;
}
.listing-jpy {
font-size: 0.875rem;
color: #888;
margin-bottom: 12px;
}
.profit-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 8px;
}
.profit-high {
background: #10b981;
color: white;
}
.profit-medium {
background: #f59e0b;
color: white;
}
.profit-low {
background: #6b7280;
color: white;
}
.listing-condition {
font-size: 0.875rem;
color: #666;
margin-bottom: 12px;
}
.listing-link {
display: inline-block;
padding: 10px 20px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background 0.3s ease;
}
.listing-link:hover {
background: #5568d3;
}
.loading {
text-align: center;
padding: 60px 20px;
color: white;
font-size: 1.5rem;
}
.filters {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
margin-bottom: 30px;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.filter-group {
flex: 1;
min-width: 200px;
}
.filter-label {
font-size: 0.875rem;
color: #666;
margin-bottom: 6px;
display: block;
}
select, input {
width: 100%;
padding: 10px 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
select:focus, input:focus {
outline: none;
border-color: #667eea;
}
.refresh-btn {
padding: 12px 24px;
background: #10b981;
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s ease;
}
.refresh-btn:hover {
background: #059669;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>💼 Hermès Birkin Arbitrage</h1>
<p class="subtitle">Live prices from Japan vs US Market Value</p>
</header>
<div class="stats-grid" id="stats">
<div class="loading">Loading data...</div>
</div>
<div class="filters">
<div class="filter-group">
<label class="filter-label">Condition</label>
<select id="conditionFilter" onchange="applyFilters()">
<option value="">All Conditions</option>
<option value="new">New/Unused</option>
<option value="excellent">Excellent</option>
<option value="good">Good</option>
<option value="used">Used</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Min Price (USD)</label>
<input type="number" id="minPrice" placeholder="0" onchange="applyFilters()">
</div>
<div class="filter-group">
<label class="filter-label">Max Price (USD)</label>
<input type="number" id="maxPrice" placeholder="50000" onchange="applyFilters()">
</div>
<div class="filter-group">
<label class="filter-label">Sort By</label>
<select id="sortBy" onchange="applyFilters()">
<option value="price_low">Price: Low to High</option>
<option value="price_high">Price: High to Low</option>
<option value="profit">Potential Profit</option>
<option value="date">Recently Added</option>
</select>
</div>
<button class="refresh-btn" onclick="loadData()">🔄 Refresh</button>
</div>
<div class="chart-container">
<h2 class="chart-title">Price Distribution</h2>
<canvas id="priceChart"></canvas>
</div>
<div class="chart-container">
<h2 class="chart-title">Available Birkins (Japan)</h2>
<div class="listings-grid" id="listings">
<div class="loading">Loading listings...</div>
</div>
</div>
</div>
<script>
let allListings = [];
let priceChart = null;
// US Birkin market prices (reference data)
const usBirkinPrices = {
'birkin 25': { min: 12000, avg: 18000, max: 35000 },
'birkin 30': { min: 13000, avg: 20000, max: 40000 },
'birkin 35': { min: 14000, avg: 22000, max: 45000 },
'birkin 40': { min: 15000, avg: 24000, max: 50000 },
'default': { min: 12000, avg: 20000, max: 50000 }
};
function estimateUSPrice(title) {
const titleLower = title.toLowerCase();
for (const [size, prices] of Object.entries(usBirkinPrices)) {
if (titleLower.includes(size)) {
return prices;
}
}
return usBirkinPrices.default;
}
function calculateProfitPotential(jpPrice, usAvg) {
const profit = usAvg - jpPrice;
const percentage = ((profit / jpPrice) * 100).toFixed(0);
return { profit, percentage };
}
async function loadData() {
try {
const response = await fetch('/api/arbitrage/birkins');
const data = await response.json();
allListings = data.listings || [];
updateStats(allListings);
updateChart(allListings);
renderListings(allListings);
} catch (error) {
console.error('Error loading data:', error);
document.getElementById('stats').innerHTML = '<div class="loading">Error loading data. Make sure the crawler has run.</div>';
}
}
function updateStats(listings) {
if (!listings || listings.length === 0) {
document.getElementById('stats').innerHTML = `
<div class="stat-card">
<div class="stat-label">Status</div>
<div class="stat-value" style="font-size: 1.5rem;">No Birkins Yet</div>
<div class="stat-subtext">Crawler is collecting data...</div>
</div>
`;
return;
}
const prices = listings.map(l => l.price_usd).filter(p => p > 0);
const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length;
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
let totalProfit = 0;
let dealsCount = 0;
listings.forEach(listing => {
const usPrice = estimateUSPrice(listing.title);
const { profit, percentage } = calculateProfitPotential(listing.price_usd, usPrice.avg);
if (parseInt(percentage) >= 30) {
dealsCount++;
totalProfit += profit;
}
});
document.getElementById('stats').innerHTML = `
<div class="stat-card">
<div class="stat-label">Total Birkins</div>
<div class="stat-value">${listings.length}</div>
<div class="stat-subtext">Available in Japan</div>
</div>
<div class="stat-card">
<div class="stat-label">Average Price</div>
<div class="stat-value">$${avgPrice.toFixed(0)}</div>
<div class="stat-subtext">¥${(avgPrice * 150).toFixed(0)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Price Range</div>
<div class="stat-value">$${minPrice.toFixed(0)}</div>
<div class="stat-subtext">to $${maxPrice.toFixed(0)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Profit Opportunities</div>
<div class="stat-value">${dealsCount}</div>
<div class="stat-subtext">30%+ potential margin</div>
</div>
`;
}
function updateChart(listings) {
if (!listings || listings.length === 0) return;
const priceRanges = {
'$0-5k': 0,
'$5-10k': 0,
'$10-15k': 0,
'$15-20k': 0,
'$20-30k': 0,
'$30k+': 0
};
listings.forEach(listing => {
const price = listing.price_usd;
if (price < 5000) priceRanges['$0-5k']++;
else if (price < 10000) priceRanges['$5-10k']++;
else if (price < 15000) priceRanges['$10-15k']++;
else if (price < 20000) priceRanges['$15-20k']++;
else if (price < 30000) priceRanges['$20-30k']++;
else priceRanges['$30k+']++;
});
const ctx = document.getElementById('priceChart').getContext('2d');
if (priceChart) {
priceChart.destroy();
}
priceChart = new Chart(ctx, {
type: 'bar',
data: {
labels: Object.keys(priceRanges),
datasets: [{
label: 'Number of Birkins',
data: Object.values(priceRanges),
backgroundColor: 'rgba(102, 126, 234, 0.8)',
borderColor: 'rgba(102, 126, 234, 1)',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
}
function renderListings(listings) {
if (!listings || listings.length === 0) {
document.getElementById('listings').innerHTML = '<div class="loading">No Birkins found. Waiting for crawler...</div>';
return;
}
const html = listings.map(listing => {
const usPrice = estimateUSPrice(listing.title);
const { profit, percentage } = calculateProfitPotential(listing.price_usd, usPrice.avg);
let profitClass = 'profit-low';
let profitLabel = 'Low Margin';
if (parseInt(percentage) >= 50) {
profitClass = 'profit-high';
profitLabel = `${percentage}% Profit Potential`;
} else if (parseInt(percentage) >= 30) {
profitClass = 'profit-medium';
profitLabel = `${percentage}% Profit Potential`;
}
return `
<div class="listing-card">
<img src="${listing.thumbnail_url || listing.image_url || '/placeholder.jpg'}"
alt="${listing.title}"
class="listing-image"
onerror="this.src='/placeholder.jpg'">
<div class="listing-content">
<div class="listing-title">${listing.title}</div>
<div class="listing-price">$${listing.price_usd.toFixed(0)}</div>
<div class="listing-jpy">¥${listing.price_jpy.toFixed(0)}</div>
${parseInt(percentage) >= 30 ? `<span class="profit-badge ${profitClass}">${profitLabel}</span>` : ''}
<div class="listing-condition">Condition: ${listing.condition || 'Used'}</div>
<div style="font-size: 0.875rem; color: #888; margin-bottom: 12px;">
US Market: $${usPrice.min.toFixed(0)} - $${usPrice.max.toFixed(0)}
</div>
<a href="${listing.product_url}" target="_blank" rel="noopener noreferrer" class="listing-link">View on ${listing.source}</a>
</div>
</div>
`;
}).join('');
document.getElementById('listings').innerHTML = html;
}
function applyFilters() {
let filtered = [...allListings];
const condition = document.getElementById('conditionFilter').value;
const minPrice = parseFloat(document.getElementById('minPrice').value) || 0;
const maxPrice = parseFloat(document.getElementById('maxPrice').value) || Infinity;
const sortBy = document.getElementById('sortBy').value;
if (condition) {
filtered = filtered.filter(l =>
l.condition && l.condition.toLowerCase().includes(condition.toLowerCase())
);
}
filtered = filtered.filter(l =>
l.price_usd >= minPrice && l.price_usd <= maxPrice
);
if (sortBy === 'price_low') {
filtered.sort((a, b) => a.price_usd - b.price_usd);
} else if (sortBy === 'price_high') {
filtered.sort((a, b) => b.price_usd - a.price_usd);
} else if (sortBy === 'profit') {
filtered.sort((a, b) => {
const profitA = calculateProfitPotential(a.price_usd, estimateUSPrice(a.title).avg).percentage;
const profitB = calculateProfitPotential(b.price_usd, estimateUSPrice(b.title).avg).percentage;
return profitB - profitA;
});
} else if (sortBy === 'date') {
filtered.sort((a, b) => new Date(b.crawled_at) - new Date(a.crawled_at));
}
renderListings(filtered);
}
// Load data on page load
loadData();
// Auto-refresh every 30 seconds
setInterval(loadData, 30000);
</script>
</body>
</html>