← back to NEW SKU Viewer
public/index.html
983 lines
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NEW-SKU Vendor Viewer | Live Product Scanner</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 20px 0;
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.title {
color: #2d3748;
}
.title h1 {
font-size: 2.5rem;
margin-bottom: 5px;
background: linear-gradient(45deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.title p {
color: #666;
font-size: 1.1rem;
}
.controls {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.stats {
display: flex;
gap: 20px;
margin: 20px 0;
flex-wrap: wrap;
}
.stat-card {
background: rgba(255, 255, 255, 0.9);
padding: 15px 25px;
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
min-width: 120px;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #667eea;
}
.stat-label {
color: #666;
font-size: 0.9rem;
margin-top: 5px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.9);
color: #667eea;
border: 2px solid #667eea;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.content {
padding: 20px 0;
}
.vendor-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--card-min, 350px), 1fr));
gap: 20px;
margin-top: 20px;
}
.grid-controls {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 16px;
padding: 10px 14px;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(10px);
border-radius: 10px;
}
.grid-controls label {
font-size: 0.85rem;
color: #4a5568;
font-weight: 500;
}
.grid-controls select {
padding: 6px 10px;
border-radius: 6px;
border: 1px solid #cbd5e0;
background: #fff;
font-size: 0.9rem;
color: #2d3748;
}
.grid-controls input[type="range"] {
width: 160px;
}
.vendor-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
border-left: 4px solid transparent;
}
.vendor-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.vendor-card.success {
border-left-color: #10b981;
}
.vendor-card.error {
border-left-color: #ef4444;
}
.vendor-card.warning {
border-left-color: #f59e0b;
}
.vendor-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.vendor-name {
font-size: 1.2rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 5px;
}
.vendor-id {
color: #666;
font-size: 0.9rem;
font-family: monospace;
}
.status-badge {
padding: 4px 8px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
}
.status-success {
background: #d1fae5;
color: #065f46;
}
.status-error {
background: #fee2e2;
color: #991b1b;
}
.status-warning {
background: #fef3c7;
color: #92400e;
}
.product-count {
font-size: 2rem;
font-weight: bold;
margin: 10px 0;
}
.product-count.has-products {
color: #10b981;
}
.product-count.no-products {
color: #6b7280;
}
.vendor-url {
color: #666;
font-size: 0.9rem;
word-break: break-all;
margin-bottom: 10px;
}
.error-message {
color: #ef4444;
font-size: 0.9rem;
margin-top: 10px;
padding: 8px;
background: #fee2e2;
border-radius: 6px;
}
.vendor-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn-small {
padding: 6px 12px;
font-size: 0.8rem;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: white;
}
.spinner {
width: 50px;
height: 50px;
border: 3px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin-bottom: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.last-update {
color: rgba(255,255,255,0.8);
font-size: 0.9rem;
margin-top: 10px;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-btn {
padding: 8px 16px;
border: 2px solid rgba(255,255,255,0.3);
background: rgba(255,255,255,0.1);
color: white;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
}
.filter-btn.active {
background: rgba(255,255,255,0.9);
color: #667eea;
}
.search-box {
margin-bottom: 20px;
}
.search-input {
width: 100%;
max-width: 400px;
padding: 12px 20px;
border: none;
border-radius: 25px;
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
font-size: 1rem;
outline: none;
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
text-align: center;
}
.title h1 {
font-size: 2rem;
}
.vendor-grid {
grid-template-columns: 1fr;
}
.stats {
justify-content: center;
}
}
/* ── Grid / List toggle ── */
.vtoggle { display: flex; border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; overflow: hidden; }
.vtoggle button { background: rgba(255,255,255,0.1); color: white; border: none; padding: 8px 16px; font-size: 13px; cursor: pointer; font-weight: 500; }
.vtoggle button.on { background: rgba(255,255,255,0.9); color: #667eea; font-weight: 700; }
/* ── List view ── */
.vendor-grid.list { display: block; }
.vendor-grid.list .vendor-card { display: flex; flex-direction: row; align-items: center; gap: 18px; margin-bottom: 10px; border-left-width: 4px; border-radius: 8px; padding: 12px 18px; }
.vendor-grid.list .vendor-card .vendor-header { flex: 0 0 220px; margin-bottom: 0; }
.vendor-grid.list .vendor-card .product-count { font-size: 1.4rem; margin: 0; }
.vendor-grid.list .vendor-card .vendor-url { flex: 1; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vendor-grid.list .vendor-card .vendor-actions { margin-top: 0; flex: none; }
/* ── Fields menu ── */
.fieldmenu { position: relative; }
.fbtn { padding: 8px 16px; border: 1px solid rgba(255,255,255,0.3); background: rgba(255,255,255,0.1); color: white; font-size: 12px; cursor: pointer; border-radius: 8px; }
.fbtn:hover { border-color: rgba(255,255,255,0.7); }
.fpop { position: absolute; top: calc(100% + 8px); right: 0; background: white; border: 1px solid #e2e8f0; border-radius: 8px; box-shadow: 0 12px 30px rgba(0,0,0,0.2); padding: 8px 4px; z-index: 30; min-width: 190px; }
.fpop[hidden] { display: none; }
.fpop label { display: flex; align-items: center; gap: 10px; padding: 7px 14px; font-size: 12px; color: #4a5568; cursor: pointer; white-space: nowrap; }
.fpop label:hover { background: #f7fafc; }
.fpop input { accent-color: #667eea; }
/* ── Field-hide rules (grid mode only) ── */
.vendor-grid:not(.list).hf-count .vendor-card .product-count { display: none; }
.vendor-grid:not(.list).hf-url .vendor-card .vendor-url { display: none; }
.vendor-grid:not(.list).hf-scraper .vendor-card .nsk-scraper { display: none; }
.vendor-grid:not(.list).hf-error .vendor-card .error-message { display: none; }
.vendor-grid:not(.list).hf-actions .vendor-card .vendor-actions { display: none; }
</style>
</head>
<body>
<div class="header">
<div class="container">
<div class="header-content">
<div class="title">
<h1><i class="fas fa-search"></i> NEW-SKU Vendor Viewer</h1>
<p>Live product scanner for all wallcovering vendors</p>
</div>
<div class="controls">
<button class="btn btn-primary" onclick="startScan()" id="scanBtn">
<i class="fas fa-sync"></i> Scan All Vendors
</button>
<button class="btn btn-secondary" onclick="refreshData()">
<i class="fas fa-refresh"></i> Refresh
</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="stats" id="stats">
<div class="stat-card">
<div class="stat-value" id="totalVendors">-</div>
<div class="stat-label">Total Vendors</div>
</div>
<div class="stat-card">
<div class="stat-value" id="successfulVendors">-</div>
<div class="stat-label">Working</div>
</div>
<div class="stat-card">
<div class="stat-value" id="vendorsWithProducts">-</div>
<div class="stat-label">With Products</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalProducts">-</div>
<div class="stat-label">Total Products</div>
</div>
</div>
<div class="content">
<div class="search-box">
<input type="text" class="search-input" placeholder="Search vendors..." id="searchInput" oninput="filterVendors()">
</div>
<div class="filters">
<div class="filter-btn active" onclick="setFilter('all')">All Vendors</div>
<div class="filter-btn" onclick="setFilter('success')">Working</div>
<div class="filter-btn" onclick="setFilter('with-products')">With Products</div>
<div class="filter-btn" onclick="setFilter('errors')">Errors</div>
</div>
<div class="grid-controls">
<label for="sortSelect">Sort:</label>
<select id="sortSelect" onchange="onSortChange()">
<option value="newest">Newest</option>
<option value="products">Most Products</option>
<option value="vendor">Vendor (SKU prefix) A→Z</option>
<option value="series">Series / Name A→Z</option>
<option value="sku">SKU A→Z</option>
<option value="title">Title A→Z</option>
<option value="status">Status (working first)</option>
<option value="errors">Errors first</option>
</select>
<label for="densityRange" style="margin-left:12px;">Density:</label>
<input type="range" id="densityRange" min="220" max="520" step="10" value="350" oninput="onDensityChange()">
<span id="densityVal" style="font-size:0.8rem;color:#666;min-width:48px;">350px</span>
<div class="vtoggle" style="margin-left:8px;">
<button id="vGrid" class="on" onclick="setNSKView('grid')">▦ Grid</button>
<button id="vList" onclick="setNSKView('list')">☰ List</button>
</div>
<div class="fieldmenu" style="margin-left:8px;">
<button class="fbtn" onclick="toggleNSKFieldsMenu(event)">Fields ▾</button>
<div class="fpop" id="fpop" hidden>
<label><input type="checkbox" data-nf="count" checked> Product count</label>
<label><input type="checkbox" data-nf="url" checked> Vendor URL</label>
<label><input type="checkbox" data-nf="scraper" checked> Scraper used</label>
<label><input type="checkbox" data-nf="error" checked> Error message</label>
<label><input type="checkbox" data-nf="actions" checked> Action buttons</label>
</div>
</div>
</div>
<div id="content">
<div class="loading">
<div class="spinner"></div>
<h3>Loading vendor data...</h3>
<p>Scanning all NEW-SKU scrapers for available products</p>
</div>
</div>
</div>
</div>
<script>
let vendorData = {};
let currentFilter = 'all';
let searchQuery = '';
// --- sort + density persistence (rule: hydrate BEFORE first API call) ---
const LS_SORT = 'newSkuViewer.grid.sort';
const LS_DENSITY = 'newSkuViewer.grid.density';
function getSavedSort() {
try { return localStorage.getItem(LS_SORT) || 'newest'; } catch (e) { return 'newest'; }
}
function getSavedDensity() {
try { return parseInt(localStorage.getItem(LS_DENSITY) || '350', 10) || 350; } catch (e) { return 350; }
}
function hydrateGridControls() {
const savedSort = getSavedSort();
const savedDensity = getSavedDensity();
const sortEl = document.getElementById('sortSelect');
const rangeEl = document.getElementById('densityRange');
const valEl = document.getElementById('densityVal');
if (sortEl) sortEl.value = savedSort;
if (rangeEl) rangeEl.value = String(savedDensity);
if (valEl) valEl.textContent = savedDensity + 'px';
document.documentElement.style.setProperty('--card-min', savedDensity + 'px');
}
function onSortChange() {
const v = document.getElementById('sortSelect').value;
try { localStorage.setItem(LS_SORT, v); } catch (e) {}
loadVendorData();
}
function onDensityChange() {
const v = parseInt(document.getElementById('densityRange').value, 10) || 350;
try { localStorage.setItem(LS_DENSITY, String(v)); } catch (e) {}
document.getElementById('densityVal').textContent = v + 'px';
document.documentElement.style.setProperty('--card-min', v + 'px');
}
async function loadVendorData() {
try {
const sort = encodeURIComponent(getSavedSort());
const response = await fetch('/api/vendors?sort=' + sort);
const data = await response.json();
vendorData = data;
updateStats(data.stats);
renderVendors(data.vendors);
if (data.lastUpdate) {
updateLastUpdateTime(data.lastUpdate);
}
// Update scan button state
const scanBtn = document.getElementById('scanBtn');
if (data.isScanning) {
scanBtn.disabled = true;
scanBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Scanning...';
} else {
scanBtn.disabled = false;
scanBtn.innerHTML = '<i class="fas fa-sync"></i> Scan All Vendors';
}
} catch (error) {
console.error('Error loading vendor data:', error);
document.getElementById('content').innerHTML =
'<div class="error">Failed to load vendor data. Please try again.</div>';
}
}
function updateStats(stats) {
document.getElementById('totalVendors').textContent = stats.total || 0;
document.getElementById('successfulVendors').textContent = stats.successful || 0;
document.getElementById('vendorsWithProducts').textContent = stats.withProducts || 0;
document.getElementById('totalProducts').textContent = stats.totalProducts || 0;
}
function renderVendors(vendors) {
const filteredVendors = filterVendorData(vendors);
if (Object.keys(filteredVendors).length === 0) {
document.getElementById('content').innerHTML =
'<div style="text-align: center; padding: 40px; color: white;"><h3>No vendors found matching your criteria</h3></div>';
return;
}
// Server already sorted via ?sort= — preserve that order.
const vendorCards = Object.entries(filteredVendors)
.map(([vendorId, vendor]) => createVendorCard(vendorId, vendor))
.join('');
document.getElementById('content').innerHTML =
`<div class="vendor-grid">${vendorCards}</div>`;
}
function createVendorCard(vendorId, vendor) {
const statusClass = vendor.success ?
(vendor.products > 0 ? 'success' : 'warning') : 'error';
const statusBadge = vendor.success ?
(vendor.products > 0 ?
`<span class="status-badge status-success">Active</span>` :
`<span class="status-badge status-warning">No Products</span>`) :
`<span class="status-badge status-error">Error</span>`;
const productCountClass = vendor.products > 0 ? 'has-products' : 'no-products';
const viewProductsBtn = vendor.products > 0 ?
`<button class="btn btn-primary btn-small" onclick="viewProducts('${vendorId}')">
<i class="fas fa-eye"></i> View Products
</button>` : '';
const importBtn = vendor.products > 0 ?
`<button class="btn btn-secondary btn-small" onclick="importProducts('${vendorId}')">
<i class="fas fa-download"></i> Import
</button>` : '';
return `
<div class="vendor-card ${statusClass}">
<div class="vendor-header">
<div>
<div class="vendor-name">${vendor.vendorName || vendorId}</div>
<div class="vendor-id">${vendorId}</div>
</div>
${statusBadge}
</div>
<div class="product-count ${productCountClass}">
${vendor.products || 0} products
</div>
<div class="vendor-url">
<i class="fas fa-link"></i>
${vendor.url ? vendor.url.substring(0, 50) + '...' : 'No URL'}
</div>
${vendor.scraperUsed ?
`<div class="nsk-scraper" style="font-size: 0.8rem; color: #666; margin-top: 5px;">
<i class="fas fa-robot"></i> ${vendor.scraperUsed}
</div>` : ''}
${vendor.error ? `<div class="error-message">${vendor.error}</div>` : ''}
<div class="vendor-actions">
<button class="btn btn-secondary btn-small" onclick="testVendor('${vendorId}')">
<i class="fas fa-refresh"></i> Retest
</button>
${viewProductsBtn}
${importBtn}
</div>
</div>
`;
}
function filterVendorData(vendors) {
let filtered = { ...vendors };
// Apply filter
if (currentFilter !== 'all') {
filtered = Object.fromEntries(
Object.entries(filtered).filter(([id, vendor]) => {
switch (currentFilter) {
case 'success':
return vendor.success;
case 'with-products':
return vendor.products > 0;
case 'errors':
return !vendor.success;
default:
return true;
}
})
);
}
// Apply search
if (searchQuery) {
const query = searchQuery.toLowerCase();
filtered = Object.fromEntries(
Object.entries(filtered).filter(([id, vendor]) =>
id.toLowerCase().includes(query) ||
(vendor.vendorName && vendor.vendorName.toLowerCase().includes(query))
)
);
}
return filtered;
}
function setFilter(filter) {
currentFilter = filter;
// Update filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
renderVendors(vendorData.vendors || {});
}
function filterVendors() {
searchQuery = document.getElementById('searchInput').value;
renderVendors(vendorData.vendors || {});
}
async function startScan() {
try {
const response = await fetch('/api/scan', { method: 'POST' });
const result = await response.json();
if (result.success) {
document.getElementById('scanBtn').disabled = true;
document.getElementById('scanBtn').innerHTML = '<i class="fas fa-spinner fa-spin"></i> Scanning...';
// Poll for updates every 5 seconds during scan
const pollInterval = setInterval(async () => {
await loadVendorData();
if (!vendorData.isScanning) {
clearInterval(pollInterval);
}
}, 5000);
}
} catch (error) {
console.error('Error starting scan:', error);
}
}
async function testVendor(vendorId) {
try {
const response = await fetch(`/api/test/${vendorId}`, { method: 'POST' });
const result = await response.json();
// Update the vendor in our local data
if (vendorData.vendors) {
vendorData.vendors[vendorId] = result;
renderVendors(vendorData.vendors);
}
} catch (error) {
console.error('Error testing vendor:', error);
}
}
function viewProducts(vendorId) {
const vendor = vendorData.vendors[vendorId];
if (vendor && vendor.products > 0) {
// Create a detailed product view modal
showProductModal(vendor);
} else {
alert('No products available for this vendor');
}
}
function showProductModal(vendor) {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
`;
const content = document.createElement('div');
content.style.cssText = `
background: white;
border-radius: 12px;
padding: 30px;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
`;
const sampleProducts = vendor.sampleProducts || [];
content.innerHTML = `
<button onclick="this.parentElement.parentElement.remove()" style="
position: absolute;
top: 15px;
right: 15px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
">×</button>
<h2 style="color: #333; margin-bottom: 20px;">
<i class="fas fa-boxes"></i> ${vendor.vendorName || vendor.vendor} Products
</h2>
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; text-align: center;">
<div>
<div style="font-size: 24px; font-weight: bold; color: #667eea;">${vendor.products}</div>
<div style="font-size: 14px; color: #666;">Total Products</div>
</div>
<div>
<div style="font-size: 24px; font-weight: bold; color: #10b981;">${sampleProducts.length}</div>
<div style="font-size: 14px; color: #666;">Sample Shown</div>
</div>
<div>
<div style="font-size: 16px; font-weight: bold; color: #f59e0b;">✅</div>
<div style="font-size: 14px; color: #666;">${vendor.scraperUsed || 'Scraper'}</div>
</div>
</div>
</div>
${sampleProducts.length > 0 ? `
<h3 style="color: #333; margin-bottom: 15px;">Sample Products (${sampleProducts.length} of ${vendor.products})</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;">
${sampleProducts.map((product, index) => `
<div style="
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 15px;
background: #fafafa;
transition: all 0.3s ease;
" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0,0,0,0.1)'"
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
<div style="font-weight: bold; color: #333; margin-bottom: 8px; font-size: 14px;">
${product.title || product.name || `Product ${index + 1}`}
</div>
${product.color && product.color !== 'Multiple Colors' ? `
<div style="color: #667eea; font-size: 12px; margin-bottom: 8px;">
<i class="fas fa-palette"></i> ${product.color}
</div>
` : ''}
<div style="font-family: monospace; font-size: 11px; color: #666; margin-bottom: 8px;">
Code: ${product.code || 'N/A'}
</div>
<a href="${product.url}" target="_blank" rel="noopener noreferrer" style="
display: inline-block;
background: #667eea;
color: white;
text-decoration: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
transition: background 0.3s ease;
" onmouseover="this.style.background='#5a6fd8'" onmouseout="this.style.background='#667eea'">
<i class="fas fa-external-link-alt"></i> View Product
</a>
</div>
`).join('')}
</div>
` : `
<div style="text-align: center; padding: 40px; color: #666;">
<i class="fas fa-inbox" style="font-size: 48px; margin-bottom: 15px; color: #ccc;"></i>
<p>No product details available</p>
</div>
`}
<div style="margin-top: 20px; text-align: center; border-top: 1px solid #e5e7eb; padding-top: 20px;">
<button onclick="window.open('${vendor.url}', '_blank')" style="
background: #10b981;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
margin-right: 10px;
cursor: pointer;
font-weight: 500;
">
<i class="fas fa-globe"></i> Visit Vendor Site
</button>
<button onclick="importProducts('${vendor.vendor}')" style="
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
">
<i class="fas fa-download"></i> Import to Shopify
</button>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
// Close on background click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
function importProducts(vendorId) {
const vendor = vendorData.vendors[vendorId];
if (vendor && vendor.url) {
const importUrl = `http://45.61.58.125:9830/bulk-import-new?vendor=${vendorId}&url=${encodeURIComponent(vendor.url)}`;
window.open(importUrl, '_blank');
} else {
alert('No import URL available for this vendor');
}
}
function refreshData() {
loadVendorData();
}
function updateLastUpdateTime(timestamp) {
const date = new Date(timestamp);
const timeStr = date.toLocaleString();
let updateElement = document.querySelector('.last-update');
if (!updateElement) {
updateElement = document.createElement('div');
updateElement.className = 'last-update';
document.querySelector('.container').appendChild(updateElement);
}
updateElement.textContent = `Last updated: ${timeStr}`;
}
// ── Grid / List view toggle ──
const NSK_LS_VIEW = 'newSkuViewer.grid.view';
const NSK_LS_FIELDS = 'newSkuViewer.grid.fields';
const NSK_FIELD_DEFAULTS = { count: true, url: true, scraper: true, error: true, actions: true };
function setNSKView(v) {
const g = document.getElementById('content');
const vg = document.querySelector('.vendor-grid');
if (vg) vg.classList.toggle('list', v === 'list');
document.getElementById('vGrid').classList.toggle('on', v !== 'list');
document.getElementById('vList').classList.toggle('on', v === 'list');
try { localStorage.setItem(NSK_LS_VIEW, v); } catch(e) {}
}
function getNSKFields() {
try { return Object.assign({}, NSK_FIELD_DEFAULTS, JSON.parse(localStorage.getItem(NSK_LS_FIELDS) || '{}')); }
catch(e) { return Object.assign({}, NSK_FIELD_DEFAULTS); }
}
function applyNSKFields(prefs) {
const vg = document.querySelector('.vendor-grid');
if (!vg) return;
for (const [k, on] of Object.entries(prefs)) vg.classList.toggle('hf-' + k, !on);
document.querySelectorAll('#fpop [data-nf]').forEach(cb => { cb.checked = !!prefs[cb.dataset.nf]; });
}
function toggleNSKFieldsMenu(e) {
e.stopPropagation();
const p = document.getElementById('fpop');
p.hidden = !p.hidden;
}
document.addEventListener('click', () => { const p = document.getElementById('fpop'); if (p) p.hidden = true; });
document.querySelectorAll('#fpop [data-nf]').forEach(cb => {
cb.addEventListener('change', () => {
const prefs = getNSKFields();
prefs[cb.dataset.nf] = cb.checked;
try { localStorage.setItem(NSK_LS_FIELDS, JSON.stringify(prefs)); } catch(e) {}
applyNSKFields(prefs);
});
});
// Re-apply view + fields after every render (renderVendors replaces innerHTML)
const _origRenderVendors = renderVendors;
function renderVendors(vendors) {
_origRenderVendors(vendors);
const savedView = (() => { try { return localStorage.getItem(NSK_LS_VIEW) || 'grid'; } catch(e) { return 'grid'; } })();
setNSKView(savedView);
applyNSKFields(getNSKFields());
}
// Initialize the application — hydrate sort/density from localStorage
// BEFORE the first API call so the initial fetch uses the saved sort.
hydrateGridControls();
// Restore view toggle state in the button bar
(function() { try { const v = localStorage.getItem(NSK_LS_VIEW)||'grid'; document.getElementById('vGrid').classList.toggle('on',v!=='list'); document.getElementById('vList').classList.toggle('on',v==='list'); } catch(e){} })();
applyNSKFields(getNSKFields());
loadVendorData();
// Refresh data every 30 seconds
setInterval(loadVendorData, 30000);
</script>
</body>
</html>