← 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);
}