← 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>