← back to Watches

public/app.js

367 lines

// Global variables
let allWatches = [];
let filteredWatches = [];
let selectedWatches = new Set();
let priceChart = null;
let watchColors = {};

// Color palette for charts
const colorPalette = [
    '#c41e3a', '#2e86ab', '#a23b72', '#f18f01', '#6a994e',
    '#bc4749', '#264653', '#e76f51', '#e9c46a', '#457b9d'
];

// Initialize application
document.addEventListener('DOMContentLoaded', () => {
    loadStatistics();
    loadWatches();
    setupEventListeners();
});

// Setup event listeners
function setupEventListeners() {
    // Search functionality
    document.getElementById('searchInput').addEventListener('input', (e) => {
        filterWatches(e.target.value);
    });

    // Filter buttons
    document.querySelectorAll('.filter-btn').forEach(btn => {
        btn.addEventListener('click', (e) => {
            document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
            e.target.classList.add('active');
            filterByCollection(e.target.dataset.collection);
        });
    });

    // Clear chart button
    document.getElementById('clearChart').addEventListener('click', clearChart);

    // Export data button
    document.getElementById('exportData').addEventListener('click', exportData);
}

// Load statistics
async function loadStatistics() {
    try {
        const response = await fetch('/api/statistics');
        const stats = await response.json();

        // Update stat cards
        document.getElementById('totalModels').textContent = stats.totalModels;

        // Calculate overall average price
        let totalPrice = 0;
        let count = 0;
        Object.values(stats.averagePrices).forEach(price => {
            totalPrice += price;
            count++;
        });
        const avgPrice = Math.round(totalPrice / count);
        document.getElementById('avgPrice').textContent = `$${avgPrice.toLocaleString()}`;

        // Top appreciation
        if (stats.topAppreciating.length > 0) {
            document.getElementById('topAppreciation').textContent =
                `+${stats.topAppreciating[0].appreciation}%`;
        }

        // Load top appreciating list
        const appreciationList = document.getElementById('topAppreciatingList');
        appreciationList.innerHTML = stats.topAppreciating.slice(0, 6).map(watch => `
            <div class="appreciation-item">
                <div class="appreciation-info">
                    <div class="appreciation-name">${watch.name}</div>
                    <div class="appreciation-details">
                        $${watch.firstPrice.toLocaleString()} → $${watch.lastPrice.toLocaleString()}
                    </div>
                </div>
                <div class="appreciation-percent">+${watch.appreciation}%</div>
            </div>
        `).join('');

    } catch (error) {
        console.error('Error loading statistics:', error);
    }
}

// Load watches
async function loadWatches() {
    try {
        const response = await fetch('/api/watches?limit=9999');
        const data = await response.json();
        allWatches = Array.isArray(data) ? data : (data.watches || []);
        filteredWatches = [...allWatches];

        // Count total data points
        let dataPoints = 0;
        allWatches.forEach(watch => {
            // We need to fetch each watch to get its price history count
            fetch(`/api/watches/${watch.id}`).then(res => res.json()).then(data => {
                dataPoints += data.priceHistory.length;
                document.getElementById('dataPoints').textContent = dataPoints;
            });
        });

        renderWatchList();
        initializeChart();
    } catch (error) {
        console.error('Error loading watches:', error);
    }
}

// Render watch list
function renderWatchList() {
    const watchesList = document.getElementById('watchesList');

    if (filteredWatches.length === 0) {
        watchesList.innerHTML = '<div class="loading">No watches found</div>';
        return;
    }

    watchesList.innerHTML = filteredWatches.map(watch => {
        const isSelected = selectedWatches.has(watch.id);
        return `
            <div class="watch-item ${isSelected ? 'selected' : ''}"
                 data-id="${watch.id}"
                 onclick="toggleWatch('${watch.id}')">
                <div class="watch-name">${watch.name}</div>
                <div class="watch-details">
                    <span class="watch-collection">${watch.collection}</span>
                    <span class="watch-year">Since ${watch.year_introduced}</span>
                </div>
            </div>
        `;
    }).join('');
}

// Toggle watch selection
async function toggleWatch(watchId) {
    const watchItem = document.querySelector(`[data-id="${watchId}"]`);

    if (selectedWatches.has(watchId)) {
        selectedWatches.delete(watchId);
        watchItem.classList.remove('selected');
        removeFromChart(watchId);
    } else {
        if (selectedWatches.size >= 5) {
            alert('Maximum 5 watches can be compared at once');
            return;
        }
        selectedWatches.add(watchId);
        watchItem.classList.add('selected');
        await addToChart(watchId);
    }

    updateSelectedWatchesList();
}

// Add watch to chart
async function addToChart(watchId) {
    try {
        const response = await fetch(`/api/watches/${watchId}`);
        const watchData = await response.json();

        // Assign color if not already assigned
        if (!watchColors[watchId]) {
            const usedColors = Object.values(watchColors);
            const availableColors = colorPalette.filter(c => !usedColors.includes(c));
            watchColors[watchId] = availableColors[0] || colorPalette[selectedWatches.size % colorPalette.length];
        }

        const dataset = {
            label: watchData.name,
            data: watchData.priceHistory.map(point => ({
                x: point.year,
                y: point.price
            })),
            borderColor: watchColors[watchId],
            backgroundColor: watchColors[watchId] + '20',
            tension: 0.2,
            borderWidth: 2,
            pointRadius: 4,
            pointHoverRadius: 6
        };

        priceChart.data.datasets.push(dataset);
        priceChart.update();
    } catch (error) {
        console.error('Error adding watch to chart:', error);
    }
}

// Remove watch from chart
function removeFromChart(watchId) {
    const watch = allWatches.find(w => w.id === watchId);
    if (watch) {
        const datasetIndex = priceChart.data.datasets.findIndex(ds => ds.label === watch.name);
        if (datasetIndex !== -1) {
            priceChart.data.datasets.splice(datasetIndex, 1);
            priceChart.update();
        }
        delete watchColors[watchId];
    }
}

// Update selected watches list
function updateSelectedWatchesList() {
    const selectedWatchesDiv = document.getElementById('selectedWatches');

    if (selectedWatches.size === 0) {
        selectedWatchesDiv.innerHTML = '<div style="color: #999;">No watches selected</div>';
        return;
    }

    selectedWatchesDiv.innerHTML = Array.from(selectedWatches).map(watchId => {
        const watch = allWatches.find(w => w.id === watchId);
        return `
            <div class="selected-watch-tag" style="border-left: 4px solid ${watchColors[watchId]}">
                ${watch.name}
                <span class="remove" onclick="toggleWatch('${watchId}')">×</span>
            </div>
        `;
    }).join('');
}

// Initialize chart
function initializeChart() {
    const ctx = document.getElementById('priceChart').getContext('2d');

    priceChart = new Chart(ctx, {
        type: 'line',
        data: {
            datasets: []
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
                title: {
                    display: false
                },
                legend: {
                    display: true,
                    position: 'top'
                },
                tooltip: {
                    callbacks: {
                        label: function(context) {
                            return context.dataset.label + ': $' + context.parsed.y.toLocaleString();
                        }
                    }
                }
            },
            scales: {
                x: {
                    type: 'linear',
                    title: {
                        display: true,
                        text: 'Year'
                    },
                    ticks: {
                        stepSize: 5,
                        callback: function(value) {
                            return value;
                        }
                    }
                },
                y: {
                    title: {
                        display: true,
                        text: 'Price (USD)'
                    },
                    ticks: {
                        callback: function(value) {
                            return '$' + value.toLocaleString();
                        }
                    }
                }
            },
            interaction: {
                mode: 'index',
                intersect: false
            }
        }
    });

    updateSelectedWatchesList();
}

// Filter watches by search term
function filterWatches(searchTerm) {
    const term = searchTerm.toLowerCase();
    const activeCollection = document.querySelector('.filter-btn.active').dataset.collection;

    filteredWatches = allWatches.filter(watch => {
        const matchesSearch = watch.name.toLowerCase().includes(term) ||
                             watch.reference?.toLowerCase().includes(term) ||
                             watch.description?.toLowerCase().includes(term);
        const matchesCollection = activeCollection === 'all' || watch.collection === activeCollection;

        return matchesSearch && matchesCollection;
    });

    renderWatchList();
}

// Filter by collection
function filterByCollection(collection) {
    const searchTerm = document.getElementById('searchInput').value;

    if (collection === 'all') {
        filteredWatches = searchTerm ?
            allWatches.filter(w => w.name.toLowerCase().includes(searchTerm.toLowerCase())) :
            [...allWatches];
    } else {
        filteredWatches = allWatches.filter(watch => {
            const matchesCollection = watch.collection === collection;
            const matchesSearch = !searchTerm ||
                watch.name.toLowerCase().includes(searchTerm.toLowerCase());
            return matchesCollection && matchesSearch;
        });
    }

    renderWatchList();
}

// Clear chart
function clearChart() {
    selectedWatches.clear();
    watchColors = {};
    priceChart.data.datasets = [];
    priceChart.update();

    document.querySelectorAll('.watch-item').forEach(item => {
        item.classList.remove('selected');
    });

    updateSelectedWatchesList();
}

// Export data
function exportData() {
    if (selectedWatches.size === 0) {
        alert('Please select at least one watch to export data');
        return;
    }

    // Create CSV data
    const csvRows = ['Watch Model,Year,Price (USD)'];

    priceChart.data.datasets.forEach(dataset => {
        dataset.data.forEach(point => {
            csvRows.push(`"${dataset.label}",${point.x},${point.y}`);
        });
    });

    const csvContent = csvRows.join('\n');
    const blob = new Blob([csvContent], { type: 'text/csv' });
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `omega-watch-prices-${new Date().toISOString().split('T')[0]}.csv`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    window.URL.revokeObjectURL(url);
}