← back to Watches
public/search-interface.html
909 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Omega Watch Search - Advanced Search & Filters</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: white;
padding: 40px;
text-align: center;
}
h1 {
font-size: 2.5em;
font-weight: 700;
margin-bottom: 10px;
}
.subtitle {
opacity: 0.9;
font-size: 1.1em;
}
/* Search Section */
.search-section {
background: #f8f9fa;
padding: 30px;
border-bottom: 2px solid #e9ecef;
}
.search-container {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.search-input-wrapper {
flex: 1;
position: relative;
}
.search-input {
width: 100%;
padding: 15px 20px 15px 50px;
font-size: 16px;
border: 2px solid #dee2e6;
border-radius: 10px;
transition: all 0.3s;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-icon {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
}
.search-btn {
padding: 15px 30px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.search-btn:hover {
transform: translateY(-2px);
}
.clear-btn {
padding: 15px 30px;
background: #6c757d;
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
}
/* Sort Section */
.sort-section {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.sort-label {
font-weight: 600;
color: #495057;
}
.sort-select {
padding: 10px 20px;
font-size: 15px;
border: 2px solid #dee2e6;
border-radius: 8px;
background: white;
cursor: pointer;
min-width: 200px;
}
/* Filter Buttons */
.filter-section {
margin-bottom: 20px;
}
.filter-group {
margin-bottom: 15px;
}
.filter-label {
font-weight: 600;
color: #495057;
margin-bottom: 10px;
display: block;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.filter-btn {
padding: 10px 20px;
border: 2px solid #dee2e6;
background: white;
border-radius: 25px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn:hover {
border-color: #667eea;
background: #f8f9ff;
}
.filter-btn.active {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-color: transparent;
}
/* Price Range */
.price-range-container {
display: flex;
gap: 15px;
align-items: center;
}
.price-input {
width: 150px;
padding: 10px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 14px;
}
/* Year Range */
.year-range-container {
display: flex;
gap: 15px;
align-items: center;
}
.year-input {
width: 100px;
padding: 10px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 14px;
}
/* Quick Filters */
.quick-filters {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.quick-filter-btn {
padding: 8px 16px;
background: #e3f2fd;
border: 1px solid #90caf9;
border-radius: 20px;
color: #1976d2;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.quick-filter-btn:hover {
background: #bbdefb;
}
/* Results Section */
.results-section {
padding: 30px;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.results-count {
font-size: 18px;
font-weight: 600;
color: #495057;
}
.view-toggle {
display: flex;
gap: 10px;
}
.view-btn {
padding: 8px 15px;
border: 1px solid #dee2e6;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.view-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
/* Watch Grid */
:root { --watch-card-min: 300px; }
.watch-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--watch-card-min), 1fr));
gap: 20px;
}
.watch-card {
background: white;
border: 1px solid #e9ecef;
border-radius: 15px;
padding: 20px;
transition: all 0.3s;
cursor: pointer;
}
.watch-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.watch-name {
font-size: 18px;
font-weight: 600;
color: #212529;
margin-bottom: 10px;
}
.watch-collection {
display: inline-block;
padding: 4px 10px;
background: #e3f2fd;
border-radius: 15px;
font-size: 12px;
color: #1976d2;
margin-bottom: 10px;
}
.watch-price {
font-size: 24px;
font-weight: 700;
color: #667eea;
margin-bottom: 5px;
}
.watch-appreciation {
font-size: 14px;
color: #28a745;
}
.watch-year {
font-size: 13px;
color: #6c757d;
}
/* Loading */
.loading {
text-align: center;
padding: 50px;
color: #6c757d;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Facets Sidebar */
.facets-sidebar {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
.facet-group {
margin-bottom: 20px;
}
.facet-title {
font-weight: 600;
color: #495057;
margin-bottom: 10px;
}
.facet-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
cursor: pointer;
transition: color 0.2s;
}
.facet-item:hover {
color: #667eea;
}
.facet-count {
background: #e9ecef;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
}
/* Responsive */
@media (max-width: 768px) {
.filter-buttons {
flex-direction: column;
}
.search-container {
flex-direction: column;
}
.watch-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🔍 Omega Watch Advanced Search</h1>
<p class="subtitle">Deep Search with SQLite FTS5 - Find Any Watch Instantly</p>
</header>
<div class="search-section">
<!-- Main Search Bar -->
<div class="search-container">
<div class="search-input-wrapper">
<span class="search-icon">🔍</span>
<input type="text" id="searchInput" class="search-input"
placeholder="Search watches, collections, references..."
autocomplete="off">
</div>
<button class="search-btn" onclick="performSearch()">Search</button>
<button class="clear-btn" onclick="clearAll()">Clear All</button>
</div>
<!-- Sort + Density Controls -->
<div class="sort-section" style="display:flex; gap:24px; align-items:center; flex-wrap:wrap;">
<div style="display:flex; gap:8px; align-items:center;">
<label class="sort-label" for="sortSelect">Sort by:</label>
<select id="sortSelect" class="sort-select" onchange="onSortChange()">
<option value="year_desc">Newest (Year)</option>
<option value="year_asc">Oldest (Year)</option>
<option value="relevance">Relevance</option>
<option value="name_asc">Model A→Z</option>
<option value="name_desc">Model Z→A</option>
<option value="price_asc">Price: Low to High</option>
<option value="price_desc">Price: High to Low</option>
<option value="appreciation_desc">Highest Appreciation</option>
</select>
</div>
<div style="display:flex; gap:8px; align-items:center;">
<label class="sort-label" for="densityRange">Density:</label>
<input type="range" id="densityRange" min="200" max="480" step="20" value="300" oninput="onDensityChange(this.value)" style="width:160px;">
<span id="densityVal" style="font-size:12px; color:#6c757d; min-width:48px;">300px</span>
</div>
</div>
<!-- Collection Filter Buttons -->
<div class="filter-group">
<label class="filter-label">Collections:</label>
<div class="filter-buttons" id="collectionButtons">
<button class="filter-btn active" data-collection="all" onclick="filterCollection('all')">All Collections</button>
<button class="filter-btn" data-collection="Speedmaster" onclick="filterCollection('Speedmaster')">Speedmaster</button>
<button class="filter-btn" data-collection="Seamaster" onclick="filterCollection('Seamaster')">Seamaster</button>
<button class="filter-btn" data-collection="Constellation" onclick="filterCollection('Constellation')">Constellation</button>
<button class="filter-btn" data-collection="De Ville" onclick="filterCollection('De Ville')">De Ville</button>
<button class="filter-btn" data-collection="Railmaster" onclick="filterCollection('Railmaster')">Railmaster</button>
<button class="filter-btn" data-collection="Flightmaster" onclick="filterCollection('Flightmaster')">Flightmaster</button>
</div>
</div>
<!-- Price Range Filter -->
<div class="filter-group">
<label class="filter-label">Price Range:</label>
<div class="price-range-container">
<input type="number" id="minPrice" class="price-input" placeholder="Min $">
<span>to</span>
<input type="number" id="maxPrice" class="price-input" placeholder="Max $">
<button class="filter-btn" onclick="performSearch()">Apply</button>
</div>
</div>
<!-- Year Range Filter -->
<div class="filter-group">
<label class="filter-label">Year Introduced:</label>
<div class="year-range-container">
<input type="number" id="yearFrom" class="year-input" placeholder="From" min="1950" max="2024">
<span>to</span>
<input type="number" id="yearTo" class="year-input" placeholder="To" min="1950" max="2024">
<button class="filter-btn" onclick="performSearch()">Apply</button>
</div>
</div>
<!-- Quick Filters -->
<div class="filter-group">
<label class="filter-label">Quick Filters:</label>
<div class="quick-filters">
<button class="quick-filter-btn" onclick="quickFilter('under5k')">Under $5,000</button>
<button class="quick-filter-btn" onclick="quickFilter('5kto10k')">$5,000 - $10,000</button>
<button class="quick-filter-btn" onclick="quickFilter('over10k')">Over $10,000</button>
<button class="quick-filter-btn" onclick="quickFilter('vintage')">Vintage (Pre-1980)</button>
<button class="quick-filter-btn" onclick="quickFilter('modern')">Modern (2000+)</button>
<button class="quick-filter-btn" onclick="quickFilter('highAppreciation')">High Appreciation (50%+)</button>
<button class="quick-filter-btn" onclick="quickFilter('moonwatch')">Moonwatch</button>
<button class="quick-filter-btn" onclick="quickFilter('chronograph')">Chronograph</button>
</div>
</div>
</div>
<!-- Facets Sidebar (populated dynamically) -->
<div id="facetsSidebar" class="facets-sidebar" style="display: none;">
<!-- Facets will be populated here -->
</div>
<!-- Results Section -->
<div class="results-section">
<div class="results-header">
<div class="results-count">
<span id="resultCount">0</span> watches found
</div>
<div class="view-toggle">
<button class="view-btn active" onclick="setView('grid')">Grid</button>
<button class="view-btn" onclick="setView('list')">List</button>
<button class="view-btn" onclick="setView('chart')">Chart</button>
</div>
</div>
<!-- Loading Spinner -->
<div id="loading" class="loading" style="display: none;">
<div class="spinner"></div>
<p>Searching watches...</p>
</div>
<!-- Results Grid -->
<div id="watchGrid" class="watch-grid">
<!-- Watch cards will be populated here -->
</div>
<!-- Chart Container -->
<canvas id="priceChart" style="display: none; margin-top: 20px;"></canvas>
</div>
</div>
<script>
const API_BASE = 'http://45.61.58.125:7600';
let currentCollection = 'all';
let currentView = 'grid';
let chart = null;
// ---- Sort + density persistence (run BEFORE first performSearch) ----
const LS_SORT = 'watches.search.sort';
const LS_DENSITY = 'watches.search.density';
function hydrateControls() {
try {
const savedSort = localStorage.getItem(LS_SORT);
if (savedSort) {
const sel = document.getElementById('sortSelect');
if ([...sel.options].some(o => o.value === savedSort)) sel.value = savedSort;
}
const savedDensity = parseInt(localStorage.getItem(LS_DENSITY), 10);
if (savedDensity >= 200 && savedDensity <= 480) {
document.getElementById('densityRange').value = String(savedDensity);
applyDensity(savedDensity);
} else {
applyDensity(parseInt(document.getElementById('densityRange').value, 10));
}
} catch (e) { /* localStorage may be unavailable */ }
}
function applyDensity(px) {
document.documentElement.style.setProperty('--watch-card-min', px + 'px');
const lbl = document.getElementById('densityVal');
if (lbl) lbl.textContent = px + 'px';
}
function onSortChange() {
try { localStorage.setItem(LS_SORT, document.getElementById('sortSelect').value); } catch (e) {}
performSearch();
}
function onDensityChange(v) {
const px = parseInt(v, 10);
applyDensity(px);
try { localStorage.setItem(LS_DENSITY, String(px)); } catch (e) {}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
hydrateControls();
performSearch();
setupAutocomplete();
});
// Perform search with all filters
async function performSearch() {
const searchQuery = document.getElementById('searchInput').value;
const sort = document.getElementById('sortSelect').value;
const minPrice = document.getElementById('minPrice').value;
const maxPrice = document.getElementById('maxPrice').value;
const yearFrom = document.getElementById('yearFrom').value;
const yearTo = document.getElementById('yearTo').value;
// Build query parameters
const params = new URLSearchParams();
if (searchQuery) params.append('q', searchQuery);
if (currentCollection !== 'all') params.append('collection', currentCollection);
if (minPrice) params.append('minPrice', minPrice);
if (maxPrice) params.append('maxPrice', maxPrice);
if (yearFrom) params.append('yearFrom', yearFrom);
if (yearTo) params.append('yearTo', yearTo);
params.append('sort', sort);
params.append('facets', 'true');
params.append('limit', '50');
// Show loading
showLoading();
try {
const response = await fetch(`${API_BASE}/api/advanced-search?${params}`);
const data = await response.json();
displayResults(data.results);
updateResultCount(data.total);
if (data.facets) {
displayFacets(data.facets);
}
} catch (error) {
console.error('Search error:', error);
showError('Failed to search watches. Please try again.');
} finally {
hideLoading();
}
}
// Filter by collection
function filterCollection(collection) {
currentCollection = collection;
// Update button states
document.querySelectorAll('#collectionButtons .filter-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.collection === collection) {
btn.classList.add('active');
}
});
performSearch();
}
// Quick filters
function quickFilter(type) {
const minPriceInput = document.getElementById('minPrice');
const maxPriceInput = document.getElementById('maxPrice');
const yearFromInput = document.getElementById('yearFrom');
const yearToInput = document.getElementById('yearTo');
const searchInput = document.getElementById('searchInput');
// Reset inputs
minPriceInput.value = '';
maxPriceInput.value = '';
yearFromInput.value = '';
yearToInput.value = '';
searchInput.value = '';
switch(type) {
case 'under5k':
maxPriceInput.value = 5000;
break;
case '5kto10k':
minPriceInput.value = 5000;
maxPriceInput.value = 10000;
break;
case 'over10k':
minPriceInput.value = 10000;
break;
case 'vintage':
yearToInput.value = 1979;
break;
case 'modern':
yearFromInput.value = 2000;
break;
case 'highAppreciation':
// This would need a backend filter for appreciation rate
searchInput.value = 'high appreciation';
break;
case 'moonwatch':
searchInput.value = 'moonwatch';
break;
case 'chronograph':
searchInput.value = 'chronograph';
break;
}
performSearch();
}
// Display results
function displayResults(watches) {
const grid = document.getElementById('watchGrid');
grid.innerHTML = '';
if (watches.length === 0) {
grid.innerHTML = '<p style="text-align: center; color: #6c757d; padding: 40px;">No watches found. Try adjusting your filters.</p>';
return;
}
watches.forEach(watch => {
const card = document.createElement('div');
card.className = 'watch-card';
card.innerHTML = `
<div class="watch-name">${watch.highlighted_name || watch.name}</div>
<div class="watch-collection">${watch.collection || 'N/A'}</div>
<div class="watch-price">$${watch.avg_price ? watch.avg_price.toLocaleString() : 'N/A'}</div>
${watch.appreciation_rate ? `<div class="watch-appreciation">+${watch.appreciation_rate.toFixed(1)}% appreciation</div>` : ''}
<div class="watch-year">Introduced: ${watch.year_introduced || 'Unknown'}</div>
${watch.text_snippet ? `<div style="margin-top: 10px; font-size: 13px; color: #6c757d;">${watch.text_snippet}</div>` : ''}
`;
grid.appendChild(card);
});
// Update chart if in chart view
if (currentView === 'chart' && watches.length > 0) {
updateChart(watches);
}
}
// Display facets
function displayFacets(facets) {
const sidebar = document.getElementById('facetsSidebar');
if (!facets || Object.keys(facets).length === 0) {
sidebar.style.display = 'none';
return;
}
let facetsHTML = '';
// Collections facet
if (facets.collections && facets.collections.length > 0) {
facetsHTML += '<div class="facet-group">';
facetsHTML += '<div class="facet-title">Collections</div>';
facets.collections.forEach(item => {
facetsHTML += `
<div class="facet-item" onclick="filterCollection('${item.collection}')">
<span>${item.collection || 'Unknown'}</span>
<span class="facet-count">${item.count}</span>
</div>
`;
});
facetsHTML += '</div>';
}
// Price ranges facet
if (facets.priceRanges && facets.priceRanges.length > 0) {
facetsHTML += '<div class="facet-group">';
facetsHTML += '<div class="facet-title">Price Ranges</div>';
facets.priceRanges.forEach(range => {
if (range.count > 0) {
facetsHTML += `
<div class="facet-item" onclick="setPriceRange(${range.min}, ${range.max})">
<span>${range.label}</span>
<span class="facet-count">${range.count}</span>
</div>
`;
}
});
facetsHTML += '</div>';
}
// Decades facet
if (facets.decades && facets.decades.length > 0) {
facetsHTML += '<div class="facet-group">';
facetsHTML += '<div class="facet-title">Decades</div>';
facets.decades.forEach(item => {
facetsHTML += `
<div class="facet-item">
<span>${item.decade}</span>
<span class="facet-count">${item.count}</span>
</div>
`;
});
facetsHTML += '</div>';
}
sidebar.innerHTML = facetsHTML;
sidebar.style.display = facetsHTML ? 'block' : 'none';
}
// Set price range from facet
function setPriceRange(min, max) {
document.getElementById('minPrice').value = min;
document.getElementById('maxPrice').value = max < 999999999 ? max : '';
performSearch();
}
// Update result count
function updateResultCount(count) {
document.getElementById('resultCount').textContent = count;
}
// Clear all filters
function clearAll() {
document.getElementById('searchInput').value = '';
document.getElementById('minPrice').value = '';
document.getElementById('maxPrice').value = '';
document.getElementById('yearFrom').value = '';
document.getElementById('yearTo').value = '';
document.getElementById('sortSelect').value = 'relevance';
filterCollection('all');
}
// Set view type
function setView(view) {
currentView = view;
// Update button states
document.querySelectorAll('.view-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Toggle displays
const grid = document.getElementById('watchGrid');
const chartCanvas = document.getElementById('priceChart');
if (view === 'chart') {
grid.style.display = 'none';
chartCanvas.style.display = 'block';
performSearch(); // Refresh to update chart
} else {
grid.style.display = 'grid';
chartCanvas.style.display = 'none';
}
}
// Update chart
function updateChart(watches) {
const ctx = document.getElementById('priceChart').getContext('2d');
if (chart) {
chart.destroy();
}
chart = new Chart(ctx, {
type: 'bar',
data: {
labels: watches.map(w => w.name),
datasets: [{
label: 'Average Price ($)',
data: watches.map(w => w.avg_price || 0),
backgroundColor: 'rgba(102, 126, 234, 0.6)',
borderColor: 'rgba(102, 126, 234, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return '$' + value.toLocaleString();
}
}
}
}
}
});
}
// Setup autocomplete
async function setupAutocomplete() {
const input = document.getElementById('searchInput');
let timeout;
input.addEventListener('input', async (e) => {
clearTimeout(timeout);
const query = e.target.value;
if (query.length < 2) return;
timeout = setTimeout(async () => {
try {
const response = await fetch(`${API_BASE}/api/search/suggestions?q=${query}`);
const data = await response.json();
// You could display suggestions in a dropdown here
console.log('Suggestions:', data.suggestions);
} catch (error) {
console.error('Autocomplete error:', error);
}
}, 300);
});
// Search on Enter key
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
}
// Loading states
function showLoading() {
document.getElementById('loading').style.display = 'block';
document.getElementById('watchGrid').style.display = 'none';
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
document.getElementById('watchGrid').style.display = 'grid';
}
function showError(message) {
const grid = document.getElementById('watchGrid');
grid.innerHTML = `<p style="text-align: center; color: #dc3545; padding: 40px;">${message}</p>`;
}
</script>
</body>
</html>